diff --git a/bramble-android/src/main/AndroidManifest.xml b/bramble-android/src/main/AndroidManifest.xml index 9a4bd9810c28b97b47a029d928d2590c669e904e..d1aebe349908420397b42fffec1f1c2f1d836887 100644 --- a/bramble-android/src/main/AndroidManifest.xml +++ b/bramble-android/src/main/AndroidManifest.xml @@ -1,15 +1,25 @@ -<manifest - package="org.briarproject.bramble" - xmlns:android="http://schemas.android.com/apk/res/android"> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.briarproject.bramble"> - <uses-feature android:name="android.hardware.bluetooth" android:required="false"/> + <uses-feature + android:name="android.hardware.bluetooth" + android:required="false" /> - <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"/> - <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> - <uses-permission android:name="android.permission.INTERNET"/> - <uses-permission android:name="android.permission.WAKE_LOCK"/> + <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" + android:maxSdkVersion="30" /> + <uses-permission + android:name="android.permission.BLUETOOTH_ADMIN" + android:maxSdkVersion="30" /> + <uses-permission + android:name="android.permission.BLUETOOTH_SCAN" + android:usesPermissionFlags="neverForLocation" /> + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> + <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:allowBackup="false" 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 70f0229cf9a26371629d3417feb8b242d06f8896..f2d8c02ad1bad892405961bac37628521d66a06e 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 @@ -55,6 +55,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission; import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; @MethodsNotNullByDefault @@ -97,6 +98,11 @@ class AndroidBluetoothPlugin extends this.clock = clock; } + @Override + protected boolean isBluetoothAccessible() { + return hasBtConnectPermission(app); + } + @Override public void start() throws PluginException { super.start(); diff --git a/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java b/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java index 0f7ad28351cc93754d569553bdcfbbe1b8cf12ff..b6c0c87da12c11a55d11d6edb735012e38495e14 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java @@ -6,7 +6,6 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ContentResolver; import android.content.Context; -import android.os.Build; import android.os.Parcel; import android.os.StrictMode; import android.provider.Settings; @@ -15,12 +14,19 @@ import org.briarproject.nullsafety.NotNullByDefault; import java.io.DataOutputStream; import java.io.IOException; +import java.util.Set; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; +import static android.os.Build.FINGERPRINT; +import static android.os.Build.SERIAL; import static android.os.Build.VERSION.SDK_INT; +import static android.os.Process.myPid; +import static android.os.Process.myTid; +import static android.os.Process.myUid; import static android.provider.Settings.Secure.ANDROID_ID; +import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission; @Immutable @NotNullByDefault @@ -39,22 +45,27 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider { @Override protected void writeToEntropyPool(DataOutputStream out) throws IOException { super.writeToEntropyPool(out); - out.writeInt(android.os.Process.myPid()); - out.writeInt(android.os.Process.myTid()); - out.writeInt(android.os.Process.myUid()); - if (Build.FINGERPRINT != null) out.writeUTF(Build.FINGERPRINT); - if (Build.SERIAL != null) out.writeUTF(Build.SERIAL); + out.writeInt(myPid()); + out.writeInt(myTid()); + out.writeInt(myUid()); + if (FINGERPRINT != null) out.writeUTF(FINGERPRINT); + if (SERIAL != null) out.writeUTF(SERIAL); ContentResolver contentResolver = appContext.getContentResolver(); String id = Settings.Secure.getString(contentResolver, ANDROID_ID); if (id != null) out.writeUTF(id); - Parcel parcel = Parcel.obtain(); - BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - if (bt != null) { - for (BluetoothDevice device : bt.getBondedDevices()) - parcel.writeParcelable(device, 0); + // use bluetooth paired devices as well, if allowed + if (hasBtConnectPermission(appContext)) { + Parcel parcel = Parcel.obtain(); + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt != null) { + @SuppressLint("MissingPermission") + Set<BluetoothDevice> deviceSet = bt.getBondedDevices(); + for (BluetoothDevice device : deviceSet) + parcel.writeParcelable(device, 0); + } + out.write(parcel.marshall()); + parcel.recycle(); } - out.write(parcel.marshall()); - parcel.recycle(); } @Override @@ -77,7 +88,7 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider { .invoke(null, (Object) seed); // Mix the output of the Linux PRNG into the OpenSSL PRNG int bytesRead = (Integer) Class.forName( - "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") .getMethod("RAND_load_file", String.class, long.class) .invoke(null, "/dev/urandom", 1024); if (bytesRead != 1024) throw new IOException(); diff --git a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java index 6fe18e08094e78a2b6aa61c525ceb908f6ca910a..ffb4216ccbbaaf6a9d33fb968106bef131008508 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java @@ -22,9 +22,14 @@ import java.util.Scanner; import javax.annotation.Nullable; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_SCAN; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.content.Context.MODE_PRIVATE; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION.SDK_INT; +import static android.os.Process.myPid; +import static android.os.Process.myUid; import static java.lang.Runtime.getRuntime; import static java.util.Arrays.asList; import static org.briarproject.nullsafety.NullSafety.requireNonNull; @@ -49,6 +54,16 @@ public class AndroidUtils { return abis; } + public static boolean hasBtScanPermission(Context ctx) { + return SDK_INT < 31 || ctx.checkPermission(BLUETOOTH_SCAN, myPid(), + myUid()) == PERMISSION_GRANTED; + } + + public static boolean hasBtConnectPermission(Context ctx) { + return SDK_INT < 31 || ctx.checkPermission(BLUETOOTH_CONNECT, myPid(), + myUid()) == PERMISSION_GRANTED; + } + public static String getBluetoothAddress(Context ctx, BluetoothAdapter adapter) { return getBluetoothAddressAndMethod(ctx, adapter).getFirst(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java index 374c10d4f450de7a3f41b337738286a0c7b61d84..18a9be8947871dc1d9b46570821b7de0e89ceeb4 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java @@ -89,6 +89,16 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin, private volatile String contactConnectionsUuid = null; + /** + * Override and return true, if the plugin is now allowed to access the + * Bluetooth hardware, so it must be + * {@link org.briarproject.bramble.api.plugin.Plugin.State#DISABLED} + * in {@link #start()}. + */ + protected boolean isBluetoothAccessible() { + return true; + } + abstract void initialiseAdapter() throws IOException; abstract boolean isAdapterEnabled(); @@ -176,19 +186,28 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin, DEFAULT_PREF_PLUGIN_ENABLE); everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED, DEFAULT_PREF_EVER_CONNECTED)); + // disable plugin, if conditions for enabling are not met + if (enabledByUser && !isBluetoothAccessible()) { + enabledByUser = false; + settings.putBoolean(PREF_PLUGIN_ENABLE, false); + callback.mergeSettings(settings); + } state.setStarted(enabledByUser); try { initialiseAdapter(); } catch (IOException e) { throw new PluginException(e); } - updateProperties(); - if (enabledByUser && isAdapterEnabled()) bind(); + if (enabledByUser) { + updateProperties(); + if (isAdapterEnabled()) bind(); + } } private void bind() { ioExecutor.execute(() -> { if (getState() != INACTIVE) return; + if (contactConnectionsUuid == null) updateProperties(); // Bind a server socket to accept connections from contacts SS ss; try { diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 3b67935d48c1dee2ba9816df5cb6e5fd028d4d3b..3518fe872eecd8e0c8052f584b910b137378dc81 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -19,8 +19,6 @@ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> - <uses-permission android:name="android.permission.BLUETOOTH" /> - <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java index f40dbe094890a35cc5cd0cea9c9c5abb2d9bd494..3ff872c64a07625c131e8cbcc6a3c54b9db5815c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java @@ -11,20 +11,31 @@ import androidx.core.util.Consumer; import androidx.fragment.app.FragmentActivity; import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.BLUETOOTH_ADVERTISE; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_SCAN; import static android.Manifest.permission.CAMERA; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION.SDK_INT; import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; import static androidx.core.content.ContextCompat.checkSelfPermission; +import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission; +import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission; +import static org.briarproject.briar.android.util.Permission.GRANTED; +import static org.briarproject.briar.android.util.Permission.PERMANENTLY_DENIED; +import static org.briarproject.briar.android.util.Permission.SHOW_RATIONALE; +import static org.briarproject.briar.android.util.Permission.UNKNOWN; import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled; import static org.briarproject.briar.android.util.UiUtils.showDenialDialog; import static org.briarproject.briar.android.util.UiUtils.showLocationDialog; import static org.briarproject.briar.android.util.UiUtils.showRationale; +import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions; class AddNearbyContactPermissionManager { - private Permission cameraPermission = Permission.UNKNOWN; - private Permission locationPermission = Permission.UNKNOWN; + private Permission cameraPermission = UNKNOWN; + private Permission locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED; + private Permission bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN; private final FragmentActivity ctx; private final Consumer<String[]> requestPermissions; @@ -39,23 +50,32 @@ class AddNearbyContactPermissionManager { } void resetPermissions() { - cameraPermission = Permission.UNKNOWN; - locationPermission = Permission.UNKNOWN; + cameraPermission = UNKNOWN; + locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED; + bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN; } static boolean areEssentialPermissionsGranted(Context ctx, boolean isBluetoothSupported) { int ok = PERMISSION_GRANTED; - return checkSelfPermission(ctx, CAMERA) == ok && - (SDK_INT < 23 || - checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok || - !isBluetoothSupported); + boolean bluetoothOk; + if (!isBluetoothSupported || SDK_INT < 23) { + bluetoothOk = true; + } else if (SDK_INT < 31) { + bluetoothOk = checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok; + } else { + bluetoothOk = hasBtConnectPermission(ctx) && + hasBtScanPermission(ctx) && + checkSelfPermission(ctx, BLUETOOTH_ADVERTISE) == ok; + } + return bluetoothOk && checkSelfPermission(ctx, CAMERA) == ok; } private boolean areEssentialPermissionsGranted() { - return cameraPermission == Permission.GRANTED && - (SDK_INT < 23 || locationPermission == Permission.GRANTED || - !isBluetoothSupported); + boolean bluetoothGranted = locationPermission == GRANTED && + bluetoothPermissions == GRANTED; + return cameraPermission == GRANTED && + (SDK_INT < 23 || !isBluetoothSupported || bluetoothGranted); } boolean checkPermissions() { @@ -63,31 +83,40 @@ class AddNearbyContactPermissionManager { if (locationEnabled && areEssentialPermissionsGranted()) return true; // If an essential permission has been permanently denied, ask the // user to change the setting - if (cameraPermission == Permission.PERMANENTLY_DENIED) { + if (cameraPermission == PERMANENTLY_DENIED) { showDenialDialog(ctx, R.string.permission_camera_title, R.string.permission_camera_denied_body); return false; } - if (isBluetoothSupported && - locationPermission == Permission.PERMANENTLY_DENIED) { + if (isBluetoothSupported && locationPermission == PERMANENTLY_DENIED) { showDenialDialog(ctx, R.string.permission_location_title, R.string.permission_location_denied_body); return false; } + if (isBluetoothSupported && + bluetoothPermissions == PERMANENTLY_DENIED) { + showDenialDialog(ctx, R.string.permission_bluetooth_title, + R.string.permission_bluetooth_denied_body); + return false; + } // Should we show the rationale for one or both permissions? - if (cameraPermission == Permission.SHOW_RATIONALE && - locationPermission == Permission.SHOW_RATIONALE) { + if (cameraPermission == SHOW_RATIONALE && + locationPermission == SHOW_RATIONALE) { showRationale(ctx, R.string.permission_camera_location_title, R.string.permission_camera_location_request_body, this::requestPermissions); - } else if (cameraPermission == Permission.SHOW_RATIONALE) { + } else if (cameraPermission == SHOW_RATIONALE) { showRationale(ctx, R.string.permission_camera_title, R.string.permission_camera_request_body, this::requestPermissions); - } else if (locationPermission == Permission.SHOW_RATIONALE) { + } else if (locationPermission == SHOW_RATIONALE) { showRationale(ctx, R.string.permission_location_title, R.string.permission_location_request_body, this::requestPermissions); + } else if (bluetoothPermissions == SHOW_RATIONALE) { + showRationale(ctx, R.string.permission_bluetooth_title, + R.string.permission_bluetooth_body, + this::requestPermissions); } else if (locationEnabled) { requestPermissions(); } else { @@ -99,7 +128,12 @@ class AddNearbyContactPermissionManager { private void requestPermissions() { String[] permissions; if (isBluetoothSupported) { - permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION}; + if (SDK_INT < 31) { + permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION}; + } else { + permissions = new String[] {CAMERA, BLUETOOTH_ADVERTISE, + BLUETOOTH_CONNECT, BLUETOOTH_SCAN}; + } } else { permissions = new String[] {CAMERA}; } @@ -108,19 +142,29 @@ class AddNearbyContactPermissionManager { void onRequestPermissionResult(Map<String, Boolean> result) { if (gotPermission(CAMERA, result)) { - cameraPermission = Permission.GRANTED; + cameraPermission = GRANTED; } else if (shouldShowRationale(CAMERA)) { - cameraPermission = Permission.SHOW_RATIONALE; + cameraPermission = SHOW_RATIONALE; } else { - cameraPermission = Permission.PERMANENTLY_DENIED; + cameraPermission = PERMANENTLY_DENIED; } if (isBluetoothSupported) { - if (gotPermission(ACCESS_FINE_LOCATION, result)) { - locationPermission = Permission.GRANTED; - } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { - locationPermission = Permission.SHOW_RATIONALE; + if (SDK_INT < 31) { + if (gotPermission(ACCESS_FINE_LOCATION, result)) { + locationPermission = GRANTED; + } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { + locationPermission = SHOW_RATIONALE; + } else { + locationPermission = PERMANENTLY_DENIED; + } } else { - locationPermission = Permission.PERMANENTLY_DENIED; + if (wasGrantedBluetoothPermissions(result)) { + bluetoothPermissions = GRANTED; + } else if (shouldShowRationale(BLUETOOTH_CONNECT)) { + bluetoothPermissions = SHOW_RATIONALE; + } else { + bluetoothPermissions = PERMANENTLY_DENIED; + } } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java index f5c543145c03034c0c38328dd6a479974ae48b68..4920e6da6a7d9e8f71c2fb1c39bdc90ed6194c6b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.contact.add.nearby; +import android.annotation.SuppressLint; import android.app.Application; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; @@ -250,6 +251,7 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread + @SuppressLint("MissingPermission") // we check permissions before private boolean isBluetoothReady() { if (bt == null || bluetoothPlugin == null) { // Continue without Bluetooth diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java index 7a8ac1d9ae0bb15099ad1b180d5fd077317f4d02..fc80179bc4b62ebc66750d8ac40237709a5f3a91 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java @@ -5,58 +5,101 @@ import android.content.Context; import org.briarproject.briar.R; import org.briarproject.briar.android.util.Permission; +import org.briarproject.briar.android.util.UiUtils; + +import java.util.Map; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION.SDK_INT; import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; +import static androidx.core.content.ContextCompat.checkSelfPermission; +import static org.briarproject.briar.android.util.Permission.GRANTED; +import static org.briarproject.briar.android.util.Permission.PERMANENTLY_DENIED; +import static org.briarproject.briar.android.util.Permission.SHOW_RATIONALE; +import static org.briarproject.briar.android.util.Permission.UNKNOWN; import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled; +import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions; import static org.briarproject.briar.android.util.UiUtils.showLocationDialog; +import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions; class BluetoothConditionManager { - private Permission locationPermission = Permission.UNKNOWN; + private Permission locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED; + private Permission bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN; /** * Call this when the using activity or fragment starts, * because permissions might have changed while it was stopped. */ void reset() { - locationPermission = Permission.UNKNOWN; + locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED; + bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN; + } + + @UiThread + void requestPermissions(ActivityResultLauncher<String[]> launcher) { + if (SDK_INT < 31) { + launcher.launch(new String[] {ACCESS_FINE_LOCATION}); + } else { + requestBluetoothPermissions(launcher); + } } @UiThread void onLocationPermissionResult(Activity activity, - @Nullable Boolean result) { - if (result != null && result) { - locationPermission = Permission.GRANTED; - } else if (shouldShowRequestPermissionRationale(activity, - ACCESS_FINE_LOCATION)) { - locationPermission = Permission.SHOW_RATIONALE; + @Nullable Map<String, Boolean> result) { + if (SDK_INT < 31) { + if (gotPermission(activity, result)) { + locationPermission = GRANTED; + } else if (shouldShowRequestPermissionRationale(activity, + ACCESS_FINE_LOCATION)) { + locationPermission = SHOW_RATIONALE; + } else { + locationPermission = PERMANENTLY_DENIED; + } } else { - locationPermission = Permission.PERMANENTLY_DENIED; + if (wasGrantedBluetoothPermissions(result)) { + bluetoothPermissions = GRANTED; + } else if (shouldShowRequestPermissionRationale(activity, + BLUETOOTH_CONNECT)) { + bluetoothPermissions = SHOW_RATIONALE; + } else { + bluetoothPermissions = PERMANENTLY_DENIED; + } } } - boolean areRequirementsFulfilled(Context ctx, - ActivityResultLauncher<String> permissionRequest, + boolean areRequirementsFulfilled(FragmentActivity ctx, + ActivityResultLauncher<String[]> permissionRequest, Runnable onLocationDenied) { boolean permissionGranted = - SDK_INT < 23 || locationPermission == Permission.GRANTED; + (SDK_INT < 23 || locationPermission == GRANTED) && + bluetoothPermissions == GRANTED; boolean locationEnabled = isLocationEnabled(ctx); if (permissionGranted && locationEnabled) return true; - if (locationPermission == Permission.PERMANENTLY_DENIED) { + if (locationPermission == PERMANENTLY_DENIED) { showDenialDialog(ctx, onLocationDenied); - } else if (locationPermission == Permission.SHOW_RATIONALE) { + } else if (locationPermission == SHOW_RATIONALE) { showRationale(ctx, permissionRequest); } else if (!locationEnabled) { showLocationDialog(ctx); + } else if (bluetoothPermissions == PERMANENTLY_DENIED) { + UiUtils.showDenialDialog(ctx, R.string.permission_bluetooth_title, + R.string.permission_bluetooth_denied_body); + } else if (bluetoothPermissions == SHOW_RATIONALE && SDK_INT >= 31) { + UiUtils.showRationale(ctx, R.string.permission_bluetooth_title, + R.string.permission_bluetooth_body, () -> + requestBluetoothPermissions(permissionRequest)); } return false; } @@ -72,13 +115,27 @@ class BluetoothConditionManager { } private void showRationale(Context ctx, - ActivityResultLauncher<String> permissionRequest) { + ActivityResultLauncher<String[]> permissionRequest) { new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) .setTitle(R.string.permission_location_title) .setMessage(R.string.permission_location_request_body) .setPositiveButton(R.string.ok, (dialog, which) -> - permissionRequest.launch(ACCESS_FINE_LOCATION)) + permissionRequest.launch( + new String[] {ACCESS_FINE_LOCATION})) .show(); } + private boolean gotPermission(Context ctx, + @Nullable Map<String, Boolean> result) { + Boolean permissionResult = + result == null ? null : result.get(ACCESS_FINE_LOCATION); + return permissionResult == null ? isLocationPermissionGranted(ctx) : + permissionResult; + } + + private boolean isLocationPermissionGranted(Context ctx) { + return checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == + PERMISSION_GRANTED; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java index ed774bcd2d1d2be3a9b4b6aa60b52b098f91ef5c..2e2413f5182a68466fc4df76b5992c3e9db2a25c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.contact.connect; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -14,15 +13,17 @@ import org.briarproject.briar.android.util.ActivityLaunchers.RequestBluetoothDis import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault; +import java.util.Map; + import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.widget.Toast.LENGTH_LONG; import static org.briarproject.briar.android.AppModule.getAndroidComponent; @@ -42,8 +43,8 @@ public class BluetoothIntroFragment extends Fragment { private final ActivityResultLauncher<Integer> bluetoothDiscoverableRequest = registerForActivityResult(new RequestBluetoothDiscoverable(), this::onBluetoothDiscoverable); - private final ActivityResultLauncher<String> permissionRequest = - registerForActivityResult(new RequestPermission(), + private final ActivityResultLauncher<String[]> permissionRequest = + registerForActivityResult(new RequestMultiplePermissions(), this::onPermissionRequestResult); @Override @@ -80,12 +81,13 @@ public class BluetoothIntroFragment extends Fragment { // if the permission is already granted. // So we can use the request as a generic entry point // to the whole flow. - permissionRequest.launch(ACCESS_FINE_LOCATION); + conditionManager.requestPermissions(permissionRequest); } } - private void onPermissionRequestResult(@Nullable Boolean result) { - Activity a = requireActivity(); + private void onPermissionRequestResult( + @Nullable Map<String, Boolean> result) { + FragmentActivity a = requireActivity(); // update permission result in BluetoothConnecter conditionManager.onLocationPermissionResult(a, result); // what to do when the user denies granting the location permission diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java index 81de79aa4d9ca9f4eb245c833f17206424c7d3f4..283b9f0eba2d09998931314295bd706ca4143532 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java @@ -26,18 +26,24 @@ import org.briarproject.nullsafety.ParametersNotNullByDefault; import java.util.ArrayList; import java.util.List; +import java.util.Map; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; import androidx.annotation.ColorRes; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SwitchCompat; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.os.Build.VERSION.SDK_INT; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; @@ -47,7 +53,13 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY; import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED; import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA; +import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission; +import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission; +import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions; +import static org.briarproject.briar.android.util.UiUtils.showDenialDialog; import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; +import static org.briarproject.briar.android.util.UiUtils.showRationale; +import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -61,6 +73,11 @@ public class TransportsActivity extends BriarActivity { private PluginViewModel viewModel; private BaseAdapter transportsAdapter; + @RequiresApi(31) + private final ActivityResultLauncher<String[]> requestPermissionLauncher = + registerForActivityResult(new RequestMultiplePermissions(), + this::handleBtPermissionResult); + @Override public void injectActivity(ActivityComponent component) { component.inject(this); @@ -149,8 +166,7 @@ public class TransportsActivity extends BriarActivity { view.findViewById(R.id.switchCompat); switchCompat.setText(getString(t.switchLabel)); switchCompat.setOnClickListener(v -> - viewModel.enableTransport(t.id, - switchCompat.isChecked())); + onClicked(t.id, switchCompat.isChecked())); switchCompat.setChecked(t.isSwitchChecked); TextView summary = view.findViewById(R.id.summary); @@ -203,6 +219,21 @@ public class TransportsActivity extends BriarActivity { }); } + private void onClicked(TransportId transportId, boolean enable) { + if (enable && SDK_INT >= 31 && + (!hasBtConnectPermission(this) || !hasBtScanPermission(this))) { + if (shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) { + showRationale(this, R.string.permission_bluetooth_title, + R.string.permission_bluetooth_body, + this::requestBtPermissions); + } else { + requestBtPermissions(); + } + } else { + viewModel.enableTransport(transportId, enable); + } + } + private String getBulletString(@StringRes int resId) { return "\u2022 " + getString(resId); } @@ -316,6 +347,23 @@ public class TransportsActivity extends BriarActivity { return transport; } + @RequiresApi(31) + private void requestBtPermissions() { + requestBluetoothPermissions(requestPermissionLauncher); + } + + @RequiresApi(31) + private void handleBtPermissionResult(Map<String, Boolean> grantedMap) { + if (wasGrantedBluetoothPermissions(grantedMap)) { + viewModel.enableTransport(BluetoothConstants.ID, true); + } else { + transportsAdapter.notifyDataSetChanged(); + showDenialDialog(this, + R.string.permission_bluetooth_title, + R.string.permission_bluetooth_denied_body); + } + } + private static class Transport { private final TransportId id; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java index ef9e6ad941ff986cf23674936eab315b7209d142..66f90e48cb20adb6f9ff75e725db5f5e661d2695 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java @@ -58,6 +58,8 @@ import static java.util.Locale.US; import static java.util.Objects.requireNonNull; import static java.util.TimeZone.getTimeZone; import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod; +import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission; +import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission; import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress; import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @@ -273,12 +275,13 @@ class BriarReportCollector { // Is Bluetooth enabled? @SuppressLint("HardwareIds") - boolean btEnabled = bt.isEnabled() + boolean btEnabled = hasBtConnectPermission(ctx) && bt.isEnabled() && !isNullOrEmpty(bt.getAddress()); connectivityInfo.add("BluetoothEnabled", btEnabled); // Is Bluetooth connectable? - int scanMode = bt.getScanMode(); + @SuppressLint("MissingPermission") + int scanMode = hasBtScanPermission(ctx) ? bt.getScanMode() : -1; boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE || scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE; connectivityInfo.add("BluetoothConnectable", btConnectable); @@ -298,11 +301,14 @@ class BriarReportCollector { btLeAdvertise); } - Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt); - String address = p.getFirst(); - String method = p.getSecond(); - connectivityInfo.add("BluetoothAddress", scrubMacAddress(address)); - connectivityInfo.add("BluetoothAddressMethod", method); + if (hasBtConnectPermission(ctx)) { + Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt); + String address = p.getFirst(); + String method = p.getSecond(); + connectivityInfo.add("BluetoothAddress", + scrubMacAddress(address)); + connectivityInfo.add("BluetoothAddressMethod", method); + } } return new ReportItem("Connectivity", R.string.dev_report_connectivity, connectivityInfo); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java index afb594ab59384eb70cca94c5b62127527abcf0dc..465de4441a36eff76d40f76d52351c1fbc7138ea 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java @@ -8,18 +8,32 @@ import org.briarproject.briar.R; import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault; +import java.util.Map; + import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; import androidx.preference.ListPreference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.os.Build.VERSION.SDK_INT; +import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission; +import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission; import static org.briarproject.briar.android.AppModule.getAndroidComponent; import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist; +import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions; +import static org.briarproject.briar.android.util.UiUtils.showDenialDialog; +import static org.briarproject.briar.android.util.UiUtils.showRationale; +import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -47,6 +61,11 @@ public class ConnectionsFragment extends PreferenceFragmentCompat { private SwitchPreferenceCompat torMobile; private SwitchPreferenceCompat torOnlyWhenCharging; + @RequiresApi(31) + private final ActivityResultLauncher<String[]> requestPermissionLauncher = + registerForActivityResult(new RequestMultiplePermissions(), + this::handleBtPermissionResult); + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -69,6 +88,25 @@ public class ConnectionsFragment extends PreferenceFragmentCompat { torNetwork.setSummaryProvider(viewModel.torSummaryProvider); + if (SDK_INT >= 31) { + enableBluetooth.setOnPreferenceChangeListener((p, value) -> { + FragmentActivity ctx = requireActivity(); + if (hasBtConnectPermission(ctx) && hasBtScanPermission(ctx)) { + return true; + } else if (shouldShowRequestPermissionRationale( + BLUETOOTH_CONNECT)) { + showRationale(ctx, R.string.permission_bluetooth_title, + R.string.permission_bluetooth_body, + this::requestBtPermissions); + // we don't update the preference directly, + // but do it via the launcher, if we got the permissions + return false; + } else { + requestBtPermissions(); + return false; + } + }); + } enableBluetooth.setPreferenceDataStore(connectionsManager.btStore); enableWifi.setPreferenceDataStore(connectionsManager.wifiStore); enableTor.setPreferenceDataStore(connectionsManager.torStore); @@ -115,4 +153,19 @@ public class ConnectionsFragment extends PreferenceFragmentCompat { requireActivity().setTitle(R.string.network_settings_title); } + @RequiresApi(31) + private void requestBtPermissions() { + requestBluetoothPermissions(requestPermissionLauncher); + } + + @RequiresApi(31) + private void handleBtPermissionResult(Map<String, Boolean> grantedMap) { + if (wasGrantedBluetoothPermissions(grantedMap)) { + enableBluetooth.setChecked(true); + } else { + showDenialDialog(requireActivity(), + R.string.permission_bluetooth_title, + R.string.permission_bluetooth_denied_body); + } + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index baa8939640bce589940c537242d0404ee90fafd5..b97ee36e3ac1f7f73a47cafc302154b8f546154b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -44,6 +44,7 @@ import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault; import java.util.Locale; +import java.util.Map; import java.util.logging.Logger; import androidx.activity.result.ActivityResultLauncher; @@ -70,6 +71,9 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; +import static android.Manifest.permission.BLUETOOTH_ADVERTISE; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_SCAN; import static android.content.Context.KEYGUARD_SERVICE; import static android.content.Intent.CATEGORY_DEFAULT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -105,6 +109,7 @@ import static androidx.core.content.ContextCompat.getColor; import static androidx.core.content.ContextCompat.getSystemService; import static androidx.core.graphics.drawable.DrawableCompat.setTint; import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL; +import static java.lang.Boolean.TRUE; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.logging.Level.WARNING; @@ -346,10 +351,10 @@ public class UiUtils { /** * @return true if location is enabled, - * or it isn't required due to this being a SDK < 28 device. + * or it isn't required due to this being a device with SDK < 28 or >= 31. */ public static boolean isLocationEnabled(Context ctx) { - if (SDK_INT >= 28) { + if (SDK_INT >= 28 && SDK_INT < 31) { LocationManager lm = ctx.getSystemService(LocationManager.class); return lm.isLocationEnabled(); } else { @@ -625,4 +630,21 @@ public class UiUtils { } Toast.makeText(ctx, R.string.error_start_activity, LENGTH_LONG).show(); } + + @RequiresApi(31) + public static void requestBluetoothPermissions( + ActivityResultLauncher<String[]> launcher) { + String[] perms = new String[] {BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, + BLUETOOTH_SCAN}; + launcher.launch(perms); + } + + @RequiresApi(31) + public static boolean wasGrantedBluetoothPermissions( + @Nullable Map<String, Boolean> grantedMap) { + return grantedMap != null && + TRUE.equals(grantedMap.get(BLUETOOTH_ADVERTISE)) && + TRUE.equals(grantedMap.get(BLUETOOTH_CONNECT)) && + TRUE.equals(grantedMap.get(BLUETOOTH_SCAN)); + } } diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 3bf3edf4952666a947b2a507b0be3b0b249052a1..0d310498676683e4ee96537ba55f2990322c3626 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -782,6 +782,10 @@ <string name="permission_location_setting_title">Location setting</string> <string name="permission_location_setting_body">Your device\'s location setting must be turned on to find other devices via Bluetooth. Please enable location to continue. You can disable it again afterwards.</string> <string name="permission_location_setting_button">Enable location</string> + <string name="permission_bluetooth_title">Nearby devices permission</string> + <string name="permission_bluetooth_body">To use Bluetooth communication, Briar needs permission to find and connect to nearby devices.</string> + <string name="permission_bluetooth_denied_body">You have denied access to nearby devices, but Briar needs this permission to use Bluetooth.\n\nPlease consider granting access.</string> + <string name="qr_code">QR code</string> <string name="show_qr_code_fullscreen">Show QR code fullscreen</string>