From 9180c9f5c03b8ec0177eaf7544402d63aca3de48 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 20 Jul 2015 23:57:16 +1000 Subject: [PATCH] WIP: Bluetooth can actually transfer indexes and apks over HTTPish! It is very hacky, and I did it through the non-swap interface, and it only works once then the state stuffs up and it no longer accepts incomming connections, but it worked! Now to smooth out all the things. --- F-Droid/res/layout/swap_bluetooth_devices.xml | 10 - .../fdroid/fdroid/localrepo/SwapService.java | 56 ++- .../localrepo/peers/BluetoothFinder.java | 9 +- .../fdroid/localrepo/peers/BluetoothPeer.java | 7 +- .../fdroid/localrepo/peers/BonjourFinder.java | 8 +- .../fdroid/localrepo/type/BluetoothSwap.java | 45 ++- .../fdroid/localrepo/type/SwapType.java | 17 +- .../fdroid/localrepo/type/WifiSwap.java | 5 + .../fdroid/net/BluetoothDownloader.java | 15 +- .../fdroid/fdroid/net/DownloaderFactory.java | 8 +- .../fdroid/net/bluetooth/BluetoothClient.java | 7 +- .../net/bluetooth/BluetoothConnection.java | 1 + .../fdroid/net/bluetooth/BluetoothServer.java | 69 ++-- .../views/swap/BluetoothDeviceListView.java | 376 ------------------ .../fdroid/views/swap/SelectAppsView.java | 2 +- .../views/swap/SwapWorkflowActivity.java | 98 +++-- 16 files changed, 220 insertions(+), 513 deletions(-) delete mode 100644 F-Droid/res/layout/swap_bluetooth_devices.xml delete mode 100644 F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java diff --git a/F-Droid/res/layout/swap_bluetooth_devices.xml b/F-Droid/res/layout/swap_bluetooth_devices.xml deleted file mode 100644 index fd16a39ed..000000000 --- a/F-Droid/res/layout/swap_bluetooth_devices.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java index 3b376095b..5ef60e9c5 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java @@ -39,7 +39,6 @@ import org.fdroid.fdroid.localrepo.peers.BluetoothFinder; import org.fdroid.fdroid.localrepo.peers.BonjourFinder; import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.localrepo.type.BluetoothSwap; -import org.fdroid.fdroid.localrepo.type.BonjourBroadcast; import org.fdroid.fdroid.localrepo.type.SwapType; import org.fdroid.fdroid.localrepo.type.WifiSwap; import org.fdroid.fdroid.net.WifiStateChangeService; @@ -65,8 +64,10 @@ import java.util.TimerTask; public class SwapService extends Service { private static final String TAG = "SwapManager"; - private static final String SHARED_PREFERENCES = "swap-state"; + public static final String SHARED_PREFERENCES = "swap-state"; private static final String KEY_APPS_TO_SWAP = "appsToSwap"; + private static final String KEY_BLUETOOTH_ENABLED = "bluetoothEnabled"; + private static final String KEY_WIFI_ENABLED = "wifiEnabled"; @NonNull private Set appsToSwap = new HashSet<>(); @@ -353,6 +354,25 @@ public class SwapService extends Service { } + // ============================================================= + // Remember which swap technologies a user used in the past + // ============================================================= + + private void persistPreferredSwapTypes() { + persistence().edit() + .putBoolean(KEY_BLUETOOTH_ENABLED, bluetoothSwap.isConnected()) + .putBoolean(KEY_WIFI_ENABLED, wifiSwap.isConnected()) + .commit(); + } + + private boolean wasBluetoothEnabled() { + return persistence().getBoolean(KEY_BLUETOOTH_ENABLED, false); + } + + private boolean wasWifiEnabled() { + return persistence().getBoolean(KEY_WIFI_ENABLED, false); + } + // ========================================== // Local repo stop/start/restart handling // ========================================== @@ -409,6 +429,14 @@ public class SwapService extends Service { // Interacting with Bluetooth adapter // ========================================== + public BonjourFinder getBonjourFinder() { + return bonjourFinder; + } + + public BluetoothFinder getBluetoothFinder() { + return bluetoothFinder; + } + public boolean isBluetoothDiscoverable() { return bluetoothSwap.isConnected(); } @@ -487,6 +515,16 @@ public class SwapService extends Service { IntentFilter filter = new IntentFilter(BLUETOOTH_STATE_CHANGE); filter.addAction(WIFI_STATE_CHANGE); LocalBroadcastManager.getInstance(this).registerReceiver(receiveSwapStatusChanged, filter); + + if (wasBluetoothEnabled()) { + Log.d(TAG, "Previously the user enabled Bluetooth swap, so enabling again automatically."); + bluetoothSwap.startInBackground(); + } + + if (wasWifiEnabled()) { + Log.d(TAG, "Previously the user enabled Wifi swap, so enabling again automatically."); + wifiSwap.startInBackground(); + } } /** @@ -505,6 +543,7 @@ public class SwapService extends Service { detachService(); } } + persistPreferredSwapTypes(); } }; @@ -520,15 +559,10 @@ public class SwapService extends Service { } public void disableAllSwapping() { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - getBluetoothSwap().stop(); - getWifiSwap().stop(); - detachService(); - return null; - } - }.execute(); + Log.i(TAG, "Asked to stop swapping, will stop bluetooth, wifi, and move service to BG for GC."); + getBluetoothSwap().stopInBackground(); + getWifiSwap().stopInBackground(); + detachService(); } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java index 6de94e849..d14f07bc0 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java @@ -8,9 +8,8 @@ import android.content.Intent; import android.content.IntentFilter; import android.util.Log; -import org.fdroid.fdroid.net.bluetooth.BluetoothServer; +import org.fdroid.fdroid.localrepo.type.BluetoothSwap; -// TODO: Still to be implemented public class BluetoothFinder extends PeerFinder { private static final String TAG = "BluetoothFinder"; @@ -69,8 +68,8 @@ public class BluetoothFinder extends PeerFinder { if (adapter.isDiscovering()) { // TODO: Can we reset the discovering timeout, so that it doesn't, e.g. time out // in 3 seconds because we had already almost completed the previous scan? - Log.d(TAG, "Requested bluetooth scan when already scanning, will cancel previous scan before continuing."); - adapter.cancelDiscovery(); + Log.d(TAG, "Requested bluetooth scan when already scanning, so will ignore request."); + return; } if (!adapter.startDiscovery()) { @@ -90,7 +89,7 @@ public class BluetoothFinder extends PeerFinder { } private void onDeviceFound(BluetoothDevice device) { - if (device != null && device.getName() != null && device.getName().startsWith(BluetoothServer.BLUETOOTH_NAME_TAG)) { + if (device != null && device.getName() != null && device.getName().startsWith(BluetoothSwap.BLUETOOTH_NAME_TAG)) { foundPeer(new BluetoothPeer(device)); } } diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java index ca3f58210..68e5606f2 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java @@ -4,9 +4,8 @@ import android.bluetooth.BluetoothDevice; import android.os.Parcel; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.net.bluetooth.BluetoothServer; +import org.fdroid.fdroid.localrepo.type.BluetoothSwap; -// TODO: Still to be implemented. public class BluetoothPeer implements Peer { private BluetoothDevice device; @@ -22,7 +21,7 @@ public class BluetoothPeer implements Peer { @Override public String getName() { - return device.getName().replaceAll("^" + BluetoothServer.BLUETOOTH_NAME_TAG, ""); + return device.getName().replaceAll("^" + BluetoothSwap.BLUETOOTH_NAME_TAG, ""); } @Override @@ -37,7 +36,7 @@ public class BluetoothPeer implements Peer { @Override public String getRepoAddress() { - return "bluetooth://" + device.getAddress() + "/fdroid/repo"; + return "bluetooth://" + device.getAddress().replace(':', '-') + "/fdroid/repo"; } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java index a6ad807e5..a00d21af5 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java @@ -68,6 +68,7 @@ public class BonjourFinder extends PeerFinder implements ServiceLis @Override protected void onPostExecute(Void result) { + // TODO: This is not threadsafe - cancelling the discovery will make jmdns null, but it could happen after this check and before call to addServiceListener(). if (jmdns != null) { Log.d(TAG, "Adding mDNS service listeners for " + HTTP_SERVICE_TYPE + " and " + HTTPS_SERVICE_TYPE); jmdns.addServiceListener(HTTP_SERVICE_TYPE, BonjourFinder.this); @@ -80,13 +81,16 @@ public class BonjourFinder extends PeerFinder implements ServiceLis } private void listServices() { + + final JmDNS mdns = jmdns; + new AsyncTask() { @Override protected Void doInBackground(Void... params) { Log.d(TAG, "Explicitly querying for services, in addition to waiting for notifications."); - addFDroidServices(jmdns.list(HTTP_SERVICE_TYPE)); - addFDroidServices(jmdns.list(HTTPS_SERVICE_TYPE)); + addFDroidServices(mdns.list(HTTP_SERVICE_TYPE)); + addFDroidServices(mdns.list(HTTPS_SERVICE_TYPE)); return null; } diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java index e7d4504dd..5e01d59df 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import org.fdroid.fdroid.localrepo.SwapService; @@ -14,11 +15,15 @@ import org.fdroid.fdroid.net.bluetooth.BluetoothServer; public class BluetoothSwap extends SwapType { private static final String TAG = "BluetoothBroadcastType"; + public final static String BLUETOOTH_NAME_TAG = "FDroid:"; @NonNull private final BluetoothAdapter adapter; - private final BluetoothServer server; + @Nullable + private BluetoothServer server; + + private String deviceBluetoothName = null; public static SwapType create(@NonNull Context context) { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); @@ -27,12 +32,11 @@ public class BluetoothSwap extends SwapType { } else { return new BluetoothSwap(context, adapter); } - }; + } private BluetoothSwap(@NonNull Context context, @NonNull BluetoothAdapter adapter) { super(context); this.adapter = adapter; - this.server = new BluetoothServer(context, context.getFilesDir()); context.registerReceiver(new BroadcastReceiver() { @Override @@ -52,17 +56,39 @@ public class BluetoothSwap extends SwapType { }, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)); } + @Override + public boolean isConnected() { + return server != null && server.isRunning() && super.isConnected(); + } + @Override public void start() { - if (server.isAlive()) { - Log.d(TAG, "Attempting to start Bluetooth swap, but it appears to be running already."); - return; + if (server != null) { + Log.d(TAG, "Attempting to start Bluetooth swap, but it appears to be running already. Will cancel it so it can be restarted."); + server.close(); + server = null; } + server = new BluetoothServer(this, context.getFilesDir()); + sendBroadcast(SwapService.EXTRA_STARTING); + //store the original bluetoothname, and update this one to be unique + deviceBluetoothName = adapter.getName(); + + Log.d(TAG, "Prefixing Bluetooth adapter name with " + BLUETOOTH_NAME_TAG + " to make it identifiable as a swap device."); + if (!deviceBluetoothName.startsWith(BLUETOOTH_NAME_TAG)) + adapter.setName(BLUETOOTH_NAME_TAG + deviceBluetoothName); + + if (!adapter.getName().startsWith(BLUETOOTH_NAME_TAG)) { + Log.e(TAG, "Couldn't change the name of the Bluetooth adapter, it will not get recognized by other swap clients."); + // TODO: Should we bail here? + } + if (!adapter.isEnabled()) { + Log.d(TAG, "Bluetooth adapter is disabled, attempting to enable."); if (!adapter.enable()) { + Log.d(TAG, "Could not enable Bluetooth adapter, so bailing out of Bluetooth swap."); setConnected(false); return; } @@ -79,7 +105,7 @@ public class BluetoothSwap extends SwapType { @Override public void stop() { - if (server.isAlive()) { + if (server != null && server.isAlive()) { server.close(); setConnected(false); } else { @@ -87,6 +113,11 @@ public class BluetoothSwap extends SwapType { } } + protected void onStopped() { + Log.d(TAG, "Resetting bluetooth device name to " + deviceBluetoothName + " after swapping."); + adapter.setName(deviceBluetoothName); + } + @Override public String getBroadcastAction() { return SwapService.BLUETOOTH_STATE_CHANGE; diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java index 9f8d48371..1e6f781a1 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid.localrepo.type; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; @@ -38,10 +39,13 @@ public abstract class SwapType { sendBroadcast(SwapService.EXTRA_STARTED); } else { isConnected = false; + onStopped(); sendBroadcast(SwapService.EXTRA_STOPPED); } } + protected void onStopped() {} + /** * Sends either a {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTING}, * {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTED} or @@ -55,7 +59,7 @@ public abstract class SwapType { } } - public final boolean isConnected() { + public boolean isConnected() { return isConnected; } @@ -84,4 +88,15 @@ public abstract class SwapType { } }.execute(); } + + public void stopInBackground() { + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + stop(); + return null; + } + }.execute(); + } + } diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiSwap.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiSwap.java index 774b2a4ee..80dc2bc99 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiSwap.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiSwap.java @@ -60,19 +60,24 @@ public class WifiSwap extends SwapType { @Override public void handleMessage(Message msg) { Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj); + setConnected(false); localHttpd.stop(); } }; try { Log.d(TAG, "Starting swap webserver..."); + sendBroadcast(SwapService.EXTRA_STARTING); localHttpd.start(); + setConnected(true); Log.d(TAG, "Swap webserver started."); } catch (BindException e) { int prev = FDroidApp.port; FDroidApp.port = FDroidApp.port + new Random().nextInt(1111); + setConnected(false); Log.w(TAG, "port " + prev + " occupied, trying on " + FDroidApp.port + "!"); context.startService(new Intent(context, WifiStateChangeService.class)); } catch (IOException e) { + setConnected(false); Log.e(TAG, "Could not start local repo HTTP server: " + e); Log.e(TAG, Log.getStackTraceString(e)); } diff --git a/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java index a860e2aa2..7f90508f7 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid.net; import android.content.Context; import android.util.Log; import org.apache.commons.io.input.BoundedInputStream; +import org.fdroid.fdroid.net.bluetooth.BluetoothClient; import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; import org.fdroid.fdroid.net.bluetooth.FileDetails; import org.fdroid.fdroid.net.bluetooth.httpish.Request; @@ -23,21 +24,15 @@ public class BluetoothDownloader extends Downloader { private FileDetails fileDetails; private final String sourcePath; - public BluetoothDownloader(BluetoothConnection connection, String sourcePath, Context ctx) throws IOException { + public BluetoothDownloader(String macAddress, String sourcePath, Context ctx) throws IOException { super(ctx); - this.connection = connection; + this.connection = new BluetoothClient(macAddress).openConnection(); this.sourcePath = sourcePath; } - public BluetoothDownloader(BluetoothConnection connection, String sourcePath, File destFile) throws FileNotFoundException, MalformedURLException { + public BluetoothDownloader(String macAddress, String sourcePath, File destFile) throws IOException { super(destFile); - this.connection = connection; - this.sourcePath = sourcePath; - } - - public BluetoothDownloader(BluetoothConnection connection, String sourcePath, OutputStream output) throws MalformedURLException { - super(output); - this.connection = connection; + this.connection = new BluetoothClient(macAddress).openConnection(); this.sourcePath = sourcePath; } diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index c03eb0376..6c99584e4 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -11,8 +11,8 @@ public class DownloaderFactory { public static Downloader create(String url, Context context) throws IOException { Uri uri = Uri.parse(url); if (isBluetoothAddress(uri)) { - // TODO: Don't pass null!!! - return new BluetoothDownloader(null, uri.getPath(), context); + String macAddress = uri.getHost().replace("-", ":"); + return new BluetoothDownloader(macAddress, uri.getPath(), context); } else if (isOnionAddress(url)) { return new TorHttpDownloader(url, context); } else { @@ -23,8 +23,8 @@ public class DownloaderFactory { public static Downloader create(String url, File destFile) throws IOException { Uri uri = Uri.parse(url); if (isBluetoothAddress(uri)) { - // TODO: Don't pass null!!! - return new BluetoothDownloader(null, uri.getPath(), destFile); + String macAddress = uri.getHost().replace("-", ":"); + return new BluetoothDownloader(macAddress, uri.getPath(), destFile); } else if (isOnionAddress(url)) { return new TorHttpDownloader(url, destFile); } else { diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java index 3179c259e..21308b0f2 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java +++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.net.bluetooth; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; @@ -10,12 +11,16 @@ public class BluetoothClient { @SuppressWarnings("unused") private static final String TAG = "BluetoothClient"; - private BluetoothDevice device; + private final BluetoothDevice device; public BluetoothClient(BluetoothDevice device) { this.device = device; } + public BluetoothClient(String macAddress) { + device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress); + } + public BluetoothConnection openConnection() throws IOException { BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid()); BluetoothConnection connection = new BluetoothConnection(socket); diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java index c74f1e3cd..bc1d5bcf5 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java +++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.net.bluetooth; import android.annotation.TargetApi; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.os.Build; import android.util.Log; diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java index 8328e8ec7..0306710c4 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java +++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java @@ -3,25 +3,19 @@ package org.fdroid.fdroid.net.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; -import android.content.Context; -import android.os.Build; import android.util.Log; import android.webkit.MimeTypeMap; -import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.net.HttpDownloader; +import org.fdroid.fdroid.localrepo.type.BluetoothSwap; import org.fdroid.fdroid.net.bluetooth.httpish.Request; import org.fdroid.fdroid.net.bluetooth.httpish.Response; import java.io.File; import java.io.FileInputStream; -import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,23 +31,23 @@ public class BluetoothServer extends Thread { private static final String TAG = "BluetoothServer"; private BluetoothServerSocket serverSocket; - private List clients = new ArrayList<>(); + private List clients = new ArrayList<>(); - private final Context context; - - private String deviceBluetoothName = null; - public final static String BLUETOOTH_NAME_TAG = "FDroid:"; private final File webRoot; + private final BluetoothSwap swap; + private boolean isRunning = false; - public BluetoothServer(Context context, File webRoot) { - this.context = context.getApplicationContext(); + public BluetoothServer(BluetoothSwap swap, File webRoot) { this.webRoot = webRoot; + this.swap = swap; } + public boolean isRunning() { return isRunning; } + public void close() { - for (Connection connection : clients) { - connection.interrupt(); + for (ClientConnection clientConnection : clients) { + clientConnection.interrupt(); } interrupt(); @@ -61,61 +55,53 @@ public class BluetoothServer extends Thread { if (serverSocket != null) { Utils.closeQuietly(serverSocket); } - - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - adapter.setName(deviceBluetoothName.replaceAll("/^" + BLUETOOTH_NAME_TAG + "/","")); - } @Override public void run() { + isRunning = true; BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - - //store the original bluetoothname, and update this one to be unique - deviceBluetoothName = adapter.getName(); - - if (!deviceBluetoothName.startsWith(BLUETOOTH_NAME_TAG)) - adapter.setName(BLUETOOTH_NAME_TAG + deviceBluetoothName); - - try { serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid()); } catch (IOException e) { - Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now - " + e.getMessage()); + Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now: " + e.getMessage()); + swap.stop(); + isRunning = false; return; } while (true) { if (isInterrupted()) { + Log.d(TAG, "Server stopped so will terminate loop looking for client connections."); break; } try { BluetoothSocket clientSocket = serverSocket.accept(); - if (clientSocket != null && !isInterrupted()) { - Connection client = new Connection(context, clientSocket, webRoot); + if (clientSocket != null) { + if (!isInterrupted()) { + Log.d(TAG, "Server stopped after socket accepted from client, but before initiating connection."); + break; + } + ClientConnection client = new ClientConnection(clientSocket, webRoot); client.start(); clients.add(client); - } else { - break; } } catch (IOException e) { - Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients - " + e.getMessage()); + Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients: " + e.getMessage()); } } - + isRunning = false; } - private static class Connection extends Thread { + private static class ClientConnection extends Thread { - private final Context context; private final BluetoothSocket socket; private final File webRoot; - public Connection(Context context, BluetoothSocket socket, File webRoot) { - this.context = context.getApplicationContext(); + public ClientConnection(BluetoothSocket socket, File webRoot) { this.socket = socket; this.webRoot = webRoot; } @@ -142,13 +128,11 @@ public class BluetoothServer extends Thread { handleRequest(incomingRequest).send(connection); } catch (IOException e) { Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage()); - - + break; } if (isInterrupted()) break; - } } @@ -160,7 +144,6 @@ public class BluetoothServer extends Thread { Response.Builder builder = null; try { -// HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port) + "/" + request.getPath(), context); int statusCode = 404; int totalSize = -1; diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java deleted file mode 100644 index ef0d06888..000000000 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/BluetoothDeviceListView.java +++ /dev/null @@ -1,376 +0,0 @@ -package org.fdroid.fdroid.views.swap; - -import android.annotation.TargetApi; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.view.MenuItemCompat; -import android.support.v4.widget.ContentLoadingProgressBar; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.localrepo.SwapService; -import org.fdroid.fdroid.net.BluetoothDownloader; -import org.fdroid.fdroid.net.bluetooth.BluetoothClient; -import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; -import org.fdroid.fdroid.net.bluetooth.BluetoothServer; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.List; - -public class BluetoothDeviceListView extends ListView implements - SwapWorkflowActivity.InnerView, - ListView.OnItemClickListener { - - private static final String TAG = "BluetoothDeviceListView"; - - private Adapter adapter = null; - - private MenuItem scanMenuItem; - private MenuItem cancelMenuItem; - - private boolean firstScan = true; - - public BluetoothDeviceListView(Context context) { - super(context); - } - - public BluetoothDeviceListView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public BluetoothDeviceListView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public BluetoothDeviceListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - @Override - public boolean buildMenu(Menu menu, @NonNull MenuInflater menuInflater) { - menuInflater.inflate(R.menu.swap_scan, menu); - - final int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; - - scanMenuItem = menu.findItem(R.id.action_scan); - scanMenuItem.setVisible(true); - MenuItemCompat.setShowAsAction(scanMenuItem, flags); - - scanMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - initiateBluetoothScan(); - return true; - } - }); - - cancelMenuItem = menu.findItem(R.id.action_cancel); - cancelMenuItem.setVisible(false); - MenuItemCompat.setShowAsAction(cancelMenuItem, flags); - - cancelMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - cancelBluetoothScan(); - return true; - } - }); - - return true; - } - - @Override - public int getStep() { - return SwapService.STEP_BLUETOOTH; - } - - @Override - public int getPreviousStep() { - return SwapService.STEP_JOIN_WIFI; - } - - @Override - public int getToolbarColour() { - return R.color.swap_blue; - } - - @Override - public String getToolbarTitle() { - return getContext().getString(R.string.swap_use_bluetooth); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - adapter = new Adapter( - getContext(), - R.layout.select_local_apps_list_item - ); - - LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View headerView = inflater.inflate(R.layout.swap_bluetooth_header, this, false); - addHeaderView(headerView); - - setAdapter(adapter); - setOnItemClickListener(this); - - final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter(); - - final TextView deviceName = (TextView) headerView.findViewById(R.id.device_name); - deviceName.setText(bluetooth.getName()); - - final TextView address = (TextView) headerView.findViewById(R.id.device_address); - address.setText(bluetooth.getAddress()); - - initiateBluetoothScan(); - - // populateBondedDevices(); - - } - - - - private void cancelBluetoothScan() { - - Log.d(TAG, "Cancelling bluetooth scan."); - - cancelMenuItem.setVisible(false); - scanMenuItem.setVisible(true); - - final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter(); - bluetooth.cancelDiscovery(); - - getLoadingIndicator().hide(); - - } - - private ContentLoadingProgressBar getLoadingIndicator() { - return ((ContentLoadingProgressBar)findViewById(R.id.loading_indicator)); - } - - private void initiateBluetoothScan() - { - Log.d(TAG, "Starting bluetooth scan..."); - - if (cancelMenuItem != null) { - cancelMenuItem.setVisible(true); - scanMenuItem.setVisible(false); - } - - final ContentLoadingProgressBar loadingBar = getLoadingIndicator(); - - loadingBar.show(); - - final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter(); - - if (firstScan) { - final 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); - Log.d(TAG, "Found bluetooth device: " + device.toString()); - - if (device != null && device.getName() != null) - if (device.getName().contains(BluetoothServer.BLUETOOTH_NAME_TAG)) { - 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); - } - } - } - } - }; - - final BroadcastReceiver scanCompleteReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "Scan complete: " + intent.getAction()); - loadingBar.hide(); - cancelMenuItem.setVisible(false); - scanMenuItem.setVisible(true); - } - }; - - getContext().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND)); - getContext().registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)); - - firstScan = false; - } - else - { - if (bluetooth.isDiscovering()) - { - bluetooth.cancelDiscovery(); - } - } - - if (!bluetooth.startDiscovery()) { - // TODO: Discovery did not start for some reason :( - Log.e(TAG, "Could not start bluetooth discovery, but am not sure why :("); - Toast.makeText(getContext(),"There was a problem looking for Bluetooth devices",Toast.LENGTH_SHORT).show(); - } - } - - private void populateBondedDevices() - { - for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) { - adapter.add(device); - } - } - - - - @Override - public void onItemClick(AdapterView parent, View view, 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(); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); - BluetoothDownloader downloader = new BluetoothDownloader(connection, "/", stream); - downloader.downloadUninterrupted(); - String result = stream.toString(); - Log.d(TAG, "Download complete."); - Log.d(TAG, result); - - Log.d(TAG, "Downloading again..."); - downloader = new BluetoothDownloader(connection, "/fdroid/repo/index.xml", stream); - downloader.downloadUninterrupted(); - result = stream.toString(); - Log.d(TAG, "Download complete."); - Log.d(TAG, result); - - /*Log.d(TAG, "Creating HEAD request for resource at \"/\"..."); - Request head = Request.createGET("/", connection); - Log.d(TAG, "Sending request..."); - Response response = head.send(); - Log.d(TAG, "Response from bluetooth: " + response.getStatusCode()); - String contents = response.readContents(); - Log.d(TAG, contents);*/ - } catch (IOException e) { - Log.e(TAG, "Error: " + e.getMessage()); - } - - /*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); - }*/ - } - - private class Adapter extends ArrayAdapter { - - public Adapter(Context context, int resource) { - super(context, resource); - } - - public Adapter(Context context, int resource, int textViewResourceId) { - super(context, resource, textViewResourceId); - } - - public Adapter(Context context, int resource, BluetoothDevice[] objects) { - super(context, resource, objects); - } - - public Adapter(Context context, int resource, int textViewResourceId, BluetoothDevice[] objects) { - super(context, resource, textViewResourceId, objects); - } - - public Adapter(Context context, int resource, List objects) { - super(context, resource, objects); - } - - public Adapter(Context context, int resource, int textViewResourceId, List objects) { - super(context, resource, textViewResourceId, objects); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view; - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflater.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 addressView = (TextView)view.findViewById(android.R.id.text2); - //TextView descriptionView = (TextView)view.findViewById(R.id.text3); - - nameView.setText(device.getName() == null ? getContext().getString(R.string.unknown) : device.getName()); - addressView.setText(device.getAddress()); - //descriptionView.setText(bondStateToLabel(device.getBondState())); - - return view; - } - - 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 getContext().getString(R.string.swap_bluetooth_bonded_device); - } else if (deviceBondState == BluetoothDevice.BOND_BONDING) { - return getContext().getString(R.string.swap_bluetooth_bonding_device); - } else { - // TODO: Might be a little bit harsh, makes it sound more malicious than it should. - return getContext().getString(R.string.swap_bluetooth_unknown_device); - } - } - } - -} diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java index b8288830f..f6750e936 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java @@ -124,7 +124,7 @@ public class SelectAppsView extends ListView implements @Override public int getPreviousStep() { - return getState().isConnectingWithPeer() ? SwapService.STEP_JOIN_WIFI : SwapService.STEP_INTRO; + return getState().isConnectingWithPeer() ? SwapService.STEP_INTRO : SwapService.STEP_JOIN_WIFI; } @ColorRes diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 53589a841..7204cc717 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -39,9 +39,9 @@ import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.localrepo.LocalRepoManager; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.peers.Peer; -import org.fdroid.fdroid.net.bluetooth.BluetoothServer; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -105,11 +105,12 @@ public class SwapWorkflowActivity extends AppCompatActivity { private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder binder) { - Log.d(TAG, "Swap service connected, enabling SwapManager to communicate with SwapService."); + Log.d(TAG, "Swap service connected. Will hold onto it so we can talk to it regularly."); service = ((SwapService.Binder)binder).getService(); showRelevantView(); } + // TODO: What causes this? Do we need to stop swapping explicitly when this is invoked? @Override public void onServiceDisconnected(ComponentName className) { Log.d(TAG, "Swap service disconnected"); @@ -133,6 +134,9 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override public void onBackPressed() { if (currentView.getStep() == SwapService.STEP_INTRO) { + if (service != null) { + service.disableAllSwapping(); + } finish(); } else { int nextStep = currentView.getPreviousStep(); @@ -235,6 +239,10 @@ public class SwapWorkflowActivity extends AppCompatActivity { case SwapService.STEP_SUCCESS: showSwapConnected(); break; + case SwapService.STEP_CONNECTING: + // TODO: Properly decide what to do here... + inflateInnerView(R.layout.swap_blank); + break; } } @@ -287,6 +295,10 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void showIntro() { + // If we were previously swapping with a specific client, forget that we were doing that, + // as we are starting over now. + getService().swapWith(null); + if (!getService().isEnabled()) { prepareInitialRepo(); } @@ -303,7 +315,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { } public void sendFDroid() { - // TODO: What is availble here? Currently we support Bluetooth (see main menu in F-Droid) + // TODO: What is available here? Currently we support Bluetooth (see main menu in F-Droid) // and Android Beam (try touching two devices together when in the app details view). } @@ -314,8 +326,8 @@ public class SwapWorkflowActivity extends AppCompatActivity { if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) { updateSwappableAppsTask = new PrepareFullSwapRepo(getService().getAppsToSwap()); updateSwappableAppsTask.execute(); - } else if (!attemptToShowNfc()) { - showWifiQr(); + } else { + onLocalRepoPrepared(); } } @@ -355,10 +367,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { inflateInnerView(R.layout.swap_join_wifi); } - private void showBluetoothDeviceList() { - inflateInnerView(R.layout.swap_bluetooth_devices); - } - public void showWifiQr() { inflateInnerView(R.layout.swap_wifi_qr); } @@ -384,6 +392,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { } public void swapWith(Peer peer) { + getService().stopScanningForPeers(); getService().swapWith(peer); showSelectApps(); } @@ -443,7 +452,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { /** * The process for setting up bluetooth is as follows: * * Assume we have bluetooth available (otherwise the button which allowed us to start - * the bluetooth process should not have been available). TODO: Remove button if bluetooth unavailable. + * the bluetooth process should not have been available). * * Ask user to enable (if not enabled yet). * * Start bluetooth server socket. * * Enable bluetooth discoverability, so that people can connect to our server socket. @@ -609,36 +618,53 @@ public class SwapWorkflowActivity extends AppCompatActivity { } + /** + * Helper class to try and make sense of what the swap workflow is currently doing. + * The more technologies are involved in the process (e.g. Bluetooth/Wifi/NFC/etc) + * the harder it becomes to reason about and debug the whole thing. Thus,this class + * will periodically dump the state to logcat so that it is easier to see when certain + * protocols are enabled/disabled. + * + * To view only this output from logcat: + * + * adb logcat | grep 'Swap Status' + * + * To exclude this output from logcat (it is very noisy): + * + * adb logcat | grep -v 'Swap Status' + * + */ class SwapDebug { - private StringBuilder status = new StringBuilder("\n"); - public void logStatus() { - append("service = " + service); - if (service != null) { - append("Swap Services:"); - append(" service.getBluetoothSwap() = " + service.getBluetoothSwap()); - append(" service.getBluetoothSwap().isConnected() = " + service.getBluetoothSwap().isConnected()); - append(" service.getWifiSwap() = " + service.getWifiSwap()); - append(" service.getWifiSwap().isConnected() = " + service.getWifiSwap().isConnected()); - append(" service.getWifiSwap().getBonjour() = " + service.getWifiSwap().getBonjour()); - append(" service.getWifiSwap().getBonjour().isConnected() = " + service.getWifiSwap().getBonjour().isConnected()); - append("Discovering Services:"); + String message = ""; + if (service == null) { + message = "No swap service"; + } else { + { + String bluetooth = service.getBluetoothSwap().isConnected() ? "Yes" : " No"; + String wifi = service.getWifiSwap().isConnected() ? "Yes" : " No"; + String mdns = service.getWifiSwap().getBonjour().isConnected() ? "Yes" : " No"; + message += "Broadcast { BT: " + bluetooth + ", WiFi: " + wifi + ", mDNS: " + mdns + "}, "; + } - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { + { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + String bluetooth = "N/A"; + if (adapter != null) { + Map scanModes = new HashMap<>(3); + scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE, "CONNECTABLE"); + scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, "CONNECTABLE_DISCOVERABLE"); + scanModes.put(BluetoothAdapter.SCAN_MODE_NONE, "NONE"); + bluetooth = "\"" + adapter.getName() + "\" - " + scanModes.get(adapter.getScanMode()); + } - Map scanModes = new HashMap<>(3); - scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE, "SCAN_MODE_CONNECTABLE"); - scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, "SCAN_MODE_CONNECTABLE_DISCOVERABLE"); - scanModes.put(BluetoothAdapter.SCAN_MODE_NONE, "SCAN_MODE_NONE"); - - append(" Bluetooth.isEnabled() = " + adapter.isEnabled()); - append(" Bluetooth.isDiscovering() = " + adapter.isDiscovering()); - append(" Bluetooth.getScanMode() = " + scanModes.get(adapter.getScanMode())); + String wifi = service.getBonjourFinder().isScanning() ? "Yes" : " No"; + message += "Discover { BT: " + bluetooth + ", WiFi: " + wifi + "}"; } } - Log.d("SwapStatus", status.toString()); + + Log.d("Swap Status", new Date().toLocaleString() + " " + message); new Timer().schedule(new TimerTask() { @Override @@ -646,13 +672,9 @@ public class SwapWorkflowActivity extends AppCompatActivity { new SwapDebug().logStatus(); } }, - 2000 + 1000 ); } - - private void append(String line) { - status.append(" ").append(line).append("\n"); - } } }