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

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