From a9c4669c75465ca926ab12064a098045413fdd18 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 9 Mar 2021 14:53:26 -0300 Subject: [PATCH 1/6] Simple version of Connect via Bluetooth UI --- .../conversation/BluetoothConnecter.java | 120 ++++++++++++++++++ .../conversation/ConversationActivity.java | 81 +++++++----- .../conversation/ConversationViewModel.java | 9 +- .../RequestBluetoothDiscoverable.java | 30 +++++ .../main/res/menu/conversation_actions.xml | 6 + briar-android/src/main/res/values/strings.xml | 8 ++ 6 files changed, 222 insertions(+), 32 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java new file mode 100644 index 000000000..8c749319d --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -0,0 +1,120 @@ +package org.briarproject.briar.android.conversation; + +import android.app.Application; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.widget.Toast; + +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.plugin.BluetoothConstants; +import org.briarproject.bramble.api.plugin.Plugin; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.R; +import org.briarproject.briar.android.contact.ContactItem; + +import java.util.Random; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; +import androidx.appcompat.app.AlertDialog; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +class BluetoothConnecter { + + private final Logger LOG = getLogger(BluetoothConnecter.class.getName()); + + private final Application app; + private final Executor ioExecutor; + private final AndroidExecutor androidExecutor; + + private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + private final Plugin bluetoothPlugin; + + @Inject + BluetoothConnecter(Application app, + PluginManager pluginManager, + @IoExecutor Executor ioExecutor, + AndroidExecutor androidExecutor) { + this.app = app; + this.ioExecutor = ioExecutor; + this.androidExecutor = androidExecutor; + this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + } + + static void showDialog(Context ctx, + ActivityResultLauncher permissionRequest) { + new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) + .setTitle(R.string.dialog_title_connect_via_bluetooth) + .setMessage(R.string.dialog_message_connect_via_bluetooth) + .setPositiveButton(R.string.start, (dialog, which) -> + permissionRequest.launch(ACCESS_FINE_LOCATION)) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + @UiThread + void onLocationPermissionResult(boolean result, + ActivityResultLauncher bluetoothDiscoverableRequest) { + if (result) { + if (isBluetoothSupported()) { + bluetoothDiscoverableRequest.launch(120); + } else { + showToast(R.string.toast_connect_via_bluetooth_error); + } + } else { + // could also show the same dialog as KeyAgreementActivity + showToast(R.string.permission_location_denied_body); + } + } + + private boolean isBluetoothSupported() { + return bt != null && bluetoothPlugin != null; + } + + @UiThread + void onBluetoothDiscoverable(boolean result, ContactItem contact) { + if (result) { + connect(contact); + } else { + showToast(R.string.toast_connect_via_bluetooth_not_discoverable); + } + } + + private void connect(ContactItem contact) { + // TODO + // * enable bluetooth connections setting, if not enabled + // * wait for plugin to become active + ioExecutor.execute(() -> { + Random r = new Random(); + try { + showToast(R.string.toast_connect_via_bluetooth_start); + // TODO do real work here + Thread.sleep(r.nextInt(3000) + 3000); + if (r.nextBoolean()) { + showToast(R.string.toast_connect_via_bluetooth_success); + } else { + showToast(R.string.toast_connect_via_bluetooth_error); + } + } catch (InterruptedException e) { + logException(LOG, WARNING, e); + } + }); + } + + private void showToast(@StringRes int res) { + androidExecutor.runOnUiThread(() -> + Toast.makeText(app, res, Toast.LENGTH_LONG).show() + ); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 17da35e55..38e7e659c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -47,6 +47,7 @@ import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentRetriever; import org.briarproject.briar.android.blog.BlogActivity; +import org.briarproject.briar.android.contact.ContactItem; import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; import org.briarproject.briar.android.forum.ForumActivity; @@ -91,6 +92,8 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; @@ -211,6 +214,21 @@ public class ConversationActivity extends BriarActivity private volatile ContactId contactId; + private final ActivityResultLauncher bluetoothDiscoverableRequest = + registerForActivityResult(new RequestBluetoothDiscoverable(), r -> { + // MenuItem only gets enabled after contactItem has loaded + ContactItem contact = + requireNonNull(viewModel.getContactItem().getValue()); + BluetoothConnecter bc = viewModel.getBluetoothConnecter(); + bc.onBluetoothDiscoverable(r, contact); + }); + private final ActivityResultLauncher permissionRequest = + registerForActivityResult(new RequestPermission(), result -> { + BluetoothConnecter bc = viewModel.getBluetoothConnecter(); + bc.onLocationPermissionResult(result, + bluetoothDiscoverableRequest); + }); + @Override public void onCreate(@Nullable Bundle state) { if (SDK_INT >= 21) { @@ -370,9 +388,11 @@ public class ConversationActivity extends BriarActivity this::showIntroductionOnboarding); } }); - // enable alias action if available - observeOnce(viewModel.getContactItem(), this, contact -> - menu.findItem(R.id.action_set_alias).setEnabled(true)); + // enable alias and bluetooth action once available + observeOnce(viewModel.getContactItem(), this, contact -> { + menu.findItem(R.id.action_set_alias).setEnabled(true); + menu.findItem(R.id.action_connect_via_bluetooth).setEnabled(true); + }); // Show auto-delete menu item if feature is enabled if (featureFlags.shouldEnableDisappearingMessages()) { MenuItem item = menu.findItem(R.id.action_conversation_settings); @@ -381,40 +401,39 @@ public class ConversationActivity extends BriarActivity viewModel.getPrivateMessageFormat().observe(this, format -> item.setEnabled(format == TEXT_IMAGES_AUTO_DELETE)); } - return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { - // Handle presses on the action bar items - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - case R.id.action_introduction: - if (contactId == null) return false; - Intent intent = new Intent(this, IntroductionActivity.class); - intent.putExtra(CONTACT_ID, contactId.getInt()); - startActivityForResult(intent, REQUEST_INTRODUCTION); - return true; - case R.id.action_set_alias: - AliasDialogFragment.newInstance().show( - getSupportFragmentManager(), AliasDialogFragment.TAG); - return true; - case R.id.action_conversation_settings: - if (contactId == null) return false; - onAutoDeleteTimerNoticeClicked(); - return true; - case R.id.action_delete_all_messages: - askToDeleteAllMessages(); - return true; - case R.id.action_social_remove_person: - askToRemoveContact(); - return true; - default: - return super.onOptionsItemSelected(item); + // contactId gets set before in onCreate() + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + onBackPressed(); + return true; + } else if (itemId == R.id.action_introduction) { + Intent intent = new Intent(this, IntroductionActivity.class); + intent.putExtra(CONTACT_ID, contactId.getInt()); + startActivityForResult(intent, REQUEST_INTRODUCTION); + return true; + } else if (itemId == R.id.action_set_alias) { + AliasDialogFragment.newInstance().show( + getSupportFragmentManager(), AliasDialogFragment.TAG); + return true; + } else if (itemId == R.id.action_conversation_settings) { + onAutoDeleteTimerNoticeClicked(); + return true; + } else if (itemId == R.id.action_connect_via_bluetooth) { + BluetoothConnecter.showDialog(this, permissionRequest); + return true; + } else if (itemId == R.id.action_delete_all_messages) { + askToDeleteAllMessages(); + return true; + } else if (itemId == R.id.action_social_remove_person) { + askToRemoveContact(); + return true; } + return super.onOptionsItemSelected(item); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index dcc42e49c..557602f31 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -101,6 +101,7 @@ public class ConversationViewModel extends DbViewModel private final AttachmentCreator attachmentCreator; private final AutoDeleteManager autoDeleteManager; private final ConversationManager conversationManager; + private final BluetoothConnecter bluetoothConnecter; @Nullable private ContactId contactId = null; @@ -139,7 +140,8 @@ public class ConversationViewModel extends DbViewModel AttachmentRetriever attachmentRetriever, AttachmentCreator attachmentCreator, AutoDeleteManager autoDeleteManager, - ConversationManager conversationManager) { + ConversationManager conversationManager, + BluetoothConnecter bluetoothConnecter) { super(application, dbExecutor, lifecycleManager, db, androidExecutor); this.db = db; this.eventBus = eventBus; @@ -152,6 +154,7 @@ public class ConversationViewModel extends DbViewModel this.attachmentCreator = attachmentCreator; this.autoDeleteManager = autoDeleteManager; this.conversationManager = conversationManager; + this.bluetoothConnecter = bluetoothConnecter; messagingGroupId = map(contactItem, c -> messagingManager.getContactGroup(c.getContact()).getId()); eventBus.addListener(this); @@ -411,6 +414,10 @@ public class ConversationViewModel extends DbViewModel return attachmentRetriever; } + BluetoothConnecter getBluetoothConnecter() { + return bluetoothConnecter; + } + LiveData getContactItem() { return contactItem; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java new file mode 100644 index 000000000..95023f6bf --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java @@ -0,0 +1,30 @@ +package org.briarproject.briar.android.conversation; + +import android.content.Context; +import android.content.Intent; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import androidx.activity.result.contract.ActivityResultContract; +import androidx.annotation.Nullable; + +import static android.app.Activity.RESULT_CANCELED; +import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; +import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION; + +@NotNullByDefault +class RequestBluetoothDiscoverable + extends ActivityResultContract { + + @Override + public Intent createIntent(Context context, Integer duration) { + Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); + i.putExtra(EXTRA_DISCOVERABLE_DURATION, duration); + return i; + } + + @Override + public Boolean parseResult(int resultCode, @Nullable Intent intent) { + return resultCode != RESULT_CANCELED; + } +} diff --git a/briar-android/src/main/res/menu/conversation_actions.xml b/briar-android/src/main/res/menu/conversation_actions.xml index f3d057b08..dad562bab 100644 --- a/briar-android/src/main/res/menu/conversation_actions.xml +++ b/briar-android/src/main/res/menu/conversation_actions.xml @@ -23,6 +23,12 @@ app:showAsAction="never" tools:visible="true" /> + + Allow Open Change + Start No data The entered text is too long @@ -168,6 +169,13 @@ Change contact name Contact name Disappearing messages + Connect via Bluetooth + Connect via Bluetooth + Your contact needs to be in close proximity for this to work. They need to press start at the same time as you and confirm the following system dialog. + Can not continue without Bluetooth + Connecting via Bluetooth… + Successfully connected via Bluetooth + Could not connect via Bluetooth Your messages will disappear after %1$s. %2$s From 539730f8ecbfa36ff84a306a25fdd39b0f433152 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 9 Mar 2021 15:21:57 -0300 Subject: [PATCH 2/6] Show dialog when permission was denied for good --- .../conversation/BluetoothConnecter.java | 20 ++++++++++++++++--- .../conversation/ConversationActivity.java | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java index 8c749319d..54e28bd47 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.conversation; +import android.app.Activity; import android.app.Application; import android.bluetooth.BluetoothAdapter; import android.content.Context; @@ -25,9 +26,11 @@ import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; class BluetoothConnecter { @@ -63,7 +66,7 @@ class BluetoothConnecter { } @UiThread - void onLocationPermissionResult(boolean result, + void onLocationPermissionResult(Activity activity, boolean result, ActivityResultLauncher bluetoothDiscoverableRequest) { if (result) { if (isBluetoothSupported()) { @@ -71,9 +74,11 @@ class BluetoothConnecter { } else { showToast(R.string.toast_connect_via_bluetooth_error); } - } else { - // could also show the same dialog as KeyAgreementActivity + } else if (shouldShowRequestPermissionRationale(activity, + ACCESS_FINE_LOCATION)) { showToast(R.string.permission_location_denied_body); + } else { + showRationale(activity); } } @@ -81,6 +86,15 @@ class BluetoothConnecter { return bt != null && bluetoothPlugin != null; } + private void showRationale(Context ctx) { + new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) + .setTitle(R.string.permission_location_title) + .setMessage(R.string.permission_location_request_body) + .setPositiveButton(R.string.ok, getGoToSettingsListener(ctx)) + .setNegativeButton(R.string.cancel, null) + .show(); + } + @UiThread void onBluetoothDiscoverable(boolean result, ContactItem contact) { if (result) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 38e7e659c..3bab9a903 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -225,7 +225,7 @@ public class ConversationActivity extends BriarActivity private final ActivityResultLauncher permissionRequest = registerForActivityResult(new RequestPermission(), result -> { BluetoothConnecter bc = viewModel.getBluetoothConnecter(); - bc.onLocationPermissionResult(result, + bc.onLocationPermissionResult(this, result, bluetoothDiscoverableRequest); }); From c736bf7c06fd8c4b682ef9640c4f570cc103d00d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 13 Apr 2021 11:24:34 -0300 Subject: [PATCH 3/6] Move Connect via Bluetooth UI into DialogFragment so it can stay active when leaving the context to enable location or permissions --- .../android/activity/ActivityComponent.java | 3 + .../AddNearbyContactPermissionManager.java | 42 +---- .../add/nearby/AddNearbyContactViewModel.java | 2 +- .../conversation/BluetoothConnecter.java | 108 +++++++++---- .../BluetoothConnecterDialogFragment.java | 143 ++++++++++++++++++ .../conversation/ConversationActivity.java | 23 +-- .../RequestBluetoothDiscoverable.java | 30 ---- .../briar/android/util/UiUtils.java | 35 +++++ 8 files changed, 264 insertions(+), 122 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index d1f3ad737..ce359bd35 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -28,6 +28,7 @@ import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment; import org.briarproject.briar.android.contact.add.remote.NicknameFragment; import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity; import org.briarproject.briar.android.conversation.AliasDialogFragment; +import org.briarproject.briar.android.conversation.BluetoothConnecterDialogFragment; import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ConversationSettingsDialog; import org.briarproject.briar.android.conversation.ImageActivity; @@ -236,4 +237,6 @@ public interface ActivityComponent { void inject(ConversationSettingsDialog dialog); + void inject( + BluetoothConnecterDialogFragment bluetoothConnecterDialogFragment); } 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 b0bc75fbe..dc8fca71f 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,10 +1,6 @@ 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; @@ -19,11 +15,11 @@ 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; +import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled; +import static org.briarproject.briar.android.util.UiUtils.showLocationDialog; class AddNearbyContactPermissionManager { @@ -51,19 +47,6 @@ 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; - } - } - static boolean areEssentialPermissionsGranted(Context ctx, boolean isBluetoothSupported) { int ok = PERMISSION_GRANTED; @@ -106,7 +89,7 @@ class AddNearbyContactPermissionManager { } else if (locationPermission == Permission.SHOW_RATIONALE) { showRationale(R.string.permission_location_title, R.string.permission_location_request_body); - } else if (isLocationEnabled(ctx)) { + } else if (locationEnabled) { requestPermissions(); } else { showLocationDialog(ctx); @@ -135,25 +118,6 @@ 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 2a284f685..a7a18dee1 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 @@ -82,11 +82,11 @@ 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; import static org.briarproject.briar.android.util.UiUtils.handleException; +import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled; @NotNullByDefault class AddNearbyContactViewModel extends AndroidViewModel diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java index 54e28bd47..c3a04d4d2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -6,6 +6,8 @@ import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.widget.Toast; +import org.briarproject.bramble.api.connection.ConnectionRegistry; +import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.plugin.BluetoothConstants; import org.briarproject.bramble.api.plugin.Plugin; @@ -21,90 +23,130 @@ import java.util.logging.Logger; import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.os.Build.VERSION.SDK_INT; import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; 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.showLocationDialog; class BluetoothConnecter { private final Logger LOG = getLogger(BluetoothConnecter.class.getName()); + private enum Permission { + UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED + } + private final Application app; private final Executor ioExecutor; private final AndroidExecutor androidExecutor; - + private final ConnectionRegistry connectionRegistry; private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); private final Plugin bluetoothPlugin; + private Permission locationPermission = Permission.UNKNOWN; + @Inject BluetoothConnecter(Application app, PluginManager pluginManager, @IoExecutor Executor ioExecutor, - AndroidExecutor androidExecutor) { + AndroidExecutor androidExecutor, + ConnectionRegistry connectionRegistry) { this.app = app; this.ioExecutor = ioExecutor; this.androidExecutor = androidExecutor; this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + this.connectionRegistry = connectionRegistry; } - static void showDialog(Context ctx, - ActivityResultLauncher permissionRequest) { - new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) - .setTitle(R.string.dialog_title_connect_via_bluetooth) - .setMessage(R.string.dialog_message_connect_via_bluetooth) - .setPositiveButton(R.string.start, (dialog, which) -> - permissionRequest.launch(ACCESS_FINE_LOCATION)) - .setNegativeButton(R.string.cancel, null) - .show(); + boolean isConnectedViaBluetooth(ContactId contactId) { + return connectionRegistry.isConnected(contactId, BluetoothConstants.ID); + } + + boolean isDiscovering() { + // TODO bluetoothPlugin.isDiscovering() + return false; + } + + /** + * Call this when the using activity or fragment starts, + * because permissions might have changed while it was stopped. + */ + void resetPermissions() { + locationPermission = Permission.UNKNOWN; } @UiThread - void onLocationPermissionResult(Activity activity, boolean result, - ActivityResultLauncher bluetoothDiscoverableRequest) { - if (result) { - if (isBluetoothSupported()) { - bluetoothDiscoverableRequest.launch(120); - } else { - showToast(R.string.toast_connect_via_bluetooth_error); - } + void onLocationPermissionResult(Activity activity, + @Nullable Boolean result) { + if (result != null && result) { + locationPermission = Permission.GRANTED; } else if (shouldShowRequestPermissionRationale(activity, ACCESS_FINE_LOCATION)) { - showToast(R.string.permission_location_denied_body); + locationPermission = Permission.SHOW_RATIONALE; } else { - showRationale(activity); + locationPermission = Permission.PERMANENTLY_DENIED; } } - private boolean isBluetoothSupported() { - return bt != null && bluetoothPlugin != null; + boolean isBluetoothNotSupported() { + // When this class is instantiated before we are logged in + // (like when returning to a killed activity), bluetoothPlugin will be + // null and we consider bluetooth not supported. + return bt == null || bluetoothPlugin == null; } - private void showRationale(Context ctx) { + boolean areRequirementsFulfilled(Context ctx, + ActivityResultLauncher permissionRequest) { + boolean permissionGranted = + SDK_INT < 23 || locationPermission == Permission.GRANTED; + boolean locationEnabled = isLocationEnabled(ctx); + if (permissionGranted && locationEnabled) return true; + + if (locationPermission == Permission.PERMANENTLY_DENIED) { + showDenialDialog(ctx); + } else if (locationPermission == Permission.SHOW_RATIONALE) { + showRationale(ctx, permissionRequest); + } else if (!locationEnabled) { + showLocationDialog(ctx); + } + return false; + } + + private void showDenialDialog(Context ctx) { new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) .setTitle(R.string.permission_location_title) - .setMessage(R.string.permission_location_request_body) + .setMessage(R.string.permission_location_denied_body) .setPositiveButton(R.string.ok, getGoToSettingsListener(ctx)) .setNegativeButton(R.string.cancel, null) .show(); } - @UiThread - void onBluetoothDiscoverable(boolean result, ContactItem contact) { - if (result) { - connect(contact); - } else { - showToast(R.string.toast_connect_via_bluetooth_not_discoverable); - } + private void showRationale(Context ctx, + ActivityResultLauncher 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)) + .show(); } - private void connect(ContactItem contact) { + @UiThread + void onBluetoothDiscoverable(ContactItem contact) { + connect(contact.getContact().getId()); + } + + private void connect(ContactId contactId) { // TODO // * enable bluetooth connections setting, if not enabled // * wait for plugin to become active diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java new file mode 100644 index 000000000..46f0f9b4a --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java @@ -0,0 +1,143 @@ +package org.briarproject.briar.android.conversation; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.contact.ContactItem; +import org.briarproject.briar.android.util.RequestBluetoothDiscoverable; + +import javax.inject.Inject; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.content.DialogInterface.BUTTON_POSITIVE; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.Objects.requireNonNull; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class BluetoothConnecterDialogFragment extends DialogFragment { + + final static String TAG = BluetoothConnecterDialogFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private ConversationViewModel viewModel; + private BluetoothConnecter bluetoothConnecter; + + private final ActivityResultLauncher bluetoothDiscoverableRequest = + registerForActivityResult(new RequestBluetoothDiscoverable(), + this::onBluetoothDiscoverable); + private final ActivityResultLauncher permissionRequest = + registerForActivityResult(new RequestPermission(), + this::onPermissionRequestResult); + + @Override + public void onAttach(Context ctx) { + super.onAttach(ctx); + ((BaseActivity) requireActivity()).getActivityComponent().inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(ConversationViewModel.class); + bluetoothConnecter = viewModel.getBluetoothConnecter(); + } + + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Context ctx = requireContext(); + return new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) + .setTitle(R.string.dialog_title_connect_via_bluetooth) + .setMessage(R.string.dialog_message_connect_via_bluetooth) + // actual listener gets set in onResume() + .setPositiveButton(R.string.start, null) + .setNegativeButton(R.string.cancel, null) + .setCancelable(false) // keep it open until dismissed + .create(); + } + + @Override + public void onStart() { + super.onStart(); + if (bluetoothConnecter.isBluetoothNotSupported()) { + showToast(R.string.toast_connect_via_bluetooth_error); + dismiss(); + return; + } + // MenuItem only gets enabled after contactItem has loaded + ContactItem contact = + requireNonNull(viewModel.getContactItem().getValue()); + ContactId contactId = contact.getContact().getId(); + if (bluetoothConnecter.isConnectedViaBluetooth(contactId)) { + showToast(R.string.toast_connect_via_bluetooth_success); + dismiss(); + return; + } + if (bluetoothConnecter.isDiscovering()) { + // TODO showToast(R.string.toast_connect_via_bluetooth_discovering); + dismiss(); + return; + } + bluetoothConnecter.resetPermissions(); + } + + @Override + public void onResume() { + super.onResume(); + // Set the click listener for the START button here + // to prevent it from automatically dismissing the dialog. + // The dialog is shown in onStart(), so we set the listener here later. + AlertDialog dialog = (AlertDialog) getDialog(); + Button positiveButton = (Button) dialog.getButton(BUTTON_POSITIVE); + positiveButton.setOnClickListener(this::onStartClicked); + } + + private void onStartClicked(View v) { + // The dialog starts a permission request which comes back as true + // if the permission is already granted. So it is a generic entry point. + permissionRequest.launch(ACCESS_FINE_LOCATION); + } + + private void onPermissionRequestResult(@Nullable Boolean result) { + Activity a = requireActivity(); + // update permission result in BluetoothConnecter + bluetoothConnecter.onLocationPermissionResult(a, result); + // if requirements are fulfilled, request Bluetooth discoverability + if (bluetoothConnecter.areRequirementsFulfilled(a, permissionRequest)) { + bluetoothDiscoverableRequest.launch(120); // for 2min + } + } + + private void onBluetoothDiscoverable(@Nullable Boolean result) { + if (result != null && result) { + // MenuItem only gets enabled after contactItem has loaded + ContactItem contact = + requireNonNull(viewModel.getContactItem().getValue()); + bluetoothConnecter.onBluetoothDiscoverable(contact); + dismiss(); + } else { + showToast(R.string.toast_connect_via_bluetooth_not_discoverable); + } + } + + private void showToast(@StringRes int stringRes) { + Toast.makeText(requireContext(), stringRes, LENGTH_LONG).show(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 3bab9a903..77088fd63 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -47,7 +47,6 @@ import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentRetriever; import org.briarproject.briar.android.blog.BlogActivity; -import org.briarproject.briar.android.contact.ContactItem; import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; import org.briarproject.briar.android.forum.ForumActivity; @@ -92,8 +91,6 @@ import java.util.logging.Logger; import javax.inject.Inject; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; @@ -102,6 +99,7 @@ import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityOptionsCompat; import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; @@ -214,21 +212,6 @@ public class ConversationActivity extends BriarActivity private volatile ContactId contactId; - private final ActivityResultLauncher bluetoothDiscoverableRequest = - registerForActivityResult(new RequestBluetoothDiscoverable(), r -> { - // MenuItem only gets enabled after contactItem has loaded - ContactItem contact = - requireNonNull(viewModel.getContactItem().getValue()); - BluetoothConnecter bc = viewModel.getBluetoothConnecter(); - bc.onBluetoothDiscoverable(r, contact); - }); - private final ActivityResultLauncher permissionRequest = - registerForActivityResult(new RequestPermission(), result -> { - BluetoothConnecter bc = viewModel.getBluetoothConnecter(); - bc.onLocationPermissionResult(this, result, - bluetoothDiscoverableRequest); - }); - @Override public void onCreate(@Nullable Bundle state) { if (SDK_INT >= 21) { @@ -424,7 +407,9 @@ public class ConversationActivity extends BriarActivity onAutoDeleteTimerNoticeClicked(); return true; } else if (itemId == R.id.action_connect_via_bluetooth) { - BluetoothConnecter.showDialog(this, permissionRequest); + FragmentManager fm = getSupportFragmentManager(); + new BluetoothConnecterDialogFragment().show(fm, + BluetoothConnecterDialogFragment.TAG); return true; } else if (itemId == R.id.action_delete_all_messages) { askToDeleteAllMessages(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java deleted file mode 100644 index 95023f6bf..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/RequestBluetoothDiscoverable.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.briarproject.briar.android.conversation; - -import android.content.Context; -import android.content.Intent; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import androidx.activity.result.contract.ActivityResultContract; -import androidx.annotation.Nullable; - -import static android.app.Activity.RESULT_CANCELED; -import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; -import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION; - -@NotNullByDefault -class RequestBluetoothDiscoverable - extends ActivityResultContract { - - @Override - public Intent createIntent(Context context, Integer duration) { - Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); - i.putExtra(EXTRA_DISCOVERABLE_DURATION, duration); - return i; - } - - @Override - public Boolean parseResult(int resultCode, @Nullable Intent intent) { - return resultCode != RESULT_CANCELED; - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index e971c8c9b..e89b07100 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -3,11 +3,13 @@ package org.briarproject.briar.android.util; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.KeyguardManager; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.location.LocationManager; import android.net.Uri; import android.os.PowerManager; import android.text.Spannable; @@ -72,6 +74,7 @@ import static android.content.Intent.EXTRA_MIME_TYPES; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.Build.MANUFACTURER; import static android.os.Build.VERSION.SDK_INT; +import static android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS; import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.FORMAT_ABBREV_ALL; @@ -331,6 +334,19 @@ public class UiUtils { return i; } + /** + * @return true if location is enabled, + * or it isn't required due to this being a SDK < 28 device. + */ + public static boolean isLocationEnabled(Context ctx) { + if (SDK_INT >= 28) { + LocationManager lm = ctx.getSystemService(LocationManager.class); + return lm.isLocationEnabled(); + } else { + return true; + } + } + public static boolean isSamsung7() { return (SDK_INT == 24 || SDK_INT == 25) && MANUFACTURER.equalsIgnoreCase("Samsung"); @@ -474,6 +490,25 @@ public class UiUtils { return isoCode; } + public 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(); + } + public static Drawable getDialogIcon(Context ctx, @DrawableRes int resId) { Drawable icon = VectorDrawableCompat.create(ctx.getResources(), resId, null); From 688bac77a8ecbcaea9faa047c6a71e6d5c771b80 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 19 Apr 2021 11:36:35 -0300 Subject: [PATCH 4/6] Use a feature flag to hide connect via Bluetooth option --- .../main/java/org/briarproject/bramble/api/FeatureFlags.java | 2 ++ .../bramble/test/BrambleCoreIntegrationTestModule.java | 5 +++++ .../main/java/org/briarproject/briar/android/AppModule.java | 5 +++++ .../briar/android/conversation/ConversationActivity.java | 3 +++ .../java/org/briarproject/briar/headless/HeadlessModule.kt | 1 + .../org/briarproject/briar/headless/HeadlessTestModule.kt | 1 + 6 files changed, 17 insertions(+) diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java index f0acbd8e1..e15074dd6 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java @@ -10,4 +10,6 @@ public interface FeatureFlags { boolean shouldEnableProfilePictures(); boolean shouldEnableDisappearingMessages(); + + boolean shouldEnableConnectViewBluetooth(); } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java index 661df400a..bb946a51a 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java @@ -38,6 +38,11 @@ public class BrambleCoreIntegrationTestModule { public boolean shouldEnableDisappearingMessages() { return true; } + + @Override + public boolean shouldEnableConnectViewBluetooth() { + return true; + } }; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 9a75f6e2e..045579ec9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -298,6 +298,11 @@ public class AppModule { public boolean shouldEnableDisappearingMessages() { return IS_DEBUG_BUILD; } + + @Override + public boolean shouldEnableConnectViewBluetooth() { + return IS_DEBUG_BUILD; + } }; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 77088fd63..a2ef7ffc8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -371,6 +371,9 @@ public class ConversationActivity extends BriarActivity this::showIntroductionOnboarding); } }); + if (!featureFlags.shouldEnableConnectViewBluetooth()) { + menu.findItem(R.id.action_connect_via_bluetooth).setVisible(false); + } // enable alias and bluetooth action once available observeOnce(viewModel.getContactItem(), this, contact -> { menu.findItem(R.id.action_set_alias).setEnabled(true); diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt index e004bdca4..48ed124f8 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt @@ -94,5 +94,6 @@ internal class HeadlessModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = false + override fun shouldEnableConnectViewBluetooth() = false } } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt index e299158b7..c16d6a2cf 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt @@ -82,6 +82,7 @@ internal class HeadlessTestModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = false + override fun shouldEnableConnectViewBluetooth() = false } @Provides From 0b89d29c7d52c6fd0053931aabd3ff2c40b84e59 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 20 Apr 2021 12:13:07 -0300 Subject: [PATCH 5/6] Reset BluetoothPlugin reference When it is assigned before we are signed-in in (like when returning to a killed activity), it would be null and without a reset, we would consider Bluetooth to not be supported. --- .../android/conversation/BluetoothConnecter.java | 14 +++++++++----- .../BluetoothConnecterDialogFragment.java | 3 +-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java index c3a04d4d2..452e7fd78 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -47,11 +47,13 @@ class BluetoothConnecter { } private final Application app; + private final PluginManager pluginManager; private final Executor ioExecutor; private final AndroidExecutor androidExecutor; private final ConnectionRegistry connectionRegistry; private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - private final Plugin bluetoothPlugin; + + private volatile Plugin bluetoothPlugin; private Permission locationPermission = Permission.UNKNOWN; @@ -62,6 +64,7 @@ class BluetoothConnecter { AndroidExecutor androidExecutor, ConnectionRegistry connectionRegistry) { this.app = app; + this.pluginManager = pluginManager; this.ioExecutor = ioExecutor; this.androidExecutor = androidExecutor; this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); @@ -81,8 +84,12 @@ class BluetoothConnecter { * Call this when the using activity or fragment starts, * because permissions might have changed while it was stopped. */ - void resetPermissions() { + void reset() { locationPermission = Permission.UNKNOWN; + // When this class is instantiated before we are logged in + // (like when returning to a killed activity), bluetoothPlugin would be + // null and we consider bluetooth not supported. So reset here. + bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); } @UiThread @@ -99,9 +106,6 @@ class BluetoothConnecter { } boolean isBluetoothNotSupported() { - // When this class is instantiated before we are logged in - // (like when returning to a killed activity), bluetoothPlugin will be - // null and we consider bluetooth not supported. return bt == null || bluetoothPlugin == null; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java index 46f0f9b4a..bb2d5ff82 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java @@ -75,6 +75,7 @@ public class BluetoothConnecterDialogFragment extends DialogFragment { @Override public void onStart() { super.onStart(); + bluetoothConnecter.reset(); if (bluetoothConnecter.isBluetoothNotSupported()) { showToast(R.string.toast_connect_via_bluetooth_error); dismiss(); @@ -92,9 +93,7 @@ public class BluetoothConnecterDialogFragment extends DialogFragment { if (bluetoothConnecter.isDiscovering()) { // TODO showToast(R.string.toast_connect_via_bluetooth_discovering); dismiss(); - return; } - bluetoothConnecter.resetPermissions(); } @Override From ea5280713f69a1e09798b958a95a2581c4c6aae7 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 20 Apr 2021 12:21:25 -0300 Subject: [PATCH 6/6] Address review feedback for Connect via Bluetooth UI --- .../briarproject/bramble/api/FeatureFlags.java | 2 +- .../test/BrambleCoreIntegrationTestModule.java | 2 +- .../org/briarproject/briar/android/AppModule.java | 2 +- .../android/conversation/BluetoothConnecter.java | 10 ++++++---- .../BluetoothConnecterDialogFragment.java | 15 ++++++++++++--- .../conversation/ConversationActivity.java | 2 +- briar-android/src/main/res/values/strings.xml | 5 +++-- .../briarproject/briar/headless/HeadlessModule.kt | 2 +- .../briar/headless/HeadlessTestModule.kt | 2 +- 9 files changed, 27 insertions(+), 15 deletions(-) diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java index e15074dd6..407e9717f 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java @@ -11,5 +11,5 @@ public interface FeatureFlags { boolean shouldEnableDisappearingMessages(); - boolean shouldEnableConnectViewBluetooth(); + boolean shouldEnableConnectViaBluetooth(); } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java index bb946a51a..22dea21f6 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java @@ -40,7 +40,7 @@ public class BrambleCoreIntegrationTestModule { } @Override - public boolean shouldEnableConnectViewBluetooth() { + public boolean shouldEnableConnectViaBluetooth() { return true; } }; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 045579ec9..5770b1021 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -300,7 +300,7 @@ public class AppModule { } @Override - public boolean shouldEnableConnectViewBluetooth() { + public boolean shouldEnableConnectViaBluetooth() { return IS_DEBUG_BUILD; } }; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java index 452e7fd78..050a91e50 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -110,14 +110,15 @@ class BluetoothConnecter { } boolean areRequirementsFulfilled(Context ctx, - ActivityResultLauncher permissionRequest) { + ActivityResultLauncher permissionRequest, + Runnable onLocationDenied) { boolean permissionGranted = SDK_INT < 23 || locationPermission == Permission.GRANTED; boolean locationEnabled = isLocationEnabled(ctx); if (permissionGranted && locationEnabled) return true; if (locationPermission == Permission.PERMANENTLY_DENIED) { - showDenialDialog(ctx); + showDenialDialog(ctx, onLocationDenied); } else if (locationPermission == Permission.SHOW_RATIONALE) { showRationale(ctx, permissionRequest); } else if (!locationEnabled) { @@ -126,12 +127,13 @@ class BluetoothConnecter { return false; } - private void showDenialDialog(Context ctx) { + private void showDenialDialog(Context ctx, Runnable onLocationDenied) { new AlertDialog.Builder(ctx, R.style.BriarDialogTheme) .setTitle(R.string.permission_location_title) .setMessage(R.string.permission_location_denied_body) .setPositiveButton(R.string.ok, getGoToSettingsListener(ctx)) - .setNegativeButton(R.string.cancel, null) + .setNegativeButton(R.string.cancel, (v, d) -> + onLocationDenied.run()) .show(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java index bb2d5ff82..30d3b781e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java @@ -103,13 +103,14 @@ public class BluetoothConnecterDialogFragment extends DialogFragment { // to prevent it from automatically dismissing the dialog. // The dialog is shown in onStart(), so we set the listener here later. AlertDialog dialog = (AlertDialog) getDialog(); - Button positiveButton = (Button) dialog.getButton(BUTTON_POSITIVE); + Button positiveButton = dialog.getButton(BUTTON_POSITIVE); positiveButton.setOnClickListener(this::onStartClicked); } private void onStartClicked(View v) { // The dialog starts a permission request which comes back as true - // if the permission is already granted. So it is a generic entry point. + // 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); } @@ -117,8 +118,16 @@ public class BluetoothConnecterDialogFragment extends DialogFragment { Activity a = requireActivity(); // update permission result in BluetoothConnecter bluetoothConnecter.onLocationPermissionResult(a, result); + // what to do when the user denies granting the location permission + Runnable onLocationPermissionDenied = () -> { + Toast.makeText(requireContext(), + R.string.toast_connect_via_bluetooth_no_location_permission, + LENGTH_LONG).show(); + dismiss(); + }; // if requirements are fulfilled, request Bluetooth discoverability - if (bluetoothConnecter.areRequirementsFulfilled(a, permissionRequest)) { + if (bluetoothConnecter.areRequirementsFulfilled(a, permissionRequest, + onLocationPermissionDenied)) { bluetoothDiscoverableRequest.launch(120); // for 2min } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index a2ef7ffc8..32b43d9db 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -371,7 +371,7 @@ public class ConversationActivity extends BriarActivity this::showIntroductionOnboarding); } }); - if (!featureFlags.shouldEnableConnectViewBluetooth()) { + if (!featureFlags.shouldEnableConnectViaBluetooth()) { menu.findItem(R.id.action_connect_via_bluetooth).setVisible(false); } // enable alias and bluetooth action once available diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 27e3ec46c..b1ed421b9 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -171,8 +171,9 @@ Disappearing messages Connect via Bluetooth Connect via Bluetooth - Your contact needs to be in close proximity for this to work. They need to press start at the same time as you and confirm the following system dialog. - Can not continue without Bluetooth + Your contact needs to be nearby for this to work.\n\nYou and your contact should both press \"Start\" at the same time. + Cannot continue without Bluetooth + Cannot continue without location permission Connecting via Bluetooth… Successfully connected via Bluetooth Could not connect via Bluetooth diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt index 48ed124f8..58c217d4a 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt @@ -94,6 +94,6 @@ internal class HeadlessModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = false - override fun shouldEnableConnectViewBluetooth() = false + override fun shouldEnableConnectViaBluetooth() = false } } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt index c16d6a2cf..c76212827 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt @@ -82,7 +82,7 @@ internal class HeadlessTestModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = false - override fun shouldEnableConnectViewBluetooth() = false + override fun shouldEnableConnectViaBluetooth() = false } @Provides