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..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 @@ -10,4 +10,6 @@ public interface FeatureFlags { boolean shouldEnableProfilePictures(); boolean shouldEnableDisappearingMessages(); + + 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 661df400a..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 @@ -38,6 +38,11 @@ public class BrambleCoreIntegrationTestModule { public boolean shouldEnableDisappearingMessages() { return true; } + + @Override + 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 9a75f6e2e..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 @@ -298,6 +298,11 @@ public class AppModule { public boolean shouldEnableDisappearingMessages() { return IS_DEBUG_BUILD; } + + @Override + public boolean shouldEnableConnectViaBluetooth() { + return IS_DEBUG_BUILD; + } }; } } 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 new file mode 100644 index 000000000..050a91e50 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -0,0 +1,182 @@ +package org.briarproject.briar.android.conversation; + +import android.app.Activity; +import android.app.Application; +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; +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.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 PluginManager pluginManager; + private final Executor ioExecutor; + private final AndroidExecutor androidExecutor; + private final ConnectionRegistry connectionRegistry; + private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + + private volatile Plugin bluetoothPlugin; + + private Permission locationPermission = Permission.UNKNOWN; + + @Inject + BluetoothConnecter(Application app, + PluginManager pluginManager, + @IoExecutor Executor ioExecutor, + AndroidExecutor androidExecutor, + ConnectionRegistry connectionRegistry) { + this.app = app; + this.pluginManager = pluginManager; + this.ioExecutor = ioExecutor; + this.androidExecutor = androidExecutor; + this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + this.connectionRegistry = connectionRegistry; + } + + 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 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 + void onLocationPermissionResult(Activity activity, + @Nullable Boolean result) { + if (result != null && result) { + locationPermission = Permission.GRANTED; + } else if (shouldShowRequestPermissionRationale(activity, + ACCESS_FINE_LOCATION)) { + locationPermission = Permission.SHOW_RATIONALE; + } else { + locationPermission = Permission.PERMANENTLY_DENIED; + } + } + + boolean isBluetoothNotSupported() { + return bt == null || bluetoothPlugin == null; + } + + boolean areRequirementsFulfilled(Context ctx, + 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, onLocationDenied); + } else if (locationPermission == Permission.SHOW_RATIONALE) { + showRationale(ctx, permissionRequest); + } else if (!locationEnabled) { + showLocationDialog(ctx); + } + return false; + } + + 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, (v, d) -> + onLocationDenied.run()) + .show(); + } + + 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(); + } + + @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 + 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/BluetoothConnecterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java new file mode 100644 index 000000000..30d3b781e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java @@ -0,0 +1,151 @@ +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(); + bluetoothConnecter.reset(); + 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(); + } + } + + @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 = 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 we can use the request as a generic entry point to the whole flow. + permissionRequest.launch(ACCESS_FINE_LOCATION); + } + + private void onPermissionRequestResult(@Nullable Boolean result) { + 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, + onLocationPermissionDenied)) { + 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 17da35e55..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 @@ -99,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; @@ -370,9 +371,14 @@ 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)); + if (!featureFlags.shouldEnableConnectViaBluetooth()) { + 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); + 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 +387,41 @@ 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) { + FragmentManager fm = getSupportFragmentManager(); + new BluetoothConnecterDialogFragment().show(fm, + BluetoothConnecterDialogFragment.TAG); + 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/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); 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,14 @@ Change contact name Contact name Disappearing messages + Connect via Bluetooth + Connect via 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 Your messages will disappear after %1$s. %2$s 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..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,5 +94,6 @@ internal class HeadlessModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = 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 e299158b7..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,6 +82,7 @@ internal class HeadlessTestModule(private val appDir: File) { override fun shouldEnableImageAttachments() = false override fun shouldEnableProfilePictures() = false override fun shouldEnableDisappearingMessages() = false + override fun shouldEnableConnectViaBluetooth() = false } @Provides