Factor out permission related code from KeyAgreementActivity to AddNearbyContactPermissionManager

This commit is contained in:
Torsten Grote
2021-02-04 14:07:53 -03:00
parent d8327d6de2
commit 700f6e05bf
2 changed files with 199 additions and 167 deletions

View File

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

View File

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