diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index 42b92faf2..083f0cfc0 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -456,7 +456,8 @@ <service android:name=".UpdateService" /> <service android:name=".net.WifiStateChangeService" /> - <service android:name=".localrepo.LocalRepoService" /> + <service android:name=".localrepo.LocalRepoWifiService" /> + <service android:name=".localrepo.LocalRepoProxyService" /> </application> </manifest> diff --git a/F-Droid/res/values/styles.xml b/F-Droid/res/values/styles.xml index 9626b410e..0a2dcc8b2 100644 --- a/F-Droid/res/values/styles.xml +++ b/F-Droid/res/values/styles.xml @@ -43,15 +43,20 @@ <item name="android:background">@color/white</item> </style> - <style name="SwapTheme.BluetoothDeviceList" parent="AppThemeLightWithDarkActionBar"> + <style name="SwapTheme.StartSwap.Text" parent="@style/SwapTheme.StartSwap"> + </style> + + <style name="SwapTheme.BluetoothDeviceList" parent="@style/SwapTheme.StartSwap"> </style> <style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar"> </style> - <style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.Wizard.MainText"> + <style name="SwapTheme.BluetoothDeviceList.Text" parent="@style/SwapTheme.BluetoothDeviceList"> + </style> + + <style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.BluetoothDeviceList.Text"> <item name="android:textSize">32.5dp</item> <!-- 58px * 96dpi / 160dpi = 32.5sp --> - <item name="android:textColor">#222</item> </style> <style name="SwapTheme.AppList" parent="AppThemeLightWithDarkActionBar"> diff --git a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java index 6f99b3106..7efa74615 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java @@ -52,7 +52,9 @@ import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppCacheUpdater; import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.localrepo.LocalRepoProxyService; import org.fdroid.fdroid.localrepo.LocalRepoService; +import org.fdroid.fdroid.localrepo.LocalRepoWifiService; import org.fdroid.fdroid.net.IconDownloader; import org.fdroid.fdroid.net.WifiStateChangeService; @@ -73,8 +75,6 @@ public class FDroidApp extends Application { // Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle. private static final org.spongycastle.jce.provider.BouncyCastleProvider spongyCastleProvider; - private static Messenger localRepoServiceMessenger = null; - private static boolean localRepoServiceIsBound = false; private static final String TAG = "FDroidApp"; @@ -91,6 +91,9 @@ public class FDroidApp extends Application { private static Theme curTheme = Theme.dark; + public static final LocalRepoServiceManager localRepoWifi = new LocalRepoServiceManager(LocalRepoWifiService.class); + public static final LocalRepoServiceManager localRepoProxy = new LocalRepoServiceManager(LocalRepoProxyService.class); + public void reloadTheme() { curTheme = Theme.valueOf(PreferenceManager .getDefaultSharedPreferences(getBaseContext()) @@ -302,52 +305,72 @@ public class FDroidApp extends Application { } } - private static final ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - localRepoServiceMessenger = new Messenger(service); - } - - @Override - public void onServiceDisconnected(ComponentName className) { - localRepoServiceMessenger = null; - } - }; - - public static void startLocalRepoService(Context context) { - if (!localRepoServiceIsBound) { - Context app = context.getApplicationContext(); - Intent service = new Intent(app, LocalRepoService.class); - localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE); - if (localRepoServiceIsBound) - app.startService(service); - } - } - - public static void stopLocalRepoService(Context context) { - Context app = context.getApplicationContext(); - if (localRepoServiceIsBound) { - app.unbindService(serviceConnection); - localRepoServiceIsBound = false; - } - app.stopService(new Intent(app, LocalRepoService.class)); - } - /** - * Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running. + * Helper class to encapsulate functionality relating to local repo service starting/stopping/restarting. + * It used to live as static methods in FDroidApp, but once there were two types of local repos which + * could get started (wifi and local proxy for bluetooth) then it got a bit messy. This allows us to + * support managing both of these services through two static variables + * {@link org.fdroid.fdroid.FDroidApp#localRepoProxy} and {@link org.fdroid.fdroid.FDroidApp#localRepoWifi}. */ - public static void restartLocalRepoServiceIfRunning() { - if (localRepoServiceMessenger != null) { - try { - Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0); - localRepoServiceMessenger.send(msg); - } catch (RemoteException e) { - e.printStackTrace(); + public static class LocalRepoServiceManager { + + private Messenger localRepoServiceMessenger = null; + private boolean localRepoServiceIsBound = false; + + private final Class<?extends LocalRepoService> serviceType; + + public LocalRepoServiceManager(Class<?extends LocalRepoService> serviceType) { + this.serviceType = serviceType; + } + + private ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + localRepoServiceMessenger = new Messenger(service); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + localRepoServiceMessenger = null; + } + }; + + public void start(Context context) { + if (!localRepoServiceIsBound) { + Context app = context.getApplicationContext(); + Intent service = new Intent(app, serviceType); + localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE); + if (localRepoServiceIsBound) + app.startService(service); } } - } - public static boolean isLocalRepoServiceRunning() { - return localRepoServiceIsBound; + public void stop(Context context) { + Context app = context.getApplicationContext(); + if (localRepoServiceIsBound) { + app.unbindService(serviceConnection); + localRepoServiceIsBound = false; + } + app.stopService(new Intent(app, serviceType)); + } + + /** + * Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running. + */ + public void restartIfRunning() { + if (localRepoServiceMessenger != null) { + try { + Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0); + localRepoServiceMessenger.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } + + public boolean isRunning() { + return localRepoServiceIsBound; + } + } } diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java index 8e835e2b6..7db6cca4c 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java @@ -5,11 +5,7 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.os.AsyncTask; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -20,8 +16,6 @@ import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import org.fdroid.fdroid.FDroidApp; -import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.Preferences.ChangeListener; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.net.LocalHTTPD; @@ -30,13 +24,13 @@ import org.fdroid.fdroid.views.swap.SwapActivity; import java.io.IOException; import java.net.BindException; -import java.util.HashMap; import java.util.Random; import javax.jmdns.JmDNS; import javax.jmdns.ServiceInfo; -public class LocalRepoService extends Service { +public abstract class LocalRepoService extends Service { + private static final String TAG = "LocalRepoService"; public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE"; @@ -50,9 +44,7 @@ public class LocalRepoService extends Service { private final int NOTIFICATION = R.string.local_repo_running; private Handler webServerThreadHandler = null; - private LocalHTTPD localHttpd; - private JmDNS jmdns; - private ServiceInfo pairService; + protected LocalHTTPD localHttpd; public static final int START = 1111111; public static final int STOP = 12345678; @@ -97,42 +89,6 @@ public class LocalRepoService extends Service { } } - private final BroadcastReceiver onWifiChange = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent i) { - stopNetworkServices(); - startNetworkServices(); - } - }; - - private ChangeListener localRepoBonjourChangeListener = new ChangeListener() { - @Override - public void onPreferenceChange() { - if (localHttpd.isAlive()) - if (Preferences.get().isLocalRepoBonjourEnabled()) - registerMDNSService(); - else - unregisterMDNSService(); - } - }; - - private final ChangeListener localRepoHttpsChangeListener = new ChangeListener() { - @Override - public void onPreferenceChange() { - Log.i(TAG, "onPreferenceChange"); - if (localHttpd.isAlive()) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - stopNetworkServices(); - startNetworkServices(); - return null; - } - }.execute(); - } - } - }; - private void showNotification() { notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // launch LocalRepoActivity if the user selects this notification @@ -150,12 +106,10 @@ public class LocalRepoService extends Service { @Override public void onCreate() { + super.onCreate(); + showNotification(); startNetworkServices(); - Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener); - - LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, - new IntentFilter(WifiStateChangeService.BROADCAST)); } @Override @@ -167,6 +121,7 @@ public class LocalRepoService extends Service { @Override public void onDestroy() { + super.onDestroy(); new Thread() { public void run() { stopNetworkServices(); @@ -174,8 +129,6 @@ public class LocalRepoService extends Service { }.start(); notificationManager.cancel(NOTIFICATION); - LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); - Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener); } @Override @@ -183,26 +136,40 @@ public class LocalRepoService extends Service { return messenger.getBinder(); } - private void startNetworkServices() { + /** + * Called immediately _after_ the webserver is started. + */ + protected abstract void onStartNetworkServices(); + + /** + * Called immediately _before_ the webserver is stopped. + */ + protected abstract void onStopNetworkServices(); + + /** + * Whether or not this particular version of LocalRepoService requires a HTTPS + * connection. In the local proxy instance, it will not require it, but in the + * wifi setting, it should use whatever preference the user selected. + */ + protected abstract boolean useHttps(); + + protected void startNetworkServices() { Log.d(TAG, "Starting local repo network services"); startWebServer(); - if (Preferences.get().isLocalRepoBonjourEnabled()) - registerMDNSService(); - Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener); + + onStartNetworkServices(); } - private void stopNetworkServices() { - Log.d(TAG, "Stopping local repo network services"); - Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener); - - Log.d(TAG, "Unregistering MDNS service..."); - unregisterMDNSService(); + protected void stopNetworkServices() { + onStopNetworkServices(); Log.d(TAG, "Stopping web server..."); stopWebServer(); } - private void startWebServer() { + protected abstract String getIpAddressToBindTo(); + + protected void startWebServer() { Runnable webServer = new Runnable() { // Tell Eclipse this is not a leak because of Looper use. @SuppressLint("HandlerLeak") @@ -210,8 +177,10 @@ public class LocalRepoService extends Service { public void run() { localHttpd = new LocalHTTPD( LocalRepoService.this, + getIpAddressToBindTo(), + FDroidApp.port, getFilesDir(), - Preferences.get().isLocalRepoHttpsEnabled()); + useHttps()); Looper.prepare(); // must be run before creating a Handler webServerThreadHandler = new Handler() { @@ -254,59 +223,5 @@ public class LocalRepoService extends Service { LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent); } - private void registerMDNSService() { - new Thread(new Runnable() { - @Override - public void run() { - /* - * a ServiceInfo can only be registered with a single instance - * of JmDNS, and there is only ever a single LocalHTTPD port to - * advertise anyway. - */ - if (pairService != null || jmdns != null) - clearCurrentMDNSService(); - String repoName = Preferences.get().getLocalRepoName(); - HashMap<String, String> values = new HashMap<>(); - values.put("path", "/fdroid/repo"); - values.put("name", repoName); - values.put("fingerprint", FDroidApp.repo.fingerprint); - String type; - if (Preferences.get().isLocalRepoHttpsEnabled()) { - values.put("type", "fdroidrepos"); - type = "_https._tcp.local."; - } else { - values.put("type", "fdroidrepo"); - type = "_http._tcp.local."; - } - try { - pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values); - jmdns = JmDNS.create(); - jmdns.registerService(pairService); - } catch (IOException e) { - Log.e(TAG, "Error while registering jmdns service: " + e); - Log.e(TAG, Log.getStackTraceString(e)); - } - } - }).start(); - } - - private void unregisterMDNSService() { - if (localRepoBonjourChangeListener != null) { - Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener); - localRepoBonjourChangeListener = null; - } - clearCurrentMDNSService(); - } - - private void clearCurrentMDNSService() { - if (jmdns != null) { - if (pairService != null) { - jmdns.unregisterService(pairService); - pairService = null; - } - jmdns.unregisterAllServices(); - Utils.closeQuietly(jmdns); - jmdns = null; - } - } } + diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java index c1a6a3650..af5e5bef5 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -2,9 +2,9 @@ package org.fdroid.fdroid.net; import android.content.Context; import android.util.Log; - import org.fdroid.fdroid.Preferences; +import javax.net.ssl.SSLHandshakeException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -16,8 +16,6 @@ import java.net.Proxy; import java.net.SocketAddress; import java.net.URL; -import javax.net.ssl.SSLHandshakeException; - public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; @@ -26,6 +24,7 @@ public class HttpDownloader extends Downloader { protected HttpURLConnection connection; private int statusCode = -1; + private boolean onlyStream = false; // The context is required for opening the file to write to. HttpDownloader(String source, File destFile) @@ -39,11 +38,22 @@ public class HttpDownloader extends Downloader { * you are done*. * @see org.fdroid.fdroid.net.Downloader#getFile() */ - HttpDownloader(String source, Context ctx) throws IOException { + public HttpDownloader(String source, Context ctx) throws IOException { super(ctx); sourceUrl = new URL(source); } + /** + * Calling this makes this downloader not download a file. Instead, it will + * only stream the file through the {@link HttpDownloader#getInputStream()} + * @return + */ + public HttpDownloader streamDontDownload() + { + onlyStream = true; + return this; + } + @Override public InputStream getInputStream() throws IOException { setupConnection(); diff --git a/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java b/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java index 6b403e257..c67f71077 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java +++ b/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java @@ -36,8 +36,8 @@ public class LocalHTTPD extends NanoHTTPD { private final File webRoot; private final boolean logRequests; - public LocalHTTPD(Context context, File webRoot, boolean useHttps) { - super(FDroidApp.ipAddressString, FDroidApp.port); + public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) { + super(hostname, port); this.logRequests = false; this.webRoot = webRoot; this.context = context.getApplicationContext(); diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java index bb90e1af4..8ec1527af 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -151,7 +151,7 @@ public class WifiStateChangeService extends Service { Intent intent = new Intent(BROADCAST); LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent); WifiStateChangeService.this.stopSelf(); - FDroidApp.restartLocalRepoServiceIfRunning(); + FDroidApp.localRepoWifi.restartIfRunning(); } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java b/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java index 6a5697392..1450fad45 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java @@ -70,7 +70,7 @@ public class LocalRepoActivity extends ActionBarActivity { public void onResume() { super.onResume(); resetNetworkInfo(); - setRepoSwitchChecked(FDroidApp.isLocalRepoServiceRunning()); + setRepoSwitchChecked(FDroidApp.localRepoWifi.isRunning()); LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST)); @@ -83,7 +83,7 @@ public class LocalRepoActivity extends ActionBarActivity { }).execute(); // start repo by default - FDroidApp.startLocalRepoService(LocalRepoActivity.this); + FDroidApp.localRepoWifi.start(LocalRepoActivity.this); // reset the timer if viewing this Activity again if (stopTimer != null) stopTimer.cancel(); @@ -93,7 +93,7 @@ public class LocalRepoActivity extends ActionBarActivity { @Override public void run() { - FDroidApp.stopLocalRepoService(LocalRepoActivity.this); + FDroidApp.localRepoWifi.stop(LocalRepoActivity.this); } }, 900000); // 15 minutes } @@ -210,9 +210,9 @@ public class LocalRepoActivity extends ActionBarActivity { public void onClick(View v) { setRepoSwitchChecked(repoSwitch.isChecked()); if (repoSwitch.isChecked()) { - FDroidApp.startLocalRepoService(LocalRepoActivity.this); + FDroidApp.localRepoWifi.start(LocalRepoActivity.this); } else { - FDroidApp.stopLocalRepoService(LocalRepoActivity.this); + FDroidApp.localRepoWifi.stop(LocalRepoActivity.this); stopTimer.cancel(); // disable automatic stop } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java b/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java index 81cd6d71f..6157e1729 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java @@ -33,7 +33,7 @@ public class QrWizardWifiNetworkActivity extends ActionBarActivity { wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); wifiManager.setWifiEnabled(true); - FDroidApp.startLocalRepoService(this); + FDroidApp.localRepoWifi.start(this); setContentView(R.layout.qr_wizard_activity); TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions); diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java index 324c2996a..f2bb8c786 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java +++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java @@ -21,9 +21,18 @@ public abstract class ThemeableListFragment extends ListFragment { return 0; } - protected View getHeaderView(LayoutInflater inflater, ViewGroup container) { + protected View getHeaderView() { + return headerView; + } + + private View headerView = null; + + private View getHeaderView(LayoutInflater inflater, ViewGroup container) { if (getHeaderLayout() > 0) { - return inflater.inflate(getHeaderLayout(), null, false); + if (headerView == null) { + headerView = inflater.inflate(getHeaderLayout(), null, false); + } + return headerView; } else { return null; } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java index 654b705dc..47429edf4 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java @@ -37,7 +37,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage private static final String STATE_WIFI_QR = "wifiQr"; private static final String STATE_BLUETOOTH_DEVICE_LIST = "bluetoothDeviceList"; - private static final int REQUEST_ENABLE_BLUETOOTH = 1; + private static final int REQUEST_BLUETOOTH_ENABLE = 1; + private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 2; private static final String TAG = "org.fdroid.fdroid.views.swap.SwapActivity"; @@ -109,7 +110,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage showFragment(new StartSwapFragment(), STATE_START_SWAP); - if (FDroidApp.isLocalRepoServiceRunning()) { + if (FDroidApp.localRepoWifi.isRunning()) { showSelectApps(); showJoinWifi(); attemptToShowNfc(); @@ -190,8 +191,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage } private void ensureLocalRepoRunning() { - if (!FDroidApp.isLocalRepoServiceRunning()) { - FDroidApp.startLocalRepoService(this); + if (!FDroidApp.localRepoWifi.isRunning()) { + FDroidApp.localRepoWifi.start(this); initLocalRepoTimer(900000); // 15 mins } } @@ -207,7 +208,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage shutdownLocalRepoTimer.schedule(new TimerTask() { @Override public void run() { - FDroidApp.stopLocalRepoService(SwapActivity.this); + FDroidApp.localRepoWifi.stop(SwapActivity.this); } }, timeoutMilliseconds); @@ -215,11 +216,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage @Override public void stopSwapping() { - if (FDroidApp.isLocalRepoServiceRunning()) { + if (FDroidApp.localRepoWifi.isRunning()) { if (shutdownLocalRepoTimer != null) { shutdownLocalRepoTimer.cancel(); } - FDroidApp.stopLocalRepoService(SwapActivity.this); + FDroidApp.localRepoWifi.stop(SwapActivity.this); } finish(); } @@ -242,11 +243,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter.isEnabled()) { Log.d(TAG, "Bluetooth enabled, will pair with device."); - startBluetoothServer(); + ensureBluetoothDiscoverable(); } else { Log.d(TAG, "Bluetooth disabled, asking user to enable it."); Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableBtIntent, REQUEST_ENABLE_BLUETOOTH); + startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE); } } @@ -254,22 +255,51 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_ENABLE_BLUETOOTH) { + if (requestCode == REQUEST_BLUETOOTH_ENABLE) { if (resultCode == RESULT_OK) { - Log.d(TAG, "User enabled Bluetooth, will pair with device."); - startBluetoothServer(); + Log.d(TAG, "User enabled Bluetooth, will make sure we are discoverable."); + ensureBluetoothDiscoverable(); } else { // Didn't enable bluetooth Log.d(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi)."); } + } else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) { + + if (resultCode != RESULT_CANCELED) { + Log.d(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server."); + startBluetoothServer(); + } else { + Log.d(TAG, "User chose not to make Bluetooth discoverable, so doing nothing (i.e. sticking with wifi)."); + } + + } + } + + private void ensureBluetoothDiscoverable() { + Log.d(TAG, "Ensuring Bluetooth is in discoverable mode."); + if (BluetoothAdapter.getDefaultAdapter().getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + + // TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery + // is cancelled prematurely. + + Log.d(TAG, "Not currently in discoverable mode, so prompting user to enable."); + Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); + startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE); + } else { + Log.d(TAG, "Bluetooth is already discoverable, so lets start the Bluetooth server."); + startBluetoothServer(); } } private void startBluetoothServer() { Log.d(TAG, "Starting bluetooth server."); - new BluetoothServer().start(); + if (!FDroidApp.localRepoProxy.isRunning()) { + FDroidApp.localRepoProxy.start(this); + } + new BluetoothServer(this).start(); showBluetoothDeviceList(); } diff --git a/res/layout-v14/simple_list_item_3.xml b/res/layout-v14/simple_list_item_3.xml new file mode 100644 index 000000000..c1cb020cf --- /dev/null +++ b/res/layout-v14/simple_list_item_3.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2006 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- + This file was modified for F-Droid to include an additional text item beyond + simple_list_item_3. Thought we may as well make it as much as possible the + same as the original, and so should essentially build on the original one + from the Android SDK. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" +> + + <TextView android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:textAppearance="?android:attr/textAppearanceListItem" + /> + + <TextView android:id="@android:id/text2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + /> + + <TextView android:id="@+id/text3" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + /> + +</LinearLayout> diff --git a/res/layout/simple_list_item_3.xml b/res/layout/simple_list_item_3.xml new file mode 100644 index 000000000..fdde261f8 --- /dev/null +++ b/res/layout/simple_list_item_3.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2006 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- + This file was modified for F-Droid to include an additional text item beyond + simple_list_item_3. Thought we may as well make it as much as possible the + same as the original, and so should essentially build on the original one + from the Android SDK. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" +> + + <TextView android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:textAppearance="?android:attr/textAppearanceLarge" + /> + + <TextView android:id="@android:id/text2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + /> + + <TextView android:id="@+id/text3" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + /> + +</LinearLayout> diff --git a/res/layout/swap_bluetooth_header.xml b/res/layout/swap_bluetooth_header.xml index ae9788b29..3977d3c86 100644 --- a/res/layout/swap_bluetooth_header.xml +++ b/res/layout/swap_bluetooth_header.xml @@ -9,19 +9,43 @@ android:layout_height="wrap_content"> <TextView - android:layout_width="394dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:id="@+id/device_ip_address" - tools:text="Your device name:\nPete's Nexus 4" - style="@style/SwapTheme.BluetoothDeviceList.Heading"/> + android:id="@+id/device_name_prefix" + android:text="Your device is" + tools:text="Your device is" + android:textAlignment="center" + style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingTop="10dp" + android:paddingBottom="10dp" android:textSize="24sp"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - tools:text="Select from devices below" - style="@style/SwapTheme.Wizard.Text"/> + android:id="@+id/device_name" + android:text="Pete's Nexus 4" + tools:text="Pete's Nexus 4" + android:textAlignment="center" + style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingBottom="10dp"/> - <ContentLoadingProgressBar + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/device_address" + android:text="(01:02:03:ab:cd:ef)" + tools:text="(01:02:03:ab:cd:ef)" + android:textAlignment="center" + style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingBottom="20dp" android:textSize="24sp"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Select from devices below, or tap here to scan for more devices..." + tools:text="Select from devices below, or tap here to scan for more devices..." + android:textAlignment="center" + style="@style/SwapTheme.BluetoothDeviceList.Text" android:paddingLeft="20dp" android:paddingRight="20dp" + android:paddingBottom="10dp"/> + + <android.support.v4.widget.ContentLoadingProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/loading_indicator"/> diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java new file mode 100644 index 000000000..7245f9180 --- /dev/null +++ b/src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java @@ -0,0 +1,29 @@ +package org.fdroid.fdroid.localrepo; + +/** + * Starts the local repo service but bound to 127.0.0.1. + * Also, it does not care about whether wifi is connected or not, + * and thus doesn't care about Bonjour. + */ +public class LocalRepoProxyService extends LocalRepoService { + + @Override + protected void onStartNetworkServices() { + // Do nothing + } + + @Override + protected void onStopNetworkServices() { + // Do nothing + } + + @Override + protected boolean useHttps() { + return false; + } + + @Override + protected String getIpAddressToBindTo() { + return "127.0.0.1"; + } +} diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java new file mode 100644 index 000000000..cc9cafd2a --- /dev/null +++ b/src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java @@ -0,0 +1,160 @@ +package org.fdroid.fdroid.localrepo; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.net.WifiStateChangeService; + +import javax.jmdns.JmDNS; +import javax.jmdns.ServiceInfo; +import java.io.IOException; +import java.util.HashMap; + +public class LocalRepoWifiService extends LocalRepoService { + + private static final String TAG = "org.fdroid.fdroid.localrepo.LocalRepoWifiService"; + private JmDNS jmdns; + private ServiceInfo pairService; + + private BroadcastReceiver onWifiChange = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent i) { + stopNetworkServices(); + startNetworkServices(); + } + }; + + @Override + public void onCreate() { + super.onCreate(); + Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener); + LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, + new IntentFilter(WifiStateChangeService.BROADCAST)); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener); + LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); + } + + @Override + protected void onStartNetworkServices() { + if (Preferences.get().isLocalRepoBonjourEnabled()) + registerMDNSService(); + Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener); + } + + @Override + protected void onStopNetworkServices() { + Log.d(TAG, "Stopping local repo network services"); + Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener); + + Log.d(TAG, "Unregistering MDNS service..."); + unregisterMDNSService(); + } + + @Override + protected boolean useHttps() { + return Preferences.get().isLocalRepoHttpsEnabled(); + } + + @Override + protected String getIpAddressToBindTo() { + return FDroidApp.ipAddressString; + } + + private Preferences.ChangeListener localRepoBonjourChangeListener = new Preferences.ChangeListener() { + @Override + public void onPreferenceChange() { + if (localHttpd.isAlive()) + if (Preferences.get().isLocalRepoBonjourEnabled()) + registerMDNSService(); + else + unregisterMDNSService(); + } + }; + + private Preferences.ChangeListener localRepoHttpsChangeListener = new Preferences.ChangeListener() { + @Override + public void onPreferenceChange() { + Log.i("localRepoHttpsChangeListener", "onPreferenceChange"); + if (localHttpd.isAlive()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + stopNetworkServices(); + startNetworkServices(); + return null; + } + }.execute(); + } + } + }; + + private void registerMDNSService() { + new Thread(new Runnable() { + @Override + public void run() { + /* + * a ServiceInfo can only be registered with a single instance + * of JmDNS, and there is only ever a single LocalHTTPD port to + * advertise anyway. + */ + if (pairService != null || jmdns != null) + clearCurrentMDNSService(); + String repoName = Preferences.get().getLocalRepoName(); + HashMap<String, String> values = new HashMap<String, String>(); + values.put("path", "/fdroid/repo"); + values.put("name", repoName); + values.put("fingerprint", FDroidApp.repo.fingerprint); + String type; + if (Preferences.get().isLocalRepoHttpsEnabled()) { + values.put("type", "fdroidrepos"); + type = "_https._tcp.local."; + } else { + values.put("type", "fdroidrepo"); + type = "_http._tcp.local."; + } + try { + pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values); + jmdns = JmDNS.create(); + jmdns.registerService(pairService); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + private void unregisterMDNSService() { + if (localRepoBonjourChangeListener != null) { + Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener); + localRepoBonjourChangeListener = null; + } + clearCurrentMDNSService(); + } + + private void clearCurrentMDNSService() { + if (jmdns != null) { + if (pairService != null) { + jmdns.unregisterService(pairService); + pairService = null; + } + jmdns.unregisterAllServices(); + try { + jmdns.close(); + } catch (IOException e) { + e.printStackTrace(); + } + jmdns = null; + } + } +} diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java index 4f77b80a1..886b215f6 100644 --- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java +++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java @@ -1,7 +1,7 @@ package org.fdroid.fdroid.net.bluetooth; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; import java.io.IOException; @@ -9,29 +9,17 @@ public class BluetoothClient { private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient"; - private final BluetoothAdapter adapter; private BluetoothDevice device; - public BluetoothClient() { - this.adapter = BluetoothAdapter.getDefaultAdapter(); - } - - public void pairWithDevice() throws IOException { - - if (adapter.getBondedDevices().size() == 0) { - throw new IOException("No paired Bluetooth devices."); - } - - // TODO: Don't just take a random bluetooth device :) - - device = adapter.getBondedDevices().iterator().next(); - device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid()); - + public BluetoothClient(BluetoothDevice device) { + this.device = device; } public BluetoothConnection openConnection() throws IOException { - return null; - // return new BluetoothConnection(); + BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid()); + BluetoothConnection connection = new BluetoothConnection(socket); + connection.open(); + return connection; } } diff --git a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java index 570f5eba0..76fb1c57d 100644 --- a/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java +++ b/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java @@ -3,9 +3,12 @@ package org.fdroid.fdroid.net.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; +import android.content.Context; import android.util.Log; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.net.HttpDownloader; import org.fdroid.fdroid.net.bluetooth.httpish.Request; +import org.fdroid.fdroid.net.bluetooth.httpish.Response; import java.io.IOException; import java.util.ArrayList; @@ -20,9 +23,14 @@ public class BluetoothServer extends Thread { private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer"; private BluetoothServerSocket serverSocket; - private List<Connection> clients = new ArrayList<Connection>(); + private final Context context; + + public BluetoothServer(Context context) { + this.context = context.getApplicationContext(); + } + public void close() { for (Connection connection : clients) { @@ -50,7 +58,7 @@ public class BluetoothServer extends Thread { try { BluetoothSocket clientSocket = serverSocket.accept(); if (clientSocket != null && !isInterrupted()) { - Connection client = new Connection(clientSocket); + Connection client = new Connection(context, clientSocket); client.start(); clients.add(client); } else { @@ -67,9 +75,12 @@ public class BluetoothServer extends Thread { { private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer.Connection"; - private BluetoothSocket socket; - public Connection(BluetoothSocket socket) { + private final Context context; + private final BluetoothSocket socket; + + public Connection(Context context, BluetoothSocket socket) { + this.context = context.getApplicationContext(); this.socket = socket; } @@ -81,6 +92,7 @@ public class BluetoothServer extends Thread { BluetoothConnection connection; try { connection = new BluetoothConnection(socket); + connection.open(); } catch (IOException e) { Log.e(TAG, "Error listening for incoming connections over bluetooth - " + e.getMessage()); return; @@ -91,7 +103,7 @@ public class BluetoothServer extends Thread { try { Log.d(TAG, "Listening for new Bluetooth request from client."); Request incomingRequest = Request.listenForRequest(connection); - handleRequest(incomingRequest); + handleRequest(incomingRequest).send(connection); } catch (IOException e) { Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage()); } @@ -103,10 +115,31 @@ public class BluetoothServer extends Thread { } - private void handleRequest(Request request) { + private Response handleRequest(Request request) throws IOException { Log.d(TAG, "Received Bluetooth request from client, will process it now."); + try { + HttpDownloader downloader = new HttpDownloader("http://127.0.0.1/" + request.getPath(), context); + + Response.ResponseBuilder builder; + + if (request.getMethod().equals(Request.Methods.HEAD)) { + builder = new Response.ResponseBuilder(); + } else { + builder = new Response.ResponseBuilder(downloader.getInputStream()); + } + + // TODO: At this stage, will need to download the file to get this info. + // However, should be able to make totalDownloadSize and getCacheTag work without downloading. + return builder + .setStatusCode(200) + .build(); + + } catch (IOException e) { + throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e); + } + } } } diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java index 3066c7142..be10cd4e3 100644 --- a/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java +++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java @@ -1,8 +1,13 @@ package org.fdroid.fdroid.net.bluetooth.httpish; +import android.util.Log; import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -10,6 +15,8 @@ import java.util.Map; public class Request { + private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Request"; + public static interface Methods { public static final String HEAD = "HEAD"; public static final String GET = "GET"; @@ -27,6 +34,9 @@ public class Request { this.method = method; this.path = path; this.connection = connection; + + output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); + input = new BufferedReader(new InputStreamReader(connection.getInputStream())); } public static Request createHEAD(String path, BluetoothConnection connection) @@ -40,8 +50,7 @@ public class Request { public Response send() throws IOException { - output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); - input = new BufferedReader(new InputStreamReader(connection.getInputStream())); + Log.d(TAG, "Sending request to server (" + path + ")"); output.write(method); output.write(' '); @@ -49,12 +58,23 @@ public class Request { output.write("\n\n"); + output.flush(); + + Log.d(TAG, "Finished sending request, now attempting to read response status code..."); + int responseCode = readResponseCode(); + + Log.d(TAG, "Read response code " + responseCode + " from server, now reading headers..."); + Map<String, String> headers = readHeaders(); + Log.d(TAG, "Read " + headers.size() + " headers"); + if (method.equals(Methods.HEAD)) { + Log.d(TAG, "Request was a " + Methods.HEAD + " request, not including anything other than headers and status..."); return new Response(responseCode, headers); } else { + Log.d(TAG, "Request was a " + Methods.GET + " request, so including content stream in response..."); return new Response(responseCode, headers, connection.getInputStream()); } @@ -109,9 +129,9 @@ public class Request { // TODO: Error handling int firstSpace = line.indexOf(' '); - int secondSpace = line.indexOf(' ', firstSpace); + int secondSpace = line.indexOf(' ', firstSpace + 1); - String status = line.substring(firstSpace, secondSpace); + String status = line.substring(firstSpace + 1, secondSpace); return Integer.parseInt(status); } @@ -135,6 +155,12 @@ public class Request { return headers; } + public String getPath() { + return path; + } + public String getMethod() { + return method; + } } diff --git a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java index fcf55eb28..44c8c900e 100644 --- a/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java +++ b/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java @@ -1,13 +1,22 @@ package org.fdroid.fdroid.net.bluetooth.httpish; +import android.util.Log; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; import org.fdroid.fdroid.net.bluetooth.FileDetails; import org.fdroid.fdroid.net.bluetooth.httpish.headers.Header; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.HashMap; import java.util.Map; public class Response { + private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Response"; + private int statusCode; private Map<String, String> headers; private final InputStream contentStream; @@ -44,13 +53,80 @@ public class Response { return details; } - /** - * After parsing a response, - */ public InputStream toContentStream() throws UnsupportedOperationException { if (contentStream == null) { throw new UnsupportedOperationException("This kind of response doesn't have a content stream. Did you perform a HEAD request instead of a GET request?"); } return contentStream; } + + public void send(BluetoothConnection connection) throws IOException { + + Log.d(TAG, "Sending Bluetooth HTTP-ish response..."); + + Writer output = new OutputStreamWriter(connection.getOutputStream()); + output.write("HTTP(ish)/0.1 200 OK\n"); + + for (Map.Entry<String, String> entry : headers.entrySet()) { + output.write(entry.getKey()); + output.write(": "); + output.write(entry.getValue()); + output.write("\n"); + } + + output.write("\n"); + output.flush(); + + if (contentStream != null) { + Utils.copy(contentStream, connection.getOutputStream()); + } + + output.flush(); + + } + + public static class ResponseBuilder { + + private InputStream contentStream; + private int statusCode = 200; + private int fileSize = -1; + private String etag = null; + + public ResponseBuilder() {} + + public ResponseBuilder(InputStream contentStream) { + this.contentStream = contentStream; + } + + public ResponseBuilder setStatusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public ResponseBuilder setFileSize(int fileSize) { + this.fileSize = fileSize; + return this; + } + + public ResponseBuilder setETag(String etag) { + this.etag = etag; + return this; + } + + public Response build() { + + Map<String, String> headers = new HashMap<>(3); + + if (fileSize > 0) { + headers.put("Content-Length", Integer.toString(fileSize)); + } + + if (etag != null) { + headers.put( "ETag", etag); + } + + return new Response(statusCode, headers, contentStream); + } + + } } diff --git a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java index f9e68699b..2467aea31 100644 --- a/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java +++ b/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListFragment.java @@ -1,26 +1,168 @@ package org.fdroid.fdroid.views.swap; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; import android.content.Context; -import android.database.Cursor; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; +import android.support.v4.widget.ContentLoadingProgressBar; +import android.util.Log; import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; -import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.InstalledAppProvider; +import org.fdroid.fdroid.net.bluetooth.BluetoothClient; +import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; +import org.fdroid.fdroid.net.bluetooth.httpish.Request; +import org.fdroid.fdroid.net.bluetooth.httpish.Response; import org.fdroid.fdroid.views.fragments.ThemeableListFragment; +import java.io.IOException; import java.util.List; public class BluetoothDeviceListFragment extends ThemeableListFragment { + private static final String TAG = "org.fdroid.fdroid.views.swap.BluetoothDeviceListFragment"; + private Adapter adapter = null; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(false); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + setEmptyText("No bluetooth devices found. Is the other device \"discoverable\"?"); + + adapter = new Adapter( + new ContextThemeWrapper(getActivity(), R.style.SwapTheme_BluetoothDeviceList_ListItem), + R.layout.select_local_apps_list_item + ); + + populateDeviceList(); + + setListAdapter(adapter); + setListShown(false); // start out with a progress indicator + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + View headerView = getHeaderView(); + if (headerView == null) { + Log.e(TAG, "Could not find header view, although we expected one to exist."); + } else { + headerView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + initiateBluetoothScan(); + return true; + } + }); + } + return view; + } + + private void initiateBluetoothScan() + { + BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter(); + BroadcastReceiver deviceFoundReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + boolean exists = false; + for (int i = 0; i < adapter.getCount(); i ++) { + if (adapter.getItem(i).getAddress().equals(device.getAddress())) { + exists = true; + break; + } + } + + if (!exists) { + adapter.add(device); + } + } + } + }; + + ((ContentLoadingProgressBar)getView().findViewById(R.id.loading_indicator)).show(); + getActivity().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND)); + + if (!bluetooth.startDiscovery()) { + // TODO: Discovery did not start for some reason :( + Log.e(TAG, "Could not start bluetooth discovery, but am not sure why :("); + } + } + + private void populateDeviceList() + { + for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) { + adapter.add(device); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + + // "position" includes the header view, so ignore that. + if (position == 0) { + return; + } + + BluetoothDevice device = adapter.getItem(position - 1); + + // TODO: I think that I can connect regardless of the bond state. + // It sounds like when I attempt to connect to a non-bonded peer, then + // Android initiates the pairing dialog on our behalf. + + BluetoothClient client = new BluetoothClient(device); + + try { + Log.d(TAG, "Testing bluetooth connection (opening connection first)."); + BluetoothConnection connection = client.openConnection(); + Log.d(TAG, "Creating HEAD request for resource at \"/\"..."); + Request head = Request.createHEAD("/", connection); + Log.d(TAG, "Sending request..."); + Response response = head.send(); + Log.d(TAG, "Response from bluetooth: " + response.getStatusCode()); + } catch (IOException e) { + + } + + /*if (device.getBondState() == BluetoothDevice.BOND_NONE) { + // attempt to bond + + } else if (device.getBondState() == BluetoothDevice.BOND_BONDING) { + // wait for bonding to finish + + } else if (device.getBondState() == BluetoothDevice.BOND_BONDED) { + // connect + BluetoothClient client = new BluetoothClient(device); + }*/ + } + + @Override + protected int getThemeStyle() { + return R.style.SwapTheme_BluetoothDeviceList; + } + + @Override + protected int getHeaderLayout() { + return R.layout.swap_bluetooth_header; + } + private class Adapter extends ArrayAdapter<BluetoothDevice> { public Adapter(Context context, int resource) { @@ -51,61 +193,37 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment { public View getView(int position, View convertView, ViewGroup parent) { View view; if (convertView == null) { - view = getActivity().getLayoutInflater().inflate(android.R.layout.simple_list_item_2, parent); + view = getActivity().getLayoutInflater().inflate(R.layout.simple_list_item_3, null); } else { view = convertView; } BluetoothDevice device = getItem(position); TextView nameView = (TextView)view.findViewById(android.R.id.text1); - TextView descriptionView = (TextView)view.findViewById(android.R.id.text2); + TextView addressView = (TextView)view.findViewById(android.R.id.text2); + TextView descriptionView = (TextView)view.findViewById(R.id.text3); nameView.setText(device.getName()); - descriptionView.setText(device.getAddress()); + addressView.setText(device.getAddress()); + descriptionView.setText(bondStateToLabel(device.getBondState())); return view; } - } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - setEmptyText("No bluetooth devices found. Is the other device \"discoverable\"?"); - - Adapter adapter = new Adapter( - new ContextThemeWrapper(getActivity(), R.style.SwapTheme_BluetoothDeviceList_ListItem), - R.layout.select_local_apps_list_item - ); - - setListAdapter(adapter); - setListShown(false); // start out with a progress indicator - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Cursor c = (Cursor) l.getAdapter().getItem(position); - String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); - if (FDroidApp.selectedApps.contains(packageName)) { - FDroidApp.selectedApps.remove(packageName); - } else { - FDroidApp.selectedApps.add(packageName); + private String bondStateToLabel(int deviceBondState) + { + if (deviceBondState == BluetoothDevice.BOND_BONDED) { + // TODO: Is the term "Bonded device" common parlance among phone users? + // It sounds a bit technical to me, maybe something more lay like "Previously connected". + // Although it is technically not as accurate, it would make sense to more people... + return "Bonded"; + } else if (deviceBondState == BluetoothDevice.BOND_BONDING) { + return "Currently bonding..."; + } else { + // TODO: Might be a little bit harsh, makes it sound more malicious than it should. + return "Unknown device"; + } } } - @Override - protected int getThemeStyle() { - return R.style.SwapTheme_StartSwap; - } - - @Override - protected int getHeaderLayout() { - return R.layout.swap_bluetooth_header; - } }