diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java index a170cbccc02d67989685f6b6bb53eca7694a3d01..65e0211faa6bf3e90cf68790ab2c3522825fcd81 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java @@ -16,18 +16,27 @@ import org.briarproject.bramble.api.plugin.PluginException; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.util.AndroidUtils; import java.io.Closeable; import java.io.IOException; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.UUID; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Logger; import javax.annotation.Nullable; +import static android.bluetooth.BluetoothAdapter.ACTION_DISCOVERY_FINISHED; +import static android.bluetooth.BluetoothAdapter.ACTION_DISCOVERY_STARTED; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED; import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE; @@ -37,8 +46,13 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERA import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE; import static android.bluetooth.BluetoothAdapter.STATE_OFF; import static android.bluetooth.BluetoothAdapter.STATE_ON; +import static android.bluetooth.BluetoothDevice.ACTION_FOUND; +import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -47,8 +61,11 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> { private static final Logger LOG = Logger.getLogger(AndroidBluetoothPlugin.class.getName()); + private static final int MAX_DISCOVERY_MS = 10_000; + private final AndroidExecutor androidExecutor; private final Context appContext; + private final Clock clock; private volatile boolean wasEnabledByUs = false; private volatile BluetoothStateReceiver receiver = null; @@ -58,12 +75,13 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> { AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter, Executor ioExecutor, AndroidExecutor androidExecutor, - Context appContext, SecureRandom secureRandom, Backoff backoff, - DuplexPluginCallback callback, int maxLatency) { + Context appContext, SecureRandom secureRandom, Clock clock, + Backoff backoff, DuplexPluginCallback callback, int maxLatency) { super(connectionLimiter, ioExecutor, secureRandom, backoff, callback, maxLatency); this.androidExecutor = androidExecutor; this.appContext = appContext; + this.clock = clock; } @Override @@ -182,6 +200,74 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> { } } + @Override + @Nullable + DuplexTransportConnection discoverAndConnect(String uuid) { + if (adapter == null) return null; + for (String address : discoverDevices()) { + try { + if (LOG.isLoggable(INFO)) + LOG.info("Connecting to " + scrubMacAddress(address)); + return connectTo(address, uuid); + } catch (IOException e) { + if (LOG.isLoggable(INFO)) { + LOG.info("Could not connect to " + + scrubMacAddress(address)); + } + } + } + LOG.info("Could not connect to any devices"); + return null; + } + + private Collection<String> discoverDevices() { + List<String> addresses = new ArrayList<>(); + BlockingQueue<Intent> intents = new LinkedBlockingQueue<>(); + DiscoveryReceiver receiver = new DiscoveryReceiver(intents); + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCOVERY_STARTED); + filter.addAction(ACTION_DISCOVERY_FINISHED); + filter.addAction(ACTION_FOUND); + appContext.registerReceiver(receiver, filter); + try { + if (adapter.startDiscovery()) { + long now = clock.currentTimeMillis(); + long end = now + MAX_DISCOVERY_MS; + while (now < end) { + Intent i = intents.poll(end - now, MILLISECONDS); + if (i == null) break; + String action = i.getAction(); + if (ACTION_DISCOVERY_STARTED.equals(action)) { + LOG.info("Discovery started"); + } else if (ACTION_DISCOVERY_FINISHED.equals(action)) { + LOG.info("Discovery finished"); + break; + } else if (ACTION_FOUND.equals(action)) { + BluetoothDevice d = i.getParcelableExtra(EXTRA_DEVICE); + String address = d.getAddress(); + if (LOG.isLoggable(INFO)) + LOG.info("Discovered " + scrubMacAddress(address)); + if (!addresses.contains(address)) + addresses.add(address); + } + now = clock.currentTimeMillis(); + } + } else { + LOG.info("Could not start discovery"); + } + } catch (InterruptedException e) { + LOG.info("Interrupted while discovering devices"); + Thread.currentThread().interrupt(); + } finally { + LOG.info("Cancelling discovery"); + adapter.cancelDiscovery(); + appContext.unregisterReceiver(receiver); + } + // Shuffle the addresses so we don't always try the same one first + Collections.shuffle(addresses); + return addresses; + } + private void tryToClose(@Nullable Closeable c) { try { if (c != null) c.close(); @@ -207,4 +293,18 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> { } } } + + private static class DiscoveryReceiver extends BroadcastReceiver { + + private final BlockingQueue<Intent> intents; + + private DiscoveryReceiver(BlockingQueue<Intent> intents) { + this.intents = intents; + } + + @Override + public void onReceive(Context ctx, Intent intent) { + intents.add(intent); + } + } } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java index c4b54673984080451b6b30a93c6aa851650aaa44..53a127caa71b8e9e2ffb627266414831093830c5 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java @@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.bramble.api.system.Clock; import java.security.SecureRandom; import java.util.concurrent.Executor; @@ -33,17 +34,19 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory { private final Context appContext; private final SecureRandom secureRandom; private final EventBus eventBus; + private final Clock clock; private final BackoffFactory backoffFactory; public AndroidBluetoothPluginFactory(Executor ioExecutor, AndroidExecutor androidExecutor, Context appContext, - SecureRandom secureRandom, EventBus eventBus, + SecureRandom secureRandom, EventBus eventBus, Clock clock, BackoffFactory backoffFactory) { this.ioExecutor = ioExecutor; this.androidExecutor = androidExecutor; this.appContext = appContext; this.secureRandom = secureRandom; this.eventBus = eventBus; + this.clock = clock; this.backoffFactory = backoffFactory; } @@ -65,7 +68,7 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory { MAX_POLLING_INTERVAL, BACKOFF_BASE); AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin( connectionLimiter, ioExecutor, androidExecutor, appContext, - secureRandom, backoff, callback, MAX_LATENCY); + secureRandom, clock, backoff, callback, MAX_LATENCY); eventBus.addListener(plugin); return plugin; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java index d329bff3fdcfb7f7331a4a4f8de7c4e9d9ec5ead..4041d0ce4a350f630f53ba4a15de4e2fa5d91a5f 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java @@ -21,7 +21,7 @@ public interface KeyAgreementConstants { /** * The connection timeout in milliseconds. */ - long CONNECTION_TIMEOUT = 20 * 1000; + long CONNECTION_TIMEOUT = 60_000; /** * The transport identifier for Bluetooth. diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java index 243548c0a8663424d74fe13cc75e41e4a640f6fc..0fdc03ceeabe9fe78dab4e813edc81539295ed42 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java @@ -23,7 +23,6 @@ import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; -import org.briarproject.bramble.util.StringUtils; import java.io.IOException; import java.security.SecureRandom; @@ -46,6 +45,9 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID; import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; +import static org.briarproject.bramble.util.StringUtils.macToBytes; +import static org.briarproject.bramble.util.StringUtils.macToString; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -96,6 +98,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { abstract DuplexTransportConnection connectTo(String address, String uuid) throws IOException; + @Nullable + abstract DuplexTransportConnection discoverAndConnect(String uuid); + BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter, Executor ioExecutor, SecureRandom secureRandom, Backoff backoff, DuplexPluginCallback callback, int maxLatency) { @@ -193,7 +198,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { address = getBluetoothAddress(); if (LOG.isLoggable(INFO)) LOG.info("Local address " + scrubMacAddress(address)); - if (!StringUtils.isNullOrEmpty(address)) { + if (!isNullOrEmpty(address)) { p.put(PROP_ADDRESS, address); changed = true; } @@ -256,9 +261,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { // Try to connect to known devices in parallel for (Entry<ContactId, TransportProperties> e : contacts.entrySet()) { String address = e.getValue().get(PROP_ADDRESS); - if (StringUtils.isNullOrEmpty(address)) continue; + if (isNullOrEmpty(address)) continue; String uuid = e.getValue().get(PROP_UUID); - if (StringUtils.isNullOrEmpty(uuid)) continue; + if (isNullOrEmpty(uuid)) continue; ContactId c = e.getKey(); ioExecutor.execute(() -> { if (!isRunning() || !shouldAllowContactConnections()) return; @@ -309,9 +314,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { if (!isRunning() || !shouldAllowContactConnections()) return null; if (!connectionLimiter.canOpenContactConnection()) return null; String address = p.get(PROP_ADDRESS); - if (StringUtils.isNullOrEmpty(address)) return null; + if (isNullOrEmpty(address)) return null; String uuid = p.get(PROP_UUID); - if (StringUtils.isNullOrEmpty(uuid)) return null; + if (isNullOrEmpty(uuid)) return null; DuplexTransportConnection conn = connect(address, uuid); if (conn == null) return null; // TODO: Why don't we reset the backoff here? @@ -326,9 +331,6 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { @Override public KeyAgreementListener createKeyAgreementListener(byte[] commitment) { if (!isRunning()) return null; - // There's no point listening if we can't discover our own address - String address = getBluetoothAddress(); - if (address == null) return null; // No truncation necessary because COMMIT_LENGTH = 16 String uuid = UUID.nameUUIDFromBytes(commitment).toString(); if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid); @@ -346,7 +348,8 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { } BdfList descriptor = new BdfList(); descriptor.add(TRANSPORT_ID_BLUETOOTH); - descriptor.add(StringUtils.macToBytes(address)); + String address = getBluetoothAddress(); + if (address != null) descriptor.add(macToBytes(address)); return new BluetoothKeyAgreementListener(descriptor, ss); } @@ -354,18 +357,25 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { public DuplexTransportConnection createKeyAgreementConnection( byte[] commitment, BdfList descriptor) { if (!isRunning()) return null; - String address; - try { - address = parseAddress(descriptor); - } catch (FormatException e) { - LOG.info("Invalid address in key agreement descriptor"); - return null; - } // No truncation necessary because COMMIT_LENGTH = 16 String uuid = UUID.nameUUIDFromBytes(commitment).toString(); - if (LOG.isLoggable(INFO)) - LOG.info("Connecting to key agreement UUID " + uuid); - DuplexTransportConnection conn = connect(address, uuid); + DuplexTransportConnection conn; + if (descriptor.size() == 1) { + if (LOG.isLoggable(INFO)) + LOG.info("Discovering address for key agreement UUID " + uuid); + conn = discoverAndConnect(uuid); + } else { + String address; + try { + address = parseAddress(descriptor); + } catch (FormatException e) { + LOG.info("Invalid address in key agreement descriptor"); + return null; + } + if (LOG.isLoggable(INFO)) + LOG.info("Connecting to key agreement UUID " + uuid); + conn = connect(address, uuid); + } if (conn != null) connectionLimiter.keyAgreementConnectionOpened(conn); return conn; } @@ -373,7 +383,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener { private String parseAddress(BdfList descriptor) throws FormatException { byte[] mac = descriptor.getRaw(1); if (mac.length != 6) throw new FormatException(); - return StringUtils.macToString(mac); + return macToString(mac); } @Override diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java index c3652bfb9c93f91c891591dbaaaac0e9a758def2..c3fe9b028763cc43fd47db6efb551de6f49583eb 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java @@ -108,6 +108,12 @@ class JavaBluetoothPlugin extends BluetoothPlugin<StreamConnectionNotifier> { return wrapSocket((StreamConnection) Connector.open(url)); } + @Override + @Nullable + DuplexTransportConnection discoverAndConnect(String uuid) { + return null; // TODO + } + private String makeUrl(String address, String uuid) { return "btspp://" + address + ":" + uuid + ";name=RFCOMM"; } diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 885429cc2dc610390ac8c490b24eba605dbe9865..b5a63032d66e2187c30c247ae22f9115982cdb2d 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ <uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.BLUETOOTH" /> diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 606647958d59240504e2bd1f8e553edc13f538b1..bbfb919cee953561eed6e554a134b88391282ea7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -107,7 +107,7 @@ public class AppModule { Context appContext = app.getApplicationContext(); DuplexPluginFactory bluetooth = new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor, - appContext, random, eventBus, backoffFactory); + appContext, random, eventBus, clock, backoffFactory); DuplexPluginFactory tor = new AndroidTorPluginFactory(ioExecutor, scheduler, appContext, networkManager, locationUtils, eventBus, torSocketFactory, backoffFactory, resourceProvider, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java index f3d72d17cd44b085212b4d65e3d4e20e21133f40..5d087d88b1866bb0d74f695434a6cfffb5d0ee90 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java @@ -9,9 +9,9 @@ public interface RequestCodes { int REQUEST_WRITE_BLOG_POST = 5; int REQUEST_SHARE_BLOG = 6; int REQUEST_RINGTONE = 7; - int REQUEST_PERMISSION_CAMERA = 8; + int REQUEST_PERMISSION_CAMERA_LOCATION = 8; int REQUEST_DOZE_WHITELISTING = 9; - int REQUEST_ENABLE_BLUETOOTH = 10; + int REQUEST_BLUETOOTH_DISCOVERABLE = 10; int REQUEST_UNLOCK = 11; int REQUEST_KEYGUARD_UNLOCK = 12; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java index d74c3e24886713d45181dfe6bd825a19450ed89a..4298e89c615126733a6fec47199e9f38b32a245e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java @@ -3,7 +3,6 @@ package org.briarproject.briar.android.keyagreement; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; @@ -11,19 +10,15 @@ import android.support.annotation.StringRes; import android.support.annotation.UiThread; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; -import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog.Builder; import android.support.v7.widget.Toolbar; import android.view.MenuItem; -import android.widget.Toast; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent; import org.briarproject.briar.R; -import org.briarproject.briar.R.string; -import org.briarproject.briar.R.style; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.fragment.BaseFragment; @@ -37,16 +32,19 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.CAMERA; -import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE; +import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; +import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED; +import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE; import static android.bluetooth.BluetoothAdapter.EXTRA_STATE; +import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE; +import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.STATE_ON; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.os.Build.VERSION.SDK_INT; -import static android.widget.Toast.LENGTH_LONG; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ENABLE_BLUETOOTH; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA; +import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE; +import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -55,7 +53,11 @@ public abstract class KeyAgreementActivity extends BriarActivity implements KeyAgreementEventListener { private enum BluetoothState { - UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED + UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED, DISCOVERABLE + } + + private enum Permission { + UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED } private static final Logger LOG = @@ -64,8 +66,27 @@ public abstract class KeyAgreementActivity extends BriarActivity implements @Inject EventBus eventBus; - private boolean isResumed = false, enableWasRequested = false; - private boolean continueClicked, gotCameraPermission; + /** + * Set to true in onPostResume() and false in onPause(). This prevents the + * QR code fragment from being shown if onRequestPermissionsResult() is + * called while the activity is paused, which could cause a crash due to + * https://issuetracker.google.com/issues/37067655. + */ + private boolean isResumed = false; + /** + * Set to true when the continue button is clicked, and false when the QR + * code fragment is shown. This prevents the QR code fragment from being + * shown automatically before the continue button has been clicked. + */ + private boolean continueClicked = false; + /** + * Records whether the Bluetooth adapter was already enabled before we + * asked for Bluetooth discoverability, so we know whether to broadcast a + * {@link BluetoothEnabledEvent}. + */ + private boolean wasAdapterEnabled = false; + private Permission cameraPermission = Permission.UNKNOWN; + private Permission locationPermission = Permission.UNKNOWN; private BluetoothState bluetoothState = BluetoothState.UNKNOWN; private BroadcastReceiver bluetoothReceiver = null; @@ -85,7 +106,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements if (state == null) { showInitialFragment(IntroFragment.newInstance()); } - IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED); + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_STATE_CHANGED); + filter.addAction(ACTION_SCAN_MODE_CHANGED); bluetoothReceiver = new BluetoothStateReceiver(); registerReceiver(bluetoothReceiver, filter); } @@ -107,20 +130,40 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } } + @Override + public void onStart() { + super.onStart(); + // Permissions may have been granted manually while we were stopped + cameraPermission = Permission.UNKNOWN; + locationPermission = Permission.UNKNOWN; + } + @Override protected void onPostResume() { super.onPostResume(); isResumed = true; // Workaround for // https://code.google.com/p/android/issues/detail?id=190966 - if (canShowQrCodeFragment()) showQrCodeFragment(); + showQrCodeFragmentIfAllowed(); + } + + private void showQrCodeFragmentIfAllowed() { + if (isResumed && continueClicked && areEssentialPermissionsGranted()) { + if (bluetoothState == BluetoothState.UNKNOWN || + bluetoothState == BluetoothState.ENABLED) { + requestBluetoothDiscoverable(); + } else if (bluetoothState != BluetoothState.WAITING) { + showQrCodeFragment(); + } + } } - private boolean canShowQrCodeFragment() { - return isResumed && continueClicked - && (SDK_INT < 23 || gotCameraPermission) - && bluetoothState != BluetoothState.UNKNOWN - && bluetoothState != BluetoothState.WAITING; + private boolean areEssentialPermissionsGranted() { + // If the camera permission has been granted, and the location + // permission has been granted or permanently denied, we can continue + return cameraPermission == Permission.GRANTED && + (locationPermission == Permission.GRANTED || + locationPermission == Permission.PERMANENTLY_DENIED); } @Override @@ -132,50 +175,54 @@ public abstract class KeyAgreementActivity extends BriarActivity implements @Override public void showNextScreen() { continueClicked = true; - if (checkPermissions()) { - if (shouldRequestEnableBluetooth()) requestEnableBluetooth(); - else if (canShowQrCodeFragment()) showQrCodeFragment(); - } - } - - private boolean shouldRequestEnableBluetooth() { - return bluetoothState == BluetoothState.UNKNOWN - || bluetoothState == BluetoothState.REFUSED; + if (checkPermissions()) showQrCodeFragmentIfAllowed(); } - private void requestEnableBluetooth() { + private void requestBluetoothDiscoverable() { BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); if (bt == null) { setBluetoothState(BluetoothState.NO_ADAPTER); - } else if (bt.isEnabled()) { - setBluetoothState(BluetoothState.ENABLED); } else { - enableWasRequested = true; setBluetoothState(BluetoothState.WAITING); - Intent i = new Intent(ACTION_REQUEST_ENABLE); - startActivityForResult(i, REQUEST_ENABLE_BLUETOOTH); + wasAdapterEnabled = bt.isEnabled(); + Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); + startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE); } } private void setBluetoothState(BluetoothState bluetoothState) { LOG.info("Setting Bluetooth state to " + bluetoothState); this.bluetoothState = bluetoothState; - if (enableWasRequested && bluetoothState == BluetoothState.ENABLED) { + if (!wasAdapterEnabled && bluetoothState == BluetoothState.ENABLED) { eventBus.broadcast(new BluetoothEnabledEvent()); - enableWasRequested = false; + wasAdapterEnabled = true; } - if (canShowQrCodeFragment()) showQrCodeFragment(); + showQrCodeFragmentIfAllowed(); } @Override public void onActivityResult(int request, int result, Intent data) { - // If the request was granted we'll catch the state change event - if (request == REQUEST_ENABLE_BLUETOOTH && result == RESULT_CANCELED) - setBluetoothState(BluetoothState.REFUSED); + if (request == REQUEST_BLUETOOTH_DISCOVERABLE) { + if (result == RESULT_CANCELED) { + setBluetoothState(BluetoothState.REFUSED); + } else { + // If Bluetooth is already discoverable, show the QR code - + // otherwise wait for the state or scan mode to change + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt == null) throw new AssertionError(); + if (bt.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) + setBluetoothState(BluetoothState.DISCOVERABLE); + } + } } private void showQrCodeFragment() { + // If we return to the intro fragment, the continue button needs to be + // clicked again before showing the QR code fragment continueClicked = false; + // If we return to the intro fragment, ask for Bluetooth + // discoverability again before showing the QR code fragment + bluetoothState = BluetoothState.UNKNOWN; // FIXME #824 FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) { @@ -194,74 +241,113 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } private boolean checkPermissions() { - if (ContextCompat.checkSelfPermission(this, CAMERA) != - PERMISSION_GRANTED) { - // Should we show an explanation? - if (ActivityCompat.shouldShowRequestPermissionRationale(this, - CAMERA)) { - OnClickListener continueListener = - (dialog, which) -> requestPermission(); - Builder builder = new Builder(this, style.BriarDialogTheme); - builder.setTitle(string.permission_camera_title); - builder.setMessage(string.permission_camera_request_body); - builder.setNeutralButton(string.continue_button, - continueListener); - builder.show(); - } else { - requestPermission(); - } - gotCameraPermission = false; + if (areEssentialPermissionsGranted()) return true; + // If the camera permission has been permanently denied, ask the + // user to change the setting + if (cameraPermission == Permission.PERMANENTLY_DENIED) { + Builder builder = new Builder(this, R.style.BriarDialogTheme); + builder.setTitle(R.string.permission_camera_title); + builder.setMessage(R.string.permission_camera_denied_body); + builder.setPositiveButton(R.string.ok, + UiUtils.getGoToSettingsListener(this)); + builder.setNegativeButton(R.string.cancel, + (dialog, which) -> supportFinishAfterTransition()); + builder.show(); return false; + } + // Should we show the rationale for one or both permissions? + if (cameraPermission == Permission.SHOW_RATIONALE && + locationPermission == Permission.SHOW_RATIONALE) { + showRationale(R.string.permission_camera_location_title, + R.string.permission_camera_location_request_body); + } else if (cameraPermission == Permission.SHOW_RATIONALE) { + showRationale(R.string.permission_camera_title, + R.string.permission_camera_request_body); + } else if (locationPermission == Permission.SHOW_RATIONALE) { + showRationale(R.string.permission_location_title, + R.string.permission_location_request_body); } else { - gotCameraPermission = true; - return true; + requestPermissions(); } + return false; + } + + private void showRationale(@StringRes int title, @StringRes int body) { + Builder builder = new Builder(this, R.style.BriarDialogTheme); + builder.setTitle(title); + builder.setMessage(body); + builder.setNeutralButton(R.string.continue_button, + (dialog, which) -> requestPermissions()); + builder.show(); } - private void requestPermission() { - ActivityCompat.requestPermissions(this, new String[] {CAMERA}, - REQUEST_PERMISSION_CAMERA); + private void requestPermissions() { + ActivityCompat.requestPermissions(this, + new String[] {CAMERA, ACCESS_COARSE_LOCATION}, + REQUEST_PERMISSION_CAMERA_LOCATION); } @Override @UiThread public void onRequestPermissionsResult(int requestCode, - String permissions[], int[] grantResults) { - if (requestCode == REQUEST_PERMISSION_CAMERA) { - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 && - grantResults[0] == PERMISSION_GRANTED) { - gotCameraPermission = true; - showNextScreen(); - } else { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, - CAMERA)) { - // The user has permanently denied the request - OnClickListener cancelListener = - (dialog, which) -> supportFinishAfterTransition(); - Builder builder = new Builder(this, style.BriarDialogTheme); - builder.setTitle(string.permission_camera_title); - builder.setMessage(string.permission_camera_denied_body); - builder.setPositiveButton(string.ok, - UiUtils.getGoToSettingsListener(this)); - builder.setNegativeButton(string.cancel, cancelListener); - builder.show(); - } else { - Toast.makeText(this, string.permission_camera_denied_toast, - LENGTH_LONG).show(); - supportFinishAfterTransition(); - } - } + String[] permissions, int[] grantResults) { + if (requestCode != REQUEST_PERMISSION_CAMERA_LOCATION) + throw new AssertionError(); + if (gotPermission(CAMERA, permissions, grantResults)) { + cameraPermission = Permission.GRANTED; + } else if (shouldShowRationale(CAMERA)) { + cameraPermission = Permission.SHOW_RATIONALE; + } else { + cameraPermission = Permission.PERMANENTLY_DENIED; } + if (gotPermission(ACCESS_COARSE_LOCATION, permissions, grantResults)) { + locationPermission = Permission.GRANTED; + } else if (shouldShowRationale(ACCESS_COARSE_LOCATION)) { + locationPermission = Permission.SHOW_RATIONALE; + } else { + locationPermission = Permission.PERMANENTLY_DENIED; + } + // If a permission dialog has been shown, showing the QR code fragment + // on this call path would cause a crash due to + // https://code.google.com/p/android/issues/detail?id=190966. + // In that case the isResumed flag prevents the fragment from being + // shown here, and showQrCodeFragmentIfAllowed() will be called again + // from onPostResume(). + if (checkPermissions()) showQrCodeFragmentIfAllowed(); + } + + private boolean gotPermission(String permission, String[] permissions, + int[] grantResults) { + for (int i = 0; i < permissions.length; i++) { + if (permission.equals(permissions[i])) + return grantResults[i] == PERMISSION_GRANTED; + } + return false; + } + + private boolean shouldShowRationale(String permission) { + return ActivityCompat.shouldShowRequestPermissionRationale(this, + permission); } private class BluetoothStateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - int state = intent.getIntExtra(EXTRA_STATE, 0); - if (state == STATE_ON) setBluetoothState(BluetoothState.ENABLED); - else setBluetoothState(BluetoothState.UNKNOWN); + String action = intent.getAction(); + if (ACTION_STATE_CHANGED.equals(action)) { + int state = intent.getIntExtra(EXTRA_STATE, 0); + if (state == STATE_ON) + setBluetoothState(BluetoothState.ENABLED); + else setBluetoothState(BluetoothState.UNKNOWN); + } else if (ACTION_SCAN_MODE_CHANGED.equals(action)) { + int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0); + if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) + setBluetoothState(BluetoothState.DISCOVERABLE); + else if (scanMode == SCAN_MODE_CONNECTABLE) + setBluetoothState(BluetoothState.ENABLED); + else setBluetoothState(BluetoothState.UNKNOWN); + } } } } diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 4875cc50c4b2c93205185adcfd32e2a579eb4c55..b67f0c25606bbf03e5f275ae438ca92a521480ab 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -468,8 +468,11 @@ <!-- Permission Requests --> <string name="permission_camera_title">Camera permission</string> <string name="permission_camera_request_body">To scan the QR code, Briar needs access to the camera.</string> + <string name="permission_location_title">Location permission</string> + <string name="permission_location_request_body">To discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string> + <string name="permission_camera_location_title">Camera and location</string> + <string name="permission_camera_location_request_body">To scan the QR code, Briar needs access to the camera.\n\nTo discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string> <string name="permission_camera_denied_body">You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access.</string> - <string name="permission_camera_denied_toast">Camera permission was not granted</string> <string name="qr_code">QR code</string> <string name="show_qr_code_fullscreen">Show QR code fullscreen</string>