diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java index 1fdbcd550..1cb3f8783 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java @@ -25,7 +25,6 @@ import javax.annotation.Nullable; import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; @@ -50,12 +49,6 @@ public class AddNearbyContactActivity extends BriarActivity ViewModelProvider.Factory viewModelFactory; private AddNearbyContactViewModel viewModel; - private AddNearbyContactPermissionManager permissionManager; - - private final ActivityResultLauncher permissionLauncher = - registerForActivityResult(new RequestMultiplePermissions(), r -> - permissionManager.onRequestPermissionResult(r, - viewModel::showQrCodeFragmentIfAllowed)); private final ActivityResultLauncher bluetoothLauncher = registerForActivityResult(new RequestBluetoothDiscoverable(), this::onBluetoothDiscoverableResult); @@ -65,8 +58,6 @@ public class AddNearbyContactActivity extends BriarActivity component.inject(this); viewModel = new ViewModelProvider(this, viewModelFactory) .get(AddNearbyContactViewModel.class); - permissionManager = new AddNearbyContactPermissionManager(this, - permissionLauncher::launch, viewModel.isBluetoothSupported()); } @Override @@ -79,8 +70,6 @@ public class AddNearbyContactActivity extends BriarActivity if (state == null) { showInitialFragment(AddNearbyContactIntroFragment.newInstance()); } - viewModel.getCheckPermissions().observeEvent(this, check -> - permissionManager.checkPermissions()); viewModel.getRequestBluetoothDiscoverable().observeEvent(this, r -> requestBluetoothDiscoverable()); // never false viewModel.getShowQrCodeFragment().observeEvent(this, show -> { @@ -92,13 +81,6 @@ public class AddNearbyContactActivity extends BriarActivity .observe(this, this::onAddContactStateChanged); } - @Override - public void onStart() { - super.onStart(); - // Permissions may have been granted manually while we were stopped - permissionManager.resetPermissions(); - } - @Override protected void onPostResume() { super.onPostResume(); @@ -143,17 +125,13 @@ public class AddNearbyContactActivity extends BriarActivity } private void requestBluetoothDiscoverable() { - if (!viewModel.isBluetoothSupported()) { - viewModel.setBluetoothDecision(BluetoothDecision.NO_ADAPTER); + Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); + if (i.resolveActivity(getPackageManager()) != null) { + LOG.info("Asking for Bluetooth discoverability"); + viewModel.setBluetoothDecision(BluetoothDecision.WAITING); + bluetoothLauncher.launch(120); // 2min discoverable } else { - Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); - if (i.resolveActivity(getPackageManager()) != null) { - LOG.info("Asking for Bluetooth discoverability"); - viewModel.setBluetoothDecision(BluetoothDecision.WAITING); - bluetoothLauncher.launch(120); // 2min discoverable - } else { - viewModel.setBluetoothDecision(BluetoothDecision.NO_ADAPTER); - } + viewModel.setBluetoothDecision(BluetoothDecision.NO_ADAPTER); } } @@ -169,7 +147,7 @@ public class AddNearbyContactActivity extends BriarActivity } } - private void onAddContactStateChanged(AddContactState state) { + private void onAddContactStateChanged(@Nullable AddContactState state) { if (state instanceof ContactExchangeFinished) { ContactExchangeResult result = ((ContactExchangeFinished) state).result; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java index 1d7e77fcb..b126fb641 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java @@ -100,7 +100,7 @@ public class AddNearbyContactFragment extends BaseFragment public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); - cameraView.setPreviewConsumer(viewModel.qrCodeDecoder); + cameraView.setPreviewConsumer(viewModel.getQrCodeDecoder()); } @Override @@ -153,7 +153,7 @@ public class AddNearbyContactFragment extends BaseFragment } @UiThread - private void onAddContactStateChanged(AddContactState state) { + private void onAddContactStateChanged(@Nullable AddContactState state) { if (state instanceof AddContactState.KeyAgreementListening) { Bitmap qrCode = ((AddContactState.KeyAgreementListening) state).qrCode; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java index 7bc666550..5a9aa3864 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java @@ -15,6 +15,8 @@ import org.briarproject.briar.android.fragment.BaseFragment; import javax.annotation.Nullable; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; import androidx.lifecycle.ViewModelProvider; import static android.view.View.FOCUS_DOWN; @@ -23,15 +25,22 @@ import static android.view.View.FOCUS_DOWN; @ParametersNotNullByDefault public class AddNearbyContactIntroFragment extends BaseFragment { - public static final String TAG = AddNearbyContactIntroFragment.class.getName(); + public static final String TAG = + AddNearbyContactIntroFragment.class.getName(); @Inject ViewModelProvider.Factory viewModelFactory; private AddNearbyContactViewModel viewModel; + private AddNearbyContactPermissionManager permissionManager; private ScrollView scrollView; + private final ActivityResultLauncher permissionLauncher = + registerForActivityResult(new RequestMultiplePermissions(), r -> + permissionManager.onRequestPermissionResult(r, + viewModel::showQrCodeFragmentIfAllowed)); + public static AddNearbyContactIntroFragment newInstance() { Bundle args = new Bundle(); AddNearbyContactIntroFragment @@ -45,6 +54,9 @@ public class AddNearbyContactIntroFragment extends BaseFragment { component.inject(this); viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) .get(AddNearbyContactViewModel.class); + permissionManager = new AddNearbyContactPermissionManager( + requireActivity(), permissionLauncher::launch, + viewModel.isBluetoothSupported()); } @Nullable @@ -57,13 +69,17 @@ public class AddNearbyContactIntroFragment extends BaseFragment { false); scrollView = v.findViewById(R.id.scrollView); View button = v.findViewById(R.id.continueButton); - button.setOnClickListener(view -> viewModel.onContinueClicked()); + button.setOnClickListener(view -> viewModel.onContinueClicked(() -> + permissionManager.checkPermissions() + )); return v; } @Override public void onStart() { super.onStart(); + // Permissions may have been granted manually while we were stopped + permissionManager.resetPermissions(); scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); } 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 index 5fde4c63e..2d122ea3b 100644 --- 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 @@ -1,20 +1,26 @@ package org.briarproject.briar.android.contact.add.nearby; +import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; +import android.widget.Toast; import org.briarproject.briar.R; -import org.briarproject.briar.android.activity.BaseActivity; import java.util.Map; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.core.util.Consumer; +import androidx.fragment.app.FragmentActivity; 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 android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS; +import static android.widget.Toast.LENGTH_LONG; import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; import static androidx.core.content.ContextCompat.checkSelfPermission; import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; @@ -28,11 +34,11 @@ public class AddNearbyContactPermissionManager { private Permission cameraPermission = Permission.UNKNOWN; private Permission locationPermission = Permission.UNKNOWN; - private final BaseActivity ctx; + private final FragmentActivity ctx; private final Consumer requestPermissions; private final boolean isBluetoothSupported; - public AddNearbyContactPermissionManager(BaseActivity ctx, + public AddNearbyContactPermissionManager(FragmentActivity ctx, Consumer requestPermissions, boolean isBluetoothSupported) { this.ctx = ctx; @@ -45,6 +51,19 @@ public class AddNearbyContactPermissionManager { locationPermission = Permission.UNKNOWN; } + /** + * @return true if location is enabled, + * or it isn't required due to this being a SDK < 28 device. + */ + static boolean isLocationEnabled(Context ctx) { + if (SDK_INT >= 28) { + LocationManager lm = ctx.getSystemService(LocationManager.class); + return lm.isLocationEnabled(); + } else { + return true; + } + } + public static boolean areEssentialPermissionsGranted(Context ctx, boolean isBluetoothSupported) { int ok = PERMISSION_GRANTED; @@ -54,14 +73,15 @@ public class AddNearbyContactPermissionManager { !isBluetoothSupported); } - boolean areEssentialPermissionsGranted() { + private boolean areEssentialPermissionsGranted() { return cameraPermission == Permission.GRANTED && (SDK_INT < 23 || locationPermission == Permission.GRANTED || !isBluetoothSupported); } - public boolean checkPermissions() { - if (areEssentialPermissionsGranted()) return true; + public boolean checkPermissions() { + boolean locationEnabled = isLocationEnabled(ctx); + 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) { @@ -86,8 +106,10 @@ public class AddNearbyContactPermissionManager { } else if (locationPermission == Permission.SHOW_RATIONALE) { showRationale(R.string.permission_location_title, R.string.permission_location_request_body); - } else { + } else if (isLocationEnabled(ctx)) { requestPermissions(); + } else { + showLocationDialog(ctx); } return false; } @@ -113,6 +135,25 @@ public class AddNearbyContactPermissionManager { builder.show(); } + private static void showLocationDialog(Context ctx) { + AlertDialog.Builder builder = + new AlertDialog.Builder(ctx, R.style.BriarDialogTheme); + builder.setTitle(R.string.permission_location_setting_title); + builder.setMessage(R.string.permission_location_setting_body); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.permission_location_setting_button, + (dialog, which) -> { + Intent i = new Intent(ACTION_LOCATION_SOURCE_SETTINGS); + try { + ctx.startActivity(i); + } catch (ActivityNotFoundException e) { + Toast.makeText(ctx, R.string.error_start_activity, + LENGTH_LONG).show(); + } + }); + builder.show(); + } + private void requestPermissions() { String[] permissions; if (isBluetoothSupported) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java index 40650d189..eca13d40d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java @@ -65,6 +65,7 @@ import javax.inject.Provider; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.core.util.Supplier; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -83,6 +84,8 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE; import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactPermissionManager.areEssentialPermissionsGranted; +import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactPermissionManager.isLocationEnabled; +import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.NO_ADAPTER; import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.REFUSED; import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.UNKNOWN; @@ -133,8 +136,6 @@ class AddNearbyContactViewModel extends AndroidViewModel private final ContactExchangeManager contactExchangeManager; private final ConnectionManager connectionManager; - private final MutableLiveEvent checkPermissions = - new MutableLiveEvent<>(); private final MutableLiveEvent requestBluetoothDiscoverable = new MutableLiveEvent<>(); private final MutableLiveEvent showQrCodeFragment = @@ -142,8 +143,9 @@ class AddNearbyContactViewModel extends AndroidViewModel private final MutableLiveData state = new MutableLiveData<>(); - final QrCodeDecoder qrCodeDecoder; - final BroadcastReceiver bluetoothReceiver = new BluetoothStateReceiver(); + private final QrCodeDecoder qrCodeDecoder; + private final BroadcastReceiver bluetoothReceiver = + new BluetoothStateReceiver(); @Nullable private final BluetoothAdapter bt; @@ -169,7 +171,7 @@ class AddNearbyContactViewModel extends AndroidViewModel private boolean hasEnabledBluetooth = false; @Nullable - private KeyAgreementTask task; + private volatile KeyAgreementTask task; private volatile boolean gotLocalPayload = false, gotRemotePayload = false; @Inject @@ -211,13 +213,12 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread - void onContinueClicked() { + void onContinueClicked(Supplier checkPermissions) { if (bluetoothDecision == REFUSED) { bluetoothDecision = UNKNOWN; // Ask again } wasContinueClicked = true; - checkPermissions.setEvent(true); - showQrCodeFragmentIfAllowed(); + if (checkPermissions.get()) showQrCodeFragmentIfAllowed(); } @UiThread @@ -226,7 +227,7 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread - boolean isWifiReady() { + private boolean isWifiReady() { if (wifiPlugin == null) return true; // Continue without wifi State state = wifiPlugin.getState(); // Wait for plugin to become enabled @@ -234,7 +235,7 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread - boolean isBluetoothReady() { + private boolean isBluetoothReady() { if (bt == null || bluetoothPlugin == null) { // Continue without Bluetooth return true; @@ -254,7 +255,7 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread - void enableWifiIfWeShould() { + private void enableWifiIfWeShould() { if (hasEnabledWifi) return; if (wifiPlugin == null) return; State state = wifiPlugin.getState(); @@ -266,7 +267,7 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread - void enableBluetoothIfWeShould() { + private void enableBluetoothIfWeShould() { if (bluetoothDecision != BluetoothDecision.ACCEPTED) return; if (hasEnabledBluetooth) return; if (bluetoothPlugin == null || !isBluetoothSupported()) return; @@ -279,7 +280,7 @@ class AddNearbyContactViewModel extends AndroidViewModel } @UiThread - void startAddingContact() { + private void startAddingContact() { // If we return to the intro fragment, the continue button needs to be // clicked again before showing the QR code fragment wasContinueClicked = false; @@ -290,6 +291,8 @@ class AddNearbyContactViewModel extends AndroidViewModel // Bluetooth again hasEnabledWifi = false; hasEnabledBluetooth = false; + // reset state, so we don't show an old QR code again + state.setValue(null); // start to listen with a KeyAgreementTask startListening(); showQrCodeFragment.setEvent(true); @@ -365,14 +368,20 @@ class AddNearbyContactViewModel extends AndroidViewModel void showQrCodeFragmentIfAllowed() { boolean permissionsGranted = areEssentialPermissionsGranted( getApplication(), isBluetoothSupported()); - if (isActivityResumed && wasContinueClicked && permissionsGranted) { + boolean locationEnabled = isLocationEnabled(getApplication()); + if (isActivityResumed && wasContinueClicked && permissionsGranted && + locationEnabled) { if (isWifiReady() && isBluetoothReady()) { LOG.info("Wifi and Bluetooth are ready"); startAddingContact(); } else { enableWifiIfWeShould(); if (bluetoothDecision == UNKNOWN) { - requestBluetoothDiscoverable.setEvent(true); + if (isBluetoothSupported()) { + requestBluetoothDiscoverable.setEvent(true); + } else { + bluetoothDecision = NO_ADAPTER; + } } else if (bluetoothDecision == REFUSED) { // Ask again when the user clicks "continue" } else { @@ -501,8 +510,8 @@ class AddNearbyContactViewModel extends AndroidViewModel showQrCodeFragmentIfAllowed(); } - LiveEvent getCheckPermissions() { - return checkPermissions; + QrCodeDecoder getQrCodeDecoder() { + return qrCodeDecoder; } LiveEvent getRequestBluetoothDiscoverable() { @@ -513,6 +522,9 @@ class AddNearbyContactViewModel extends AndroidViewModel return showQrCodeFragment; } + /** + * This LiveData will be null initially. + */ LiveData getState() { return state; } diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index bd4caa517..6009c363b 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -609,6 +609,9 @@ To scan the QR code, Briar needs access to the camera.\n\nTo discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone. You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access. You have denied access to your location, but Briar needs this permission to discover Bluetooth devices.\n\nPlease consider granting access. + Location setting + 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. + Enable location QR code Show QR code fullscreen