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
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"

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.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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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" />

View File

@@ -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;
}
}
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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>