diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/BluetoothManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/BluetoothManager.java new file mode 100644 index 000000000..b7f8de854 --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/localrepo/BluetoothManager.java @@ -0,0 +1,135 @@ +package org.fdroid.fdroid.localrepo; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.Process; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; +import android.util.Log; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.net.bluetooth.BluetoothServer; + +import java.lang.ref.WeakReference; + +/** + * Manage the {@link android.bluetooth.BluetoothAdapter}in a {@link HandlerThread}. + * The start process is in {@link HandlerThread#onLooperPrepared()} so that it is + * always started before any messages get delivered from the queue. + * + * @see BonjourManager + * @see LocalRepoManager + */ +public class BluetoothManager { + private static final String TAG = "BluetoothManager"; + + public static final String ACTION_STATUS = "BluetoothStatus"; + public static final String EXTRA_STATUS = "BluetoothStatusExtra"; + public static final int STATUS_STARTING = 0; + public static final int STATUS_STARTED = 1; + public static final int STATUS_STOPPING = 2; + public static final int STATUS_STOPPED = 3; + public static final int STATUS_ERROR = 0xffff; + + private static final int STOP = 5709; + + private static WeakReference context; + private static Handler handler; + private static volatile HandlerThread handlerThread; + private static BluetoothAdapter bluetoothAdapter; + + /** + * Stops the Bluetooth adapter, triggering a status broadcast via {@link #ACTION_STATUS}. + * {@link #STATUS_STOPPED} can be broadcast multiple times for the same session, + * so make sure {@link android.content.BroadcastReceiver}s handle duplicates. + */ + public static void stop(Context context) { + BluetoothManager.context = new WeakReference<>(context); + if (handler == null || handlerThread == null || !handlerThread.isAlive()) { + Log.w(TAG, "handlerThread is already stopped, doing nothing!"); + sendBroadcast(STATUS_STOPPED, null); + return; + } + sendBroadcast(STATUS_STOPPING, null); + handler.sendEmptyMessage(STOP); + } + + /** + * Starts the service, triggering a status broadcast via {@link #ACTION_STATUS}. + * {@link #STATUS_STARTED} can be broadcast multiple times for the same session, + * so make sure {@link android.content.BroadcastReceiver}s handle duplicates. + */ + public static void start(final Context context) { + BluetoothManager.context = new WeakReference<>(context); + if (handlerThread != null && handlerThread.isAlive()) { + sendBroadcast(STATUS_STARTED, null); + return; + } + sendBroadcast(STATUS_STARTING, null); + + final BluetoothServer bluetoothServer = new BluetoothServer(context.getFilesDir()); + handlerThread = new HandlerThread("BluetoothManager", Process.THREAD_PRIORITY_LESS_FAVORABLE) { + @Override + protected void onLooperPrepared() { + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (!bluetoothAdapter.enable()) { + sendBroadcast(STATUS_ERROR, context.getString(R.string.swap_error_cannot_start_bluetooth)); + return; + } + bluetoothServer.start(); + if (bluetoothAdapter.startDiscovery()) { + sendBroadcast(STATUS_STARTED, null); + } else { + sendBroadcast(STATUS_ERROR, context.getString(R.string.swap_error_cannot_start_bluetooth)); + } + } + }; + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + bluetoothServer.close(); + if (bluetoothAdapter != null) { + bluetoothAdapter.cancelDiscovery(); + if (!SwapService.wasBluetoothEnabledBeforeSwap()) { + bluetoothAdapter.disable(); + } + } + handlerThread.quit(); + handlerThread = null; + sendBroadcast(STATUS_STOPPED, null); + } + }; + } + + public static void restart(Context context) { + stop(context); + try { + handlerThread.join(10000); + } catch (InterruptedException | NullPointerException e) { + // ignored + } + start(context); + } + + public static void setName(Context context, String name) { + // TODO + } + + public static boolean isAlive() { + return handlerThread != null && handlerThread.isAlive(); + } + + private static void sendBroadcast(int status, String message) { + + Intent intent = new Intent(ACTION_STATUS); + intent.putExtra(EXTRA_STATUS, status); + if (!TextUtils.isEmpty(message)) { + intent.putExtra(Intent.EXTRA_TEXT, message); + } + LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent); + } +} diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java index 66290c1f8..57b307158 100644 --- a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -32,7 +32,6 @@ import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.localrepo.peers.PeerFinder; -import org.fdroid.fdroid.localrepo.type.BluetoothSwap; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.WifiStateChangeService; import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; @@ -91,10 +90,9 @@ public class SwapService extends Service { context.stopService(intent); } - // ========================================================== - // Search for peers to swap - // ========================================================== - + /** + * Search for peers to swap + */ private Observable peerFinder; /** @@ -132,7 +130,7 @@ public class SwapService extends Service { throw new IllegalStateException("Cannot connect to peer, no peer has been selected."); } connectTo(getPeer()); - if (isEnabled() && getPeer().shouldPromptForSwapBack()) { + if (LocalHTTPDManager.isAlive() && getPeer().shouldPromptForSwapBack()) { askServerToSwapWithUs(peerRepo); } } @@ -334,32 +332,9 @@ public class SwapService extends Service { swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, visible).apply(); } - public boolean isEnabled() { - return bluetoothSwap.isConnected() || LocalHTTPDManager.isAlive(); - } - - // ========================================== - // Interacting with Bluetooth adapter - // ========================================== - - public boolean isBluetoothDiscoverable() { - return bluetoothSwap.isDiscoverable(); - } - - // =============================================================== - // Old SwapService stuff being merged into that. - // =============================================================== - - public static final String BLUETOOTH_STATE_CHANGE = "org.fdroid.fdroid.BLUETOOTH_STATE_CHANGE"; - public static final String EXTRA_STARTING = "STARTING"; - public static final String EXTRA_STARTED = "STARTED"; - public static final String EXTRA_STOPPING = "STOPPING"; - public static final String EXTRA_STOPPED = "STOPPED"; - private static final int NOTIFICATION = 1; private final Binder binder = new Binder(); - private BluetoothSwap bluetoothSwap; private static final int TIMEOUT = 15 * 60 * 1000; // 15 mins @@ -369,10 +344,6 @@ public class SwapService extends Service { @Nullable private Timer timer; - public BluetoothSwap getBluetoothSwap() { - return bluetoothSwap; - } - public class Binder extends android.os.Binder { public SwapService getService() { return SwapService.this; @@ -390,6 +361,11 @@ public class SwapService extends Service { bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter != null) { SwapService.putBluetoothEnabledBeforeSwap(bluetoothAdapter.isEnabled()); + if (bluetoothAdapter.isEnabled()) { + BluetoothManager.start(this); + } + registerReceiver(bluetoothScanModeChanged, + new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)); } wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); @@ -398,21 +374,12 @@ public class SwapService extends Service { } appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, ""))); - bluetoothSwap = BluetoothSwap.create(this); Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); - localBroadcastManager.registerReceiver(onWifiChange, - new IntentFilter(WifiStateChangeService.BROADCAST)); - localBroadcastManager.registerReceiver(onBluetoothSwapStateChange, - new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE)); - - if (getBluetoothVisibleUserPreference()) { - Utils.debugLog(TAG, "Previously the user enabled Bluetooth swap, so enabling again automatically."); - bluetoothSwap.startInBackground(); // TODO replace with Intent to SwapService - } else { - Utils.debugLog(TAG, "Bluetooth was NOT enabled last time user swapped, starting not visible."); - } + localBroadcastManager.registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST)); + localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS)); + localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS)); BonjourManager.start(this); BonjourManager.setVisible(this, getWifiVisibleUserPreference()); @@ -442,11 +409,11 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); localBroadcastManager.unregisterReceiver(onWifiChange); - localBroadcastManager.unregisterReceiver(onBluetoothSwapStateChange); + localBroadcastManager.unregisterReceiver(bluetoothStatus); - if (bluetoothAdapter != null && !wasBluetoothEnabledBeforeSwap()) { - bluetoothAdapter.disable(); - } + unregisterReceiver(bluetoothScanModeChanged); + + BluetoothManager.stop(this); BonjourManager.stop(this); LocalHTTPDManager.stop(this); @@ -456,8 +423,6 @@ public class SwapService extends Service { stopPollingConnectedSwapRepo(); - //TODO getBluetoothSwap().stopInBackground(); - if (timer != null) { timer.cancel(); } @@ -564,7 +529,8 @@ public class SwapService extends Service { } }; - private final BroadcastReceiver onBluetoothSwapStateChange = new SwapStateChangeReceiver(); + private final BroadcastReceiver bluetoothStatus = new SwapStateChangeReceiver(); + private final BroadcastReceiver localRepoStatus = new SwapStateChangeReceiver(); /** * When swapping is setup, then start the index polling. @@ -574,10 +540,13 @@ public class SwapService extends Service { @Override public void onReceive(Context context, Intent intent) { - if (intent.hasExtra(SwapService.EXTRA_STARTED)) { + int bluetoothStatus = intent.getIntExtra(BluetoothManager.ACTION_STATUS, -1); + int wifiStatus = intent.getIntExtra(LocalRepoService.EXTRA_STATUS, -1); + if (bluetoothStatus == BluetoothManager.STATUS_STARTED + || wifiStatus == LocalRepoService.STATUS_STARTED) { localBroadcastManager.registerReceiver(pollForUpdatesReceiver, - new IntentFilter()); - } else if (intent.hasExtra(SwapService.EXTRA_STOPPING) || intent.hasExtra(SwapService.EXTRA_STOPPED)) { + new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); + } else { localBroadcastManager.unregisterReceiver(pollForUpdatesReceiver); } } @@ -597,4 +566,23 @@ public class SwapService extends Service { } } } + + /** + * Handle events if the user or system changes the Bluetooth setup outside of F-Droid. + */ + private final BroadcastReceiver bluetoothScanModeChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) { + case BluetoothAdapter.SCAN_MODE_NONE: + BluetoothManager.stop(SwapService.this); + break; + + case BluetoothAdapter.SCAN_MODE_CONNECTABLE: + case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: + BluetoothManager.start(SwapService.this); + break; + } + } + }; } \ No newline at end of file diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java index c1a2c1a9b..6f98626c2 100644 --- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java +++ b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java @@ -4,10 +4,11 @@ import android.bluetooth.BluetoothDevice; import android.os.Parcel; import android.text.TextUtils; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.localrepo.type.BluetoothSwap; public class BluetoothPeer implements Peer { + private static final String BLUETOOTH_NAME_TAG = "FDroid:"; + private final BluetoothDevice device; public BluetoothPeer(BluetoothDevice device) { @@ -21,7 +22,7 @@ public class BluetoothPeer implements Peer { @Override public String getName() { - return device.getName().replaceAll("^" + BluetoothSwap.BLUETOOTH_NAME_TAG, ""); + return device.getName().replaceAll("^" + BLUETOOTH_NAME_TAG, ""); } @Override diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java b/app/src/full/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java deleted file mode 100644 index 4e34de4e0..000000000 --- a/app/src/full/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.fdroid.fdroid.localrepo.type; - -import android.bluetooth.BluetoothAdapter; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.AsyncTask; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.localrepo.SwapService; -import org.fdroid.fdroid.net.bluetooth.BluetoothServer; - -@SuppressWarnings("LineLength") -public class BluetoothSwap { - - private static final String TAG = "BluetoothSwap"; - public static final String BLUETOOTH_NAME_TAG = "FDroid:"; - - private static BluetoothSwap mInstance; - - @NonNull - private final BluetoothAdapter adapter; - private boolean isDiscoverable; - - private boolean isConnected; - - @NonNull - protected final Context context; - - @Nullable - private BluetoothServer server; - - private String deviceBluetoothName; - - public static BluetoothSwap create(@NonNull Context context) { - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter == null) { - return new NoBluetoothType(context); - } - if (mInstance == null) { - mInstance = new BluetoothSwap(context, adapter); - } - - return mInstance; - } - - private BluetoothSwap(@NonNull Context context, @NonNull BluetoothAdapter adapter) { - this.context = context; - this.adapter = adapter; - } - - public boolean isDiscoverable() { - return isDiscoverable; - } - - public boolean isConnected() { - return server != null && server.isRunning() && isConnected; - } - - public synchronized void start() { - if (isConnected()) { - Utils.debugLog(TAG, "already running, quitting start()"); - return; - } - - BroadcastReceiver receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) { - case BluetoothAdapter.SCAN_MODE_NONE: - setConnected(false); - break; - - case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: - isDiscoverable = true; - if (server != null && server.isRunning()) { - setConnected(true); - } - break; - - // Only other is BluetoothAdapter.SCAN_MODE_CONNECTABLE. For now don't handle that. - } - } - }; - context.registerReceiver(receiver, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)); - - /* - if (server != null) { - Utils.debugLog(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; - }*/ - - if (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(); - - /* - Utils.debugLog(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()) { - Utils.debugLog(TAG, "Bluetooth adapter is disabled, attempting to enable."); - if (!adapter.enable()) { - Utils.debugLog(TAG, "Could not enable Bluetooth adapter, so bailing out of Bluetooth swap."); - setConnected(false); - return; - } - } - - if (adapter.isEnabled()) { - setConnected(true); - } else { - Log.i(TAG, "Didn't start Bluetooth swapping server, because Bluetooth is disabled and couldn't be enabled."); - setConnected(false); - } - } - - public void stop() { - if (server != null && server.isAlive()) { - server.close(); - setConnected(false); - } else { - Log.i(TAG, "Attempting to stop Bluetooth swap, but it is not currently running."); - } - } - - protected void onStopped() { - Utils.debugLog(TAG, "Resetting bluetooth device name to " + deviceBluetoothName + " after swapping."); - adapter.setName(deviceBluetoothName); - } - - protected String getBroadcastAction() { - return SwapService.BLUETOOTH_STATE_CHANGE; - } - - protected final void setConnected(boolean connected) { - if (connected) { - isConnected = true; - sendBroadcast(SwapService.EXTRA_STARTED); - } else { - isConnected = false; - sendBroadcast(SwapService.EXTRA_STOPPED); - } - } - - /** - * Sends either a {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTING}, - * {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTED} or - * {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STOPPED} broadcast. - */ - protected final void sendBroadcast(String extra) { - if (getBroadcastAction() != null) { - Intent intent = new Intent(getBroadcastAction()); - intent.putExtra(extra, true); - Utils.debugLog(TAG, "Sending broadcast " + extra + " from " + getClass().getSimpleName()); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - } - - public void startInBackground() { - // TODO switch to thread which is killed if still running, like WiFiStateChangeService - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - start(); - return null; - } - }.execute(); - } - - - private static class NoBluetoothType extends BluetoothSwap { - - NoBluetoothType(@NonNull Context context) { - super(context, null); - } - - @Override - public void start() { - } - - @Override - public void stop() { - } - - @Override - protected String getBroadcastAction() { - return null; - } - - } -} diff --git a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java index 66cca3643..801280ca4 100644 --- a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java +++ b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java @@ -7,62 +7,26 @@ import android.bluetooth.BluetoothSocket; import java.io.IOException; public class BluetoothClient { - - @SuppressWarnings("unused") private static final String TAG = "BluetoothClient"; 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 = null; BluetoothConnection connection = null; try { - socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid()); + BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid()); connection = new BluetoothConnection(socket); connection.open(); return connection; - } catch (IOException e1) { - + } finally { if (connection != null) { connection.closeQuietly(); } - - throw e1; - - /* - Log.e(TAG, "There was an error while establishing Bluetooth connection. Falling back to reflection"); - Class clazz = socket.getRemoteDevice().getClass(); - Class[] paramTypes = new Class[]{Integer.TYPE}; - - Method method; - try { - method = clazz.getMethod("createInsecureRfcommSocket", paramTypes); - Object[] params = new Object[]{1}; - BluetoothSocket sockFallback = (BluetoothSocket) method.invoke(socket.getRemoteDevice(), params); - - BluetoothConnection connection = new BluetoothConnection(sockFallback); - connection.open(); - return connection; - } catch (NoSuchMethodException e) { - throw e1; - } catch (IllegalAccessException e) { - throw e1; - } catch (InvocationTargetException e) { - throw e1; - }*/ - - // Don't catch exceptions this time, let it bubble up as we did our best but don't - // have anythign else to offer in terms of resolving the problem right now. } } - } diff --git a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java index e4410f095..df20f70b8 100644 --- a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java +++ b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java @@ -7,7 +7,6 @@ import android.util.Log; import android.webkit.MimeTypeMap; import fi.iki.elonen.NanoHTTPD; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.localrepo.type.BluetoothSwap; import org.fdroid.fdroid.net.bluetooth.httpish.Request; import org.fdroid.fdroid.net.bluetooth.httpish.Response; @@ -34,18 +33,9 @@ public class BluetoothServer extends Thread { private final List clients = new ArrayList<>(); private final File webRoot; - private final BluetoothSwap swap; - private boolean isRunning; - public BluetoothServer(BluetoothSwap swap, File webRoot) { + public BluetoothServer(File webRoot) { this.webRoot = webRoot; - this.swap = swap; - - start(); - } - - public boolean isRunning() { - return isRunning; } public void close() { @@ -64,15 +54,12 @@ public class BluetoothServer extends Thread { @Override public void run() { - isRunning = true; final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 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); - swap.stop(); - isRunning = false; return; } @@ -102,7 +89,6 @@ public class BluetoothServer extends Thread { Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients", e); } } - isRunning = false; } private static class ClientConnection extends Thread { diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java index 60c7db5ec..54fc4fff1 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java +++ b/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java @@ -26,6 +26,7 @@ import cc.mvdan.accesspoint.WifiApControl; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.localrepo.BluetoothManager; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.SwapView; import org.fdroid.fdroid.localrepo.peers.Peer; @@ -135,7 +136,6 @@ public class StartSwapView extends SwapView { bluetoothSwitch.setOnCheckedChangeListener(null); } - LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onBluetoothSwapStateChanged); LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiNetworkChanged); } @@ -170,13 +170,6 @@ public class StartSwapView extends SwapView { getActivity().sendFDroid(); } }); - - findViewById(R.id.btn_qr_scanner).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - getActivity().startQrWorkflow(); - } - }); } /** @@ -218,57 +211,17 @@ public class StartSwapView extends SwapView { viewBluetoothId.setVisibility(bluetooth.isEnabled() ? View.VISIBLE : View.GONE); textBluetoothVisible = findViewById(R.id.bluetooth_visible); - if (getActivity().getSwapService().isBluetoothDiscoverable()) { - textBluetoothVisible.setText(R.string.swap_visible_bluetooth); - } else { - textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); - } bluetoothSwitch = (SwitchCompat) findViewById(R.id.switch_bluetooth); - Utils.debugLog(TAG, getActivity().getSwapService().isBluetoothDiscoverable() - ? "Initially marking switch as checked, because Bluetooth is discoverable." - : "Initially marking switch as not-checked, because Bluetooth is not discoverable."); bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled); - setBluetoothSwitchState(getActivity().getSwapService().isBluetoothDiscoverable(), true); - - LocalBroadcastManager.getInstance(getContext()).registerReceiver(onBluetoothSwapStateChanged, - new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE)); - + bluetoothSwitch.setChecked(SwapService.getBluetoothVisibleUserPreference()); + bluetoothSwitch.setEnabled(true); + bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled); } else { findViewById(R.id.bluetooth_info).setVisibility(View.GONE); } } - private final BroadcastReceiver onBluetoothSwapStateChanged = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.hasExtra(SwapService.EXTRA_STARTING)) { - Utils.debugLog(TAG, "Bluetooth service is starting (setting toggle to disabled, not checking because we will wait for an intent that bluetooth is actually enabled)"); - bluetoothSwitch.setEnabled(false); - textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth); - // bluetoothSwitch.setChecked(true); - } else { - if (intent.hasExtra(SwapService.EXTRA_STARTED)) { - Utils.debugLog(TAG, "Bluetooth service has started (updating text to visible, but not marking as checked)."); - textBluetoothVisible.setText(R.string.swap_visible_bluetooth); - bluetoothSwitch.setEnabled(true); - // bluetoothSwitch.setChecked(true); - } else { - Utils.debugLog(TAG, "Bluetooth service has stopped (setting switch to not-visible)."); - textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); - setBluetoothSwitchState(false, true); - } - } - } - }; - - private void setBluetoothSwitchState(boolean isChecked, boolean isEnabled) { - bluetoothSwitch.setOnCheckedChangeListener(null); - bluetoothSwitch.setChecked(isChecked); - bluetoothSwitch.setEnabled(isEnabled); - bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled); - } - private final CompoundButton.OnCheckedChangeListener onBluetoothSwitchToggled = new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -281,7 +234,7 @@ public class StartSwapView extends SwapView { // TODO: When they deny the request for enabling bluetooth, we need to disable this switch... } else { Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, disabling Bluetooth swap."); - getActivity().getSwapService().getBluetoothSwap().stop(); + BluetoothManager.stop(getContext()); textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); viewBluetoothId.setVisibility(View.GONE); Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, Bluetooth swap disabled successfully."); diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 17dadee80..4685d71cb 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -62,11 +62,13 @@ import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; +import org.fdroid.fdroid.localrepo.BluetoothManager; import org.fdroid.fdroid.localrepo.BonjourManager; import org.fdroid.fdroid.localrepo.LocalHTTPDManager; import org.fdroid.fdroid.localrepo.LocalRepoService; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.SwapView; +import org.fdroid.fdroid.localrepo.peers.BluetoothPeer; import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.Downloader; @@ -349,6 +351,10 @@ public class SwapWorkflowActivity extends AppCompatActivity { localBroadcastManager.registerReceiver(repoUpdateReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); localBroadcastManager.registerReceiver(bonjourStatus, new IntentFilter(BonjourManager.ACTION_STATUS)); + localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS)); + + registerReceiver(bluetoothScanModeChanged, + new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)); checkIncomingIntent(); @@ -362,10 +368,13 @@ public class SwapWorkflowActivity extends AppCompatActivity { protected void onPause() { super.onPause(); + unregisterReceiver(bluetoothScanModeChanged); + localBroadcastManager.unregisterReceiver(onWifiStateChanged); localBroadcastManager.unregisterReceiver(localRepoStatus); localBroadcastManager.unregisterReceiver(repoUpdateReceiver); localBroadcastManager.unregisterReceiver(bonjourStatus); + localBroadcastManager.unregisterReceiver(bluetoothStatus); } @Override @@ -522,24 +531,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { inflateSwapView(R.layout.swap_start_swap); } - public void startQrWorkflow() { - if (!getSwapService().isEnabled()) { - new AlertDialog.Builder(this) - .setTitle(R.string.not_visible_nearby) - .setMessage(R.string.not_visible_nearby_description) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // Do nothing. The dialog will get dismissed anyway, which is all we ever wanted... - } - }) - .create().show(); - } else { - inflateSwapView(R.layout.swap_wifi_qr); - } - } - /** * On {@code android-26}, only apps with privileges can access * {@code WRITE_SETTINGS}. So this just shows the tethering settings @@ -720,7 +711,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { if (resultCode != RESULT_CANCELED) { Utils.debugLog(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server."); - getSwapService().getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService + BluetoothManager.start(this); } else { Utils.debugLog(TAG, "User chose not to make Bluetooth discoverable, so doing nothing"); SwapService.putBluetoothVisibleUserPreference(false); @@ -759,21 +750,42 @@ public class SwapWorkflowActivity extends AppCompatActivity { private void ensureBluetoothDiscoverableThenStart() { Utils.debugLog(TAG, "Ensuring Bluetooth is in discoverable mode."); if (bluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { - - // TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery - // is cancelled prematurely. - - // 3600 is new maximum! TODO: What about when this expires? What if user manually disables discovery? - final int discoverableTimeout = 3600; - Utils.debugLog(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, discoverableTimeout); + intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600); // 1 hour startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE); } - service.getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService + BluetoothManager.start(this); } + private final BroadcastReceiver bluetoothScanModeChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + SwitchCompat bluetoothSwitch = container.findViewById(R.id.switch_bluetooth); + TextView textBluetoothVisible = container.findViewById(R.id.bluetooth_visible); + if (bluetoothSwitch == null || textBluetoothVisible == null + || !BluetoothManager.ACTION_STATUS.equals(intent.getAction())) { + return; + } + switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) { + case BluetoothAdapter.SCAN_MODE_NONE: + textBluetoothVisible.setText(R.string.disabled); + bluetoothSwitch.setEnabled(true); + break; + + case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: + textBluetoothVisible.setText(R.string.swap_visible_bluetooth); + bluetoothSwitch.setEnabled(true); + break; + + case BluetoothAdapter.SCAN_MODE_CONNECTABLE: + textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); + bluetoothSwitch.setEnabled(true); + break; + } + } + }; + /** * 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) @@ -799,7 +811,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { if (service == null) { message = "No swap service"; } else { - String bluetooth = getSwapService().getBluetoothSwap().isConnected() ? "Y" : " N"; + String bluetooth; bluetooth = "N/A"; if (bluetoothAdapter != null) { @@ -1015,6 +1027,13 @@ public class SwapWorkflowActivity extends AppCompatActivity { } }); + findViewById(R.id.btn_scan_qr).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + inflateSwapView(R.layout.swap_wifi_qr); + } + }); + if (SwapService.getWifiVisibleUserPreference()) { wifiSwitch.setChecked(true); } else { @@ -1085,6 +1104,82 @@ public class SwapWorkflowActivity extends AppCompatActivity { } }; + private final BroadcastReceiver bluetoothStatus = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + SwitchCompat bluetoothSwitch = container.findViewById(R.id.switch_bluetooth); + TextView textBluetoothVisible = container.findViewById(R.id.bluetooth_visible); + TextView textDeviceIdBluetooth = container.findViewById(R.id.device_id_bluetooth); + TextView peopleNearbyText = container.findViewById(R.id.text_people_nearby); + ProgressBar peopleNearbyProgress = container.findViewById(R.id.searching_people_nearby); + if (bluetoothSwitch == null || textBluetoothVisible == null || textDeviceIdBluetooth == null + || peopleNearbyText == null || peopleNearbyProgress == null + || !BluetoothManager.ACTION_STATUS.equals(intent.getAction())) { + return; + } + int status = intent.getIntExtra(BluetoothManager.EXTRA_STATUS, -1); + Log.i(TAG, "BluetoothManager.EXTRA_STATUS: " + status); + switch (status) { + case BluetoothManager.STATUS_STARTING: + bluetoothSwitch.setEnabled(false); + textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth); + textDeviceIdBluetooth.setVisibility(View.VISIBLE); + peopleNearbyText.setText(R.string.swap_scanning_for_peers); + peopleNearbyText.setVisibility(View.VISIBLE); + peopleNearbyProgress.setVisibility(View.VISIBLE); + break; + case BluetoothManager.STATUS_STARTED: + bluetoothSwitch.setEnabled(true); + textBluetoothVisible.setText(R.string.swap_visible_bluetooth); + textDeviceIdBluetooth.setVisibility(View.VISIBLE); + peopleNearbyText.setText(R.string.swap_scanning_for_peers); + peopleNearbyText.setVisibility(View.VISIBLE); + peopleNearbyProgress.setVisibility(View.VISIBLE); + break; + case BluetoothManager.STATUS_STOPPING: + bluetoothSwitch.setEnabled(false); + textBluetoothVisible.setText(R.string.swap_stopping); + textDeviceIdBluetooth.setVisibility(View.GONE); + if (!BonjourManager.isAlive()) { + peopleNearbyText.setText(R.string.swap_stopping); + peopleNearbyText.setVisibility(View.VISIBLE); + peopleNearbyProgress.setVisibility(View.VISIBLE); + } + break; + case BluetoothManager.STATUS_STOPPED: + bluetoothSwitch.setEnabled(true); + textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth); + textDeviceIdBluetooth.setVisibility(View.GONE); + if (!BonjourManager.isAlive()) { + peopleNearbyText.setVisibility(View.GONE); + peopleNearbyProgress.setVisibility(View.GONE); + } + + ListView peopleNearbyView = container.findViewById(R.id.list_people_nearby); + if (peopleNearbyView == null) { + break; + } + ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyView.getAdapter(); + for (int i = 0; i < peopleNearbyAdapter.getCount(); i++) { + Peer peer = (Peer) peopleNearbyAdapter.getItem(i); + if (peer.getClass().equals(BluetoothPeer.class)) { + Utils.debugLog(TAG, "Removing bluetooth peer: " + peer.getName()); + peopleNearbyAdapter.remove(peer); + } + } + break; + case BluetoothManager.STATUS_ERROR: + bluetoothSwitch.setEnabled(true); + textBluetoothVisible.setText(intent.getStringExtra(Intent.EXTRA_TEXT)); + textDeviceIdBluetooth.setVisibility(View.VISIBLE); + break; + default: + throw new IllegalArgumentException("Bad intent: " + intent); + } + + } + }; + private void setUpUseBluetoothButton() { Button useBluetooth = findViewById(R.id.btn_use_bluetooth); if (useBluetooth != null) { diff --git a/app/src/full/res/layout/swap_start_swap.xml b/app/src/full/res/layout/swap_start_swap.xml index 679d64feb..0d7e639ee 100644 --- a/app/src/full/res/layout/swap_start_swap.xml +++ b/app/src/full/res/layout/swap_start_swap.xml @@ -42,7 +42,7 @@ android:id="@+id/bluetooth_visible" android:layout_width="wrap_content" android:layout_height="wrap_content" - tools:text="@string/swap_visible_bluetooth" + tools:text="@string/swap_not_visible_bluetooth" android:textSize="18sp"/>