diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java new file mode 100644 index 000000000..1244da3ae --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java @@ -0,0 +1,154 @@ +package org.briarproject.briar.android.contact.add.nearby; + +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.CAMERA; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION.SDK_INT; +import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; +import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; + +class AddNearbyContactPermissionManager { + + private enum Permission { + UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED + } + + private Permission cameraPermission = Permission.UNKNOWN; + private Permission locationPermission = Permission.UNKNOWN; + + private final BaseActivity ctx; + private final boolean isBluetoothSupported; + + AddNearbyContactPermissionManager(BaseActivity ctx, + boolean isBluetoothSupported) { + this.ctx = ctx; + this.isBluetoothSupported = isBluetoothSupported; + } + + void resetPermissions() { + cameraPermission = Permission.UNKNOWN; + locationPermission = Permission.UNKNOWN; + } + + boolean areEssentialPermissionsGranted() { + return cameraPermission == Permission.GRANTED && + (SDK_INT < 23 || locationPermission == Permission.GRANTED || + !isBluetoothSupported); + } + + boolean checkPermissions() { + if (areEssentialPermissionsGranted()) return true; + // If an essential permission has been permanently denied, ask the + // user to change the setting + if (cameraPermission == Permission.PERMANENTLY_DENIED) { + showDenialDialog(R.string.permission_camera_title, + R.string.permission_camera_denied_body); + return false; + } + if (isBluetoothSupported && + locationPermission == Permission.PERMANENTLY_DENIED) { + showDenialDialog(R.string.permission_location_title, + R.string.permission_location_denied_body); + return false; + } + // Should we show the rationale for one or both permissions? + if (cameraPermission == Permission.SHOW_RATIONALE && + locationPermission == Permission.SHOW_RATIONALE) { + showRationale(R.string.permission_camera_location_title, + R.string.permission_camera_location_request_body); + } else if (cameraPermission == Permission.SHOW_RATIONALE) { + showRationale(R.string.permission_camera_title, + R.string.permission_camera_request_body); + } else if (locationPermission == Permission.SHOW_RATIONALE) { + showRationale(R.string.permission_location_title, + R.string.permission_location_request_body); + } else { + requestPermissions(); + } + return false; + } + + private void showDenialDialog(@StringRes int title, @StringRes int body) { + AlertDialog.Builder builder = + new AlertDialog.Builder(ctx, R.style.BriarDialogTheme); + builder.setTitle(title); + builder.setMessage(body); + builder.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx)); + builder.setNegativeButton(R.string.cancel, + (dialog, which) -> ctx.supportFinishAfterTransition()); + builder.show(); + } + + private void showRationale(@StringRes int title, @StringRes int body) { + AlertDialog.Builder builder = + new AlertDialog.Builder(ctx, R.style.BriarDialogTheme); + builder.setTitle(title); + builder.setMessage(body); + builder.setNeutralButton(R.string.continue_button, + (dialog, which) -> requestPermissions()); + builder.show(); + } + + private void requestPermissions() { + String[] permissions; + if (isBluetoothSupported) { + permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION}; + } else { + permissions = new String[] {CAMERA}; + } + ActivityCompat.requestPermissions(ctx, permissions, + REQUEST_PERMISSION_CAMERA_LOCATION); + } + + void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults, Runnable onPermissionsGranted) { + if (requestCode != REQUEST_PERMISSION_CAMERA_LOCATION) + throw new AssertionError(); + if (gotPermission(CAMERA, permissions, grantResults)) { + cameraPermission = Permission.GRANTED; + } else if (shouldShowRationale(CAMERA)) { + cameraPermission = Permission.SHOW_RATIONALE; + } else { + cameraPermission = Permission.PERMANENTLY_DENIED; + } + if (isBluetoothSupported) { + if (gotPermission(ACCESS_FINE_LOCATION, permissions, + grantResults)) { + locationPermission = Permission.GRANTED; + } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { + locationPermission = Permission.SHOW_RATIONALE; + } else { + locationPermission = Permission.PERMANENTLY_DENIED; + } + } + // If a permission dialog has been shown, showing the QR code fragment + // on this call path would cause a crash due to + // https://code.google.com/p/android/issues/detail?id=190966. + // In that case the isResumed flag prevents the fragment from being + // shown here, and showQrCodeFragmentIfAllowed() will be called again + // from onPostResume(). + if (checkPermissions()) onPermissionsGranted.run(); + } + + private boolean gotPermission(String permission, String[] permissions, + int[] grantResults) { + for (int i = 0; i < permissions.length; i++) { + if (permission.equals(permissions[i])) + return grantResults[i] == PERMISSION_GRANTED; + } + return false; + } + + private boolean shouldShowRationale(String permission) { + return ActivityCompat + .shouldShowRequestPermissionRationale(ctx, permission); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java index 6eb87f38b..6c8b76a41 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java @@ -21,38 +21,25 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; -import androidx.annotation.StringRes; import androidx.annotation.UiThread; -import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.widget.Toolbar; -import androidx.core.app.ActivityCompat; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; -import static android.Manifest.permission.CAMERA; import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.os.Build.VERSION.SDK_INT; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.ACCEPTED; import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.REFUSED; import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.UNKNOWN; -import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; @MethodsNotNullByDefault @ParametersNotNullByDefault public abstract class KeyAgreementActivity extends BriarActivity implements BaseFragmentListener { - private enum Permission { - UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED - } - private static final Logger LOG = getLogger(KeyAgreementActivity.class.getName()); @@ -60,6 +47,7 @@ public abstract class KeyAgreementActivity extends BriarActivity ViewModelProvider.Factory viewModelFactory; protected ContactExchangeViewModel viewModel; + private AddNearbyContactPermissionManager permissionManager; /** * Set to true in onPostResume() and false in onPause(). This prevents the @@ -68,9 +56,6 @@ public abstract class KeyAgreementActivity extends BriarActivity * https://issuetracker.google.com/issues/37067655. */ private boolean isResumed = false; - - private Permission cameraPermission = Permission.UNKNOWN; - private Permission locationPermission = Permission.UNKNOWN; private BroadcastReceiver bluetoothReceiver = null; @Override @@ -78,6 +63,8 @@ public abstract class KeyAgreementActivity extends BriarActivity component.inject(this); viewModel = new ViewModelProvider(this, viewModelFactory) .get(ContactExchangeViewModel.class); + permissionManager = new AddNearbyContactPermissionManager(this, + viewModel.isBluetoothSupported()); } @Override @@ -94,7 +81,9 @@ public abstract class KeyAgreementActivity extends BriarActivity bluetoothReceiver = new BluetoothStateReceiver(); registerReceiver(bluetoothReceiver, filter); viewModel.getWasContinueClicked().observe(this, clicked -> { - if (clicked && checkPermissions()) showQrCodeFragmentIfAllowed(); + if (clicked && permissionManager.checkPermissions()) { + showQrCodeFragmentIfAllowed(); + } }); viewModel.getTransportStateChanged().observeEvent(this, t -> showQrCodeFragmentIfAllowed()); @@ -107,8 +96,7 @@ public abstract class KeyAgreementActivity extends BriarActivity public void onStart() { super.onStart(); // Permissions may have been granted manually while we were stopped - cameraPermission = Permission.UNKNOWN; - locationPermission = Permission.UNKNOWN; + permissionManager.resetPermissions(); } @Override @@ -126,11 +114,6 @@ public abstract class KeyAgreementActivity extends BriarActivity isResumed = false; } - @Override - protected void onStop() { - super.onStop(); - } - @Override public void onDestroy() { super.onDestroy(); @@ -146,31 +129,29 @@ public abstract class KeyAgreementActivity extends BriarActivity return super.onOptionsItemSelected(item); } - @SuppressWarnings("StatementWithEmptyBody") - private void showQrCodeFragmentIfAllowed() { - boolean continueClicked = // never set to null - requireNonNull(viewModel.getWasContinueClicked().getValue()); - if (isResumed && continueClicked && areEssentialPermissionsGranted()) { - if (viewModel.isWifiReady() && viewModel.isBluetoothReady()) { - LOG.info("Wifi and Bluetooth are ready"); - viewModel.startAddingContact(); + @Override + public void onActivityResult(int request, int result, + @Nullable Intent data) { + if (request == REQUEST_BLUETOOTH_DISCOVERABLE) { + if (result == RESULT_CANCELED) { + LOG.info("Bluetooth discoverability was refused"); + viewModel.bluetoothDecision = REFUSED; } else { - viewModel.enableWifiIfWeShould(); - if (viewModel.bluetoothDecision == UNKNOWN) { - requestBluetoothDiscoverable(); - } else if (viewModel.bluetoothDecision == REFUSED) { - // Ask again when the user clicks "continue" - } else { - viewModel.enableBluetoothIfWeShould(); - } + LOG.info("Bluetooth discoverability was accepted"); + viewModel.bluetoothDecision = ACCEPTED; } - } + showQrCodeFragmentIfAllowed(); + } else super.onActivityResult(request, result, data); } - private boolean areEssentialPermissionsGranted() { - return cameraPermission == Permission.GRANTED && - (SDK_INT < 23 || locationPermission == Permission.GRANTED || - !viewModel.isBluetoothSupported()); + @Override + @UiThread + public void onRequestPermissionsResult(int requestCode, + String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + permissionManager.onRequestPermissionsResult(requestCode, permissions, + grantResults, this::showQrCodeFragmentIfAllowed); } private void requestBluetoothDiscoverable() { @@ -190,19 +171,27 @@ public abstract class KeyAgreementActivity extends BriarActivity } } - @Override - public void onActivityResult(int request, int result, - @Nullable Intent data) { - if (request == REQUEST_BLUETOOTH_DISCOVERABLE) { - if (result == RESULT_CANCELED) { - LOG.info("Bluetooth discoverability was refused"); - viewModel.bluetoothDecision = REFUSED; + @SuppressWarnings("StatementWithEmptyBody") + private void showQrCodeFragmentIfAllowed() { + boolean continueClicked = // never set to null + requireNonNull(viewModel.getWasContinueClicked().getValue()); + boolean permissionsGranted = + permissionManager.areEssentialPermissionsGranted(); + if (isResumed && continueClicked && permissionsGranted) { + if (viewModel.isWifiReady() && viewModel.isBluetoothReady()) { + LOG.info("Wifi and Bluetooth are ready"); + viewModel.startAddingContact(); } else { - LOG.info("Bluetooth discoverability was accepted"); - viewModel.bluetoothDecision = ACCEPTED; + viewModel.enableWifiIfWeShould(); + if (viewModel.bluetoothDecision == UNKNOWN) { + requestBluetoothDiscoverable(); + } else if (viewModel.bluetoothDecision == REFUSED) { + // Ask again when the user clicks "continue" + } else { + viewModel.enableBluetoothIfWeShould(); + } } - showQrCodeFragmentIfAllowed(); - } else super.onActivityResult(request, result, data); + } } private void showQrCodeFragment() { @@ -217,118 +206,7 @@ public abstract class KeyAgreementActivity extends BriarActivity } } - private boolean checkPermissions() { - if (areEssentialPermissionsGranted()) return true; - // If an essential permission has been permanently denied, ask the - // user to change the setting - if (cameraPermission == Permission.PERMANENTLY_DENIED) { - showDenialDialog(R.string.permission_camera_title, - R.string.permission_camera_denied_body); - return false; - } - if (viewModel.isBluetoothSupported() && - locationPermission == Permission.PERMANENTLY_DENIED) { - showDenialDialog(R.string.permission_location_title, - R.string.permission_location_denied_body); - return false; - } - // Should we show the rationale for one or both permissions? - if (cameraPermission == Permission.SHOW_RATIONALE && - locationPermission == Permission.SHOW_RATIONALE) { - showRationale(R.string.permission_camera_location_title, - R.string.permission_camera_location_request_body); - } else if (cameraPermission == Permission.SHOW_RATIONALE) { - showRationale(R.string.permission_camera_title, - R.string.permission_camera_request_body); - } else if (locationPermission == Permission.SHOW_RATIONALE) { - showRationale(R.string.permission_location_title, - R.string.permission_location_request_body); - } else { - requestPermissions(); - } - return false; - } - - private void showDenialDialog(@StringRes int title, @StringRes int body) { - Builder builder = new Builder(this, R.style.BriarDialogTheme); - builder.setTitle(title); - builder.setMessage(body); - builder.setPositiveButton(R.string.ok, getGoToSettingsListener(this)); - builder.setNegativeButton(R.string.cancel, - (dialog, which) -> supportFinishAfterTransition()); - builder.show(); - } - - private void showRationale(@StringRes int title, @StringRes int body) { - Builder builder = new Builder(this, R.style.BriarDialogTheme); - builder.setTitle(title); - builder.setMessage(body); - builder.setNeutralButton(R.string.continue_button, - (dialog, which) -> requestPermissions()); - builder.show(); - } - - private void requestPermissions() { - String[] permissions; - if (viewModel.isBluetoothSupported()) { - permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION}; - } else { - permissions = new String[] {CAMERA}; - } - ActivityCompat.requestPermissions(this, permissions, - REQUEST_PERMISSION_CAMERA_LOCATION); - } - - @Override - @UiThread - public void onRequestPermissionsResult(int requestCode, - String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, - grantResults); - if (requestCode != REQUEST_PERMISSION_CAMERA_LOCATION) - throw new AssertionError(); - if (gotPermission(CAMERA, permissions, grantResults)) { - cameraPermission = Permission.GRANTED; - } else if (shouldShowRationale(CAMERA)) { - cameraPermission = Permission.SHOW_RATIONALE; - } else { - cameraPermission = Permission.PERMANENTLY_DENIED; - } - if (viewModel.isBluetoothSupported()) { - if (gotPermission(ACCESS_FINE_LOCATION, permissions, - grantResults)) { - locationPermission = Permission.GRANTED; - } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { - locationPermission = Permission.SHOW_RATIONALE; - } else { - locationPermission = Permission.PERMANENTLY_DENIED; - } - } - // If a permission dialog has been shown, showing the QR code fragment - // on this call path would cause a crash due to - // https://code.google.com/p/android/issues/detail?id=190966. - // In that case the isResumed flag prevents the fragment from being - // shown here, and showQrCodeFragmentIfAllowed() will be called again - // from onPostResume(). - if (checkPermissions()) showQrCodeFragmentIfAllowed(); - } - - private boolean gotPermission(String permission, String[] permissions, - int[] grantResults) { - for (int i = 0; i < permissions.length; i++) { - if (permission.equals(permissions[i])) - return grantResults[i] == PERMISSION_GRANTED; - } - return false; - } - - private boolean shouldShowRationale(String permission) { - return ActivityCompat.shouldShowRequestPermissionRationale(this, - permission); - } - private class BluetoothStateReceiver extends BroadcastReceiver { - @Override public void onReceive(Context context, Intent intent) { LOG.info("Bluetooth scan mode changed");