Handle new BLUETOOTH_SCAN and BLUETOOTH_CONNECT permission

We need to have those permissions before doing things like accessing the Bluetooth address. So we force-disable the Bluetooth plugin if the permission is not granted. The UI then forces the permission before allowing to enable the plugin.
This commit is contained in:
Torsten Grote
2022-09-12 17:05:52 -03:00
parent 113793045f
commit 824a9e1124
15 changed files with 387 additions and 90 deletions

View File

@@ -1,15 +1,25 @@
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.briarproject.bramble" package="org.briarproject.bramble">
xmlns:android="http://schemas.android.com/apk/res/android">
<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_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> android:name="android.permission.BLUETOOTH"
<uses-permission android:name="android.permission.INTERNET"/> android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WAKE_LOCK"/> <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 <application
android:allowBackup="false" android:allowBackup="false"

View File

@@ -55,6 +55,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -97,6 +98,11 @@ class AndroidBluetoothPlugin extends
this.clock = clock; this.clock = clock;
} }
@Override
protected boolean isBluetoothAccessible() {
return hasBtConnectPermission(app);
}
@Override @Override
public void start() throws PluginException { public void start() throws PluginException {
super.start(); super.start();

View File

@@ -6,7 +6,6 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.os.Parcel; import android.os.Parcel;
import android.os.StrictMode; import android.os.StrictMode;
import android.provider.Settings; import android.provider.Settings;
@@ -15,12 +14,19 @@ import org.briarproject.nullsafety.NotNullByDefault;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Set;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; 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.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 android.provider.Settings.Secure.ANDROID_ID;
import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
@@ -39,22 +45,27 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
@Override @Override
protected void writeToEntropyPool(DataOutputStream out) throws IOException { protected void writeToEntropyPool(DataOutputStream out) throws IOException {
super.writeToEntropyPool(out); super.writeToEntropyPool(out);
out.writeInt(android.os.Process.myPid()); out.writeInt(myPid());
out.writeInt(android.os.Process.myTid()); out.writeInt(myTid());
out.writeInt(android.os.Process.myUid()); out.writeInt(myUid());
if (Build.FINGERPRINT != null) out.writeUTF(Build.FINGERPRINT); if (FINGERPRINT != null) out.writeUTF(FINGERPRINT);
if (Build.SERIAL != null) out.writeUTF(Build.SERIAL); if (SERIAL != null) out.writeUTF(SERIAL);
ContentResolver contentResolver = appContext.getContentResolver(); ContentResolver contentResolver = appContext.getContentResolver();
String id = Settings.Secure.getString(contentResolver, ANDROID_ID); String id = Settings.Secure.getString(contentResolver, ANDROID_ID);
if (id != null) out.writeUTF(id); if (id != null) out.writeUTF(id);
Parcel parcel = Parcel.obtain(); // use bluetooth paired devices as well, if allowed
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); if (hasBtConnectPermission(appContext)) {
if (bt != null) { Parcel parcel = Parcel.obtain();
for (BluetoothDevice device : bt.getBondedDevices()) BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
parcel.writeParcelable(device, 0); 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 @Override
@@ -77,7 +88,7 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
.invoke(null, (Object) seed); .invoke(null, (Object) seed);
// Mix the output of the Linux PRNG into the OpenSSL PRNG // Mix the output of the Linux PRNG into the OpenSSL PRNG
int bytesRead = (Integer) Class.forName( 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) .getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024); .invoke(null, "/dev/urandom", 1024);
if (bytesRead != 1024) throw new IOException(); if (bytesRead != 1024) throw new IOException();

View File

@@ -22,9 +22,14 @@ import java.util.Scanner;
import javax.annotation.Nullable; 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.app.PendingIntent.FLAG_IMMUTABLE;
import static android.content.Context.MODE_PRIVATE; 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.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.lang.Runtime.getRuntime;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.briarproject.nullsafety.NullSafety.requireNonNull; import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@@ -49,6 +54,16 @@ public class AndroidUtils {
return abis; 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, public static String getBluetoothAddress(Context ctx,
BluetoothAdapter adapter) { BluetoothAdapter adapter) {
return getBluetoothAddressAndMethod(ctx, adapter).getFirst(); return getBluetoothAddressAndMethod(ctx, adapter).getFirst();

View File

@@ -89,6 +89,16 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
private volatile String contactConnectionsUuid = null; 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 void initialiseAdapter() throws IOException;
abstract boolean isAdapterEnabled(); abstract boolean isAdapterEnabled();
@@ -176,19 +186,28 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
DEFAULT_PREF_PLUGIN_ENABLE); DEFAULT_PREF_PLUGIN_ENABLE);
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED, everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
DEFAULT_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); state.setStarted(enabledByUser);
try { try {
initialiseAdapter(); initialiseAdapter();
} catch (IOException e) { } catch (IOException e) {
throw new PluginException(e); throw new PluginException(e);
} }
updateProperties(); if (enabledByUser) {
if (enabledByUser && isAdapterEnabled()) bind(); updateProperties();
if (isAdapterEnabled()) bind();
}
} }
private void bind() { private void bind() {
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
if (getState() != INACTIVE) return; if (getState() != INACTIVE) return;
if (contactConnectionsUuid == null) updateProperties();
// Bind a server socket to accept connections from contacts // Bind a server socket to accept connections from contacts
SS ss; SS ss;
try { try {

View File

@@ -19,8 +19,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_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.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.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

View File

@@ -11,20 +11,31 @@ import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import static android.Manifest.permission.ACCESS_FINE_LOCATION; 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.Manifest.permission.CAMERA;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
import static androidx.core.content.ContextCompat.checkSelfPermission; 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.isLocationEnabled;
import static org.briarproject.briar.android.util.UiUtils.showDenialDialog; 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.showLocationDialog;
import static org.briarproject.briar.android.util.UiUtils.showRationale; import static org.briarproject.briar.android.util.UiUtils.showRationale;
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
class AddNearbyContactPermissionManager { class AddNearbyContactPermissionManager {
private Permission cameraPermission = Permission.UNKNOWN; private Permission cameraPermission = UNKNOWN;
private Permission locationPermission = Permission.UNKNOWN; private Permission locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED;
private Permission bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN;
private final FragmentActivity ctx; private final FragmentActivity ctx;
private final Consumer<String[]> requestPermissions; private final Consumer<String[]> requestPermissions;
@@ -39,23 +50,32 @@ class AddNearbyContactPermissionManager {
} }
void resetPermissions() { void resetPermissions() {
cameraPermission = Permission.UNKNOWN; cameraPermission = UNKNOWN;
locationPermission = Permission.UNKNOWN; locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED;
bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN;
} }
static boolean areEssentialPermissionsGranted(Context ctx, static boolean areEssentialPermissionsGranted(Context ctx,
boolean isBluetoothSupported) { boolean isBluetoothSupported) {
int ok = PERMISSION_GRANTED; int ok = PERMISSION_GRANTED;
return checkSelfPermission(ctx, CAMERA) == ok && boolean bluetoothOk;
(SDK_INT < 23 || if (!isBluetoothSupported || SDK_INT < 23) {
checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok || bluetoothOk = true;
!isBluetoothSupported); } 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() { private boolean areEssentialPermissionsGranted() {
return cameraPermission == Permission.GRANTED && boolean bluetoothGranted = locationPermission == GRANTED &&
(SDK_INT < 23 || locationPermission == Permission.GRANTED || bluetoothPermissions == GRANTED;
!isBluetoothSupported); return cameraPermission == GRANTED &&
(SDK_INT < 23 || !isBluetoothSupported || bluetoothGranted);
} }
boolean checkPermissions() { boolean checkPermissions() {
@@ -63,31 +83,40 @@ class AddNearbyContactPermissionManager {
if (locationEnabled && areEssentialPermissionsGranted()) return true; if (locationEnabled && areEssentialPermissionsGranted()) return true;
// If an essential permission has been permanently denied, ask the // If an essential permission has been permanently denied, ask the
// user to change the setting // user to change the setting
if (cameraPermission == Permission.PERMANENTLY_DENIED) { if (cameraPermission == PERMANENTLY_DENIED) {
showDenialDialog(ctx, R.string.permission_camera_title, showDenialDialog(ctx, R.string.permission_camera_title,
R.string.permission_camera_denied_body); R.string.permission_camera_denied_body);
return false; return false;
} }
if (isBluetoothSupported && if (isBluetoothSupported && locationPermission == PERMANENTLY_DENIED) {
locationPermission == Permission.PERMANENTLY_DENIED) {
showDenialDialog(ctx, R.string.permission_location_title, showDenialDialog(ctx, R.string.permission_location_title,
R.string.permission_location_denied_body); R.string.permission_location_denied_body);
return false; 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? // Should we show the rationale for one or both permissions?
if (cameraPermission == Permission.SHOW_RATIONALE && if (cameraPermission == SHOW_RATIONALE &&
locationPermission == Permission.SHOW_RATIONALE) { locationPermission == SHOW_RATIONALE) {
showRationale(ctx, R.string.permission_camera_location_title, showRationale(ctx, R.string.permission_camera_location_title,
R.string.permission_camera_location_request_body, R.string.permission_camera_location_request_body,
this::requestPermissions); this::requestPermissions);
} else if (cameraPermission == Permission.SHOW_RATIONALE) { } else if (cameraPermission == SHOW_RATIONALE) {
showRationale(ctx, R.string.permission_camera_title, showRationale(ctx, R.string.permission_camera_title,
R.string.permission_camera_request_body, R.string.permission_camera_request_body,
this::requestPermissions); this::requestPermissions);
} else if (locationPermission == Permission.SHOW_RATIONALE) { } else if (locationPermission == SHOW_RATIONALE) {
showRationale(ctx, R.string.permission_location_title, showRationale(ctx, R.string.permission_location_title,
R.string.permission_location_request_body, R.string.permission_location_request_body,
this::requestPermissions); this::requestPermissions);
} else if (bluetoothPermissions == SHOW_RATIONALE) {
showRationale(ctx, R.string.permission_bluetooth_title,
R.string.permission_bluetooth_body,
this::requestPermissions);
} else if (locationEnabled) { } else if (locationEnabled) {
requestPermissions(); requestPermissions();
} else { } else {
@@ -99,7 +128,12 @@ class AddNearbyContactPermissionManager {
private void requestPermissions() { private void requestPermissions() {
String[] permissions; String[] permissions;
if (isBluetoothSupported) { 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 { } else {
permissions = new String[] {CAMERA}; permissions = new String[] {CAMERA};
} }
@@ -108,19 +142,29 @@ class AddNearbyContactPermissionManager {
void onRequestPermissionResult(Map<String, Boolean> result) { void onRequestPermissionResult(Map<String, Boolean> result) {
if (gotPermission(CAMERA, result)) { if (gotPermission(CAMERA, result)) {
cameraPermission = Permission.GRANTED; cameraPermission = GRANTED;
} else if (shouldShowRationale(CAMERA)) { } else if (shouldShowRationale(CAMERA)) {
cameraPermission = Permission.SHOW_RATIONALE; cameraPermission = SHOW_RATIONALE;
} else { } else {
cameraPermission = Permission.PERMANENTLY_DENIED; cameraPermission = PERMANENTLY_DENIED;
} }
if (isBluetoothSupported) { if (isBluetoothSupported) {
if (gotPermission(ACCESS_FINE_LOCATION, result)) { if (SDK_INT < 31) {
locationPermission = Permission.GRANTED; if (gotPermission(ACCESS_FINE_LOCATION, result)) {
} else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { locationPermission = GRANTED;
locationPermission = Permission.SHOW_RATIONALE; } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
locationPermission = SHOW_RATIONALE;
} else {
locationPermission = PERMANENTLY_DENIED;
}
} else { } else {
locationPermission = Permission.PERMANENTLY_DENIED; if (wasGrantedBluetoothPermissions(result)) {
bluetoothPermissions = GRANTED;
} else if (shouldShowRationale(BLUETOOTH_CONNECT)) {
bluetoothPermissions = SHOW_RATIONALE;
} else {
bluetoothPermissions = PERMANENTLY_DENIED;
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.contact.add.nearby;
import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@@ -250,6 +251,7 @@ class AddNearbyContactViewModel extends AndroidViewModel
} }
@UiThread @UiThread
@SuppressLint("MissingPermission") // we check permissions before
private boolean isBluetoothReady() { private boolean isBluetoothReady() {
if (bt == null || bluetoothPlugin == null) { if (bt == null || bluetoothPlugin == null) {
// Continue without Bluetooth // Continue without Bluetooth

View File

@@ -5,58 +5,101 @@ import android.content.Context;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.util.Permission; 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.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import static android.Manifest.permission.ACCESS_FINE_LOCATION; 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 android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; 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.getGoToSettingsListener;
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled; 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.showLocationDialog;
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
class BluetoothConditionManager { 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, * Call this when the using activity or fragment starts,
* because permissions might have changed while it was stopped. * because permissions might have changed while it was stopped.
*/ */
void reset() { 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 @UiThread
void onLocationPermissionResult(Activity activity, void onLocationPermissionResult(Activity activity,
@Nullable Boolean result) { @Nullable Map<String, Boolean> result) {
if (result != null && result) { if (SDK_INT < 31) {
locationPermission = Permission.GRANTED; if (gotPermission(activity, result)) {
} else if (shouldShowRequestPermissionRationale(activity, locationPermission = GRANTED;
ACCESS_FINE_LOCATION)) { } else if (shouldShowRequestPermissionRationale(activity,
locationPermission = Permission.SHOW_RATIONALE; ACCESS_FINE_LOCATION)) {
locationPermission = SHOW_RATIONALE;
} else {
locationPermission = PERMANENTLY_DENIED;
}
} else { } 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, boolean areRequirementsFulfilled(FragmentActivity ctx,
ActivityResultLauncher<String> permissionRequest, ActivityResultLauncher<String[]> permissionRequest,
Runnable onLocationDenied) { Runnable onLocationDenied) {
boolean permissionGranted = boolean permissionGranted =
SDK_INT < 23 || locationPermission == Permission.GRANTED; (SDK_INT < 23 || locationPermission == GRANTED) &&
bluetoothPermissions == GRANTED;
boolean locationEnabled = isLocationEnabled(ctx); boolean locationEnabled = isLocationEnabled(ctx);
if (permissionGranted && locationEnabled) return true; if (permissionGranted && locationEnabled) return true;
if (locationPermission == Permission.PERMANENTLY_DENIED) { if (locationPermission == PERMANENTLY_DENIED) {
showDenialDialog(ctx, onLocationDenied); showDenialDialog(ctx, onLocationDenied);
} else if (locationPermission == Permission.SHOW_RATIONALE) { } else if (locationPermission == SHOW_RATIONALE) {
showRationale(ctx, permissionRequest); showRationale(ctx, permissionRequest);
} else if (!locationEnabled) { } else if (!locationEnabled) {
showLocationDialog(ctx); 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; return false;
} }
@@ -72,13 +115,27 @@ class BluetoothConditionManager {
} }
private void showRationale(Context ctx, private void showRationale(Context ctx,
ActivityResultLauncher<String> permissionRequest) { ActivityResultLauncher<String[]> permissionRequest) {
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) new AlertDialog.Builder(ctx, R.style.BriarDialogTheme)
.setTitle(R.string.permission_location_title) .setTitle(R.string.permission_location_title)
.setMessage(R.string.permission_location_request_body) .setMessage(R.string.permission_location_request_body)
.setPositiveButton(R.string.ok, (dialog, which) -> .setPositiveButton(R.string.ok, (dialog, which) ->
permissionRequest.launch(ACCESS_FINE_LOCATION)) permissionRequest.launch(
new String[] {ACCESS_FINE_LOCATION}))
.show(); .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;
}
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.contact.connect; package org.briarproject.briar.android.contact.connect;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; 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.MethodsNotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher; 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.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.briar.android.AppModule.getAndroidComponent; import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@@ -42,8 +43,8 @@ public class BluetoothIntroFragment extends Fragment {
private final ActivityResultLauncher<Integer> bluetoothDiscoverableRequest = private final ActivityResultLauncher<Integer> bluetoothDiscoverableRequest =
registerForActivityResult(new RequestBluetoothDiscoverable(), registerForActivityResult(new RequestBluetoothDiscoverable(),
this::onBluetoothDiscoverable); this::onBluetoothDiscoverable);
private final ActivityResultLauncher<String> permissionRequest = private final ActivityResultLauncher<String[]> permissionRequest =
registerForActivityResult(new RequestPermission(), registerForActivityResult(new RequestMultiplePermissions(),
this::onPermissionRequestResult); this::onPermissionRequestResult);
@Override @Override
@@ -80,12 +81,13 @@ public class BluetoothIntroFragment extends Fragment {
// if the permission is already granted. // if the permission is already granted.
// So we can use the request as a generic entry point // So we can use the request as a generic entry point
// to the whole flow. // to the whole flow.
permissionRequest.launch(ACCESS_FINE_LOCATION); conditionManager.requestPermissions(permissionRequest);
} }
} }
private void onPermissionRequestResult(@Nullable Boolean result) { private void onPermissionRequestResult(
Activity a = requireActivity(); @Nullable Map<String, Boolean> result) {
FragmentActivity a = requireActivity();
// update permission result in BluetoothConnecter // update permission result in BluetoothConnecter
conditionManager.onLocationPermissionResult(a, result); conditionManager.onLocationPermissionResult(a, result);
// what to do when the user denies granting the location permission // what to do when the user denies granting the location permission

View File

@@ -26,18 +26,24 @@ import org.briarproject.nullsafety.ParametersNotNullByDefault;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
import androidx.annotation.ColorRes; import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.widget.SwitchCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider; 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.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; 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_BATTERY;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED; 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.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.showOnboardingDialog;
import static org.briarproject.briar.android.util.UiUtils.showRationale;
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -61,6 +73,11 @@ public class TransportsActivity extends BriarActivity {
private PluginViewModel viewModel; private PluginViewModel viewModel;
private BaseAdapter transportsAdapter; private BaseAdapter transportsAdapter;
@RequiresApi(31)
private final ActivityResultLauncher<String[]> requestPermissionLauncher =
registerForActivityResult(new RequestMultiplePermissions(),
this::handleBtPermissionResult);
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
component.inject(this); component.inject(this);
@@ -149,8 +166,7 @@ public class TransportsActivity extends BriarActivity {
view.findViewById(R.id.switchCompat); view.findViewById(R.id.switchCompat);
switchCompat.setText(getString(t.switchLabel)); switchCompat.setText(getString(t.switchLabel));
switchCompat.setOnClickListener(v -> switchCompat.setOnClickListener(v ->
viewModel.enableTransport(t.id, onClicked(t.id, switchCompat.isChecked()));
switchCompat.isChecked()));
switchCompat.setChecked(t.isSwitchChecked); switchCompat.setChecked(t.isSwitchChecked);
TextView summary = view.findViewById(R.id.summary); 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) { private String getBulletString(@StringRes int resId) {
return "\u2022 " + getString(resId); return "\u2022 " + getString(resId);
} }
@@ -316,6 +347,23 @@ public class TransportsActivity extends BriarActivity {
return transport; 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 static class Transport {
private final TransportId id; private final TransportId id;

View File

@@ -58,6 +58,8 @@ import static java.util.Locale.US;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.TimeZone.getTimeZone; import static java.util.TimeZone.getTimeZone;
import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod; 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.scrubInetAddress;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -273,12 +275,13 @@ class BriarReportCollector {
// Is Bluetooth enabled? // Is Bluetooth enabled?
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
boolean btEnabled = bt.isEnabled() boolean btEnabled = hasBtConnectPermission(ctx) && bt.isEnabled()
&& !isNullOrEmpty(bt.getAddress()); && !isNullOrEmpty(bt.getAddress());
connectivityInfo.add("BluetoothEnabled", btEnabled); connectivityInfo.add("BluetoothEnabled", btEnabled);
// Is Bluetooth connectable? // Is Bluetooth connectable?
int scanMode = bt.getScanMode(); @SuppressLint("MissingPermission")
int scanMode = hasBtScanPermission(ctx) ? bt.getScanMode() : -1;
boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE || boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE ||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE; scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
connectivityInfo.add("BluetoothConnectable", btConnectable); connectivityInfo.add("BluetoothConnectable", btConnectable);
@@ -298,11 +301,14 @@ class BriarReportCollector {
btLeAdvertise); btLeAdvertise);
} }
Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt); if (hasBtConnectPermission(ctx)) {
String address = p.getFirst(); Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt);
String method = p.getSecond(); String address = p.getFirst();
connectivityInfo.add("BluetoothAddress", scrubMacAddress(address)); String method = p.getSecond();
connectivityInfo.add("BluetoothAddressMethod", method); connectivityInfo.add("BluetoothAddress",
scrubMacAddress(address));
connectivityInfo.add("BluetoothAddressMethod", method);
}
} }
return new ReportItem("Connectivity", R.string.dev_report_connectivity, return new ReportItem("Connectivity", R.string.dev_report_connectivity,
connectivityInfo); connectivityInfo);

View File

@@ -8,18 +8,32 @@ import org.briarproject.briar.R;
import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.nullsafety.MethodsNotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat; 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.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist; 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 @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -47,6 +61,11 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
private SwitchPreferenceCompat torMobile; private SwitchPreferenceCompat torMobile;
private SwitchPreferenceCompat torOnlyWhenCharging; private SwitchPreferenceCompat torOnlyWhenCharging;
@RequiresApi(31)
private final ActivityResultLauncher<String[]> requestPermissionLauncher =
registerForActivityResult(new RequestMultiplePermissions(),
this::handleBtPermissionResult);
@Override @Override
public void onAttach(@NonNull Context context) { public void onAttach(@NonNull Context context) {
super.onAttach(context); super.onAttach(context);
@@ -69,6 +88,25 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
torNetwork.setSummaryProvider(viewModel.torSummaryProvider); 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); enableBluetooth.setPreferenceDataStore(connectionsManager.btStore);
enableWifi.setPreferenceDataStore(connectionsManager.wifiStore); enableWifi.setPreferenceDataStore(connectionsManager.wifiStore);
enableTor.setPreferenceDataStore(connectionsManager.torStore); enableTor.setPreferenceDataStore(connectionsManager.torStore);
@@ -115,4 +153,19 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
requireActivity().setTitle(R.string.network_settings_title); 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);
}
}
} }

View File

@@ -44,6 +44,7 @@ import org.briarproject.nullsafety.MethodsNotNullByDefault;
import org.briarproject.nullsafety.ParametersNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
@@ -70,6 +71,9 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; 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.Context.KEYGUARD_SERVICE;
import static android.content.Intent.CATEGORY_DEFAULT; import static android.content.Intent.CATEGORY_DEFAULT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 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.content.ContextCompat.getSystemService;
import static androidx.core.graphics.drawable.DrawableCompat.setTint; import static androidx.core.graphics.drawable.DrawableCompat.setTint;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL; 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.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -346,10 +351,10 @@ public class UiUtils {
/** /**
* @return true if location is enabled, * @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) { public static boolean isLocationEnabled(Context ctx) {
if (SDK_INT >= 28) { if (SDK_INT >= 28 && SDK_INT < 31) {
LocationManager lm = ctx.getSystemService(LocationManager.class); LocationManager lm = ctx.getSystemService(LocationManager.class);
return lm.isLocationEnabled(); return lm.isLocationEnabled();
} else { } else {
@@ -625,4 +630,21 @@ public class UiUtils {
} }
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_LONG).show(); 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));
}
} }

View File

@@ -782,6 +782,10 @@
<string name="permission_location_setting_title">Location setting</string> <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_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_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="qr_code">QR code</string>
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string> <string name="show_qr_code_fullscreen">Show QR code fullscreen</string>