diff --git a/.gitignore b/.gitignore
index 21c3404ab..f4f05ec62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ local.properties
# Android Studio
.idea/*
+!.idea/inspectionProfiles/
!.idea/runConfigurations/
!.idea/codeStyleSettings.xml
!.idea/codeStyles
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 000000000..9180c3471
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java
index 5785575ee..f3c19076d 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java
@@ -34,12 +34,12 @@ class KeyAgreementTransport {
Logger.getLogger(KeyAgreementTransport.class.getName());
// Accept records with current protocol version, known record type
- private static Predicate ACCEPT = r ->
+ private static final Predicate ACCEPT = r ->
r.getProtocolVersion() == PROTOCOL_VERSION &&
isKnownRecordType(r.getRecordType());
// Ignore records with current protocol version, unknown record type
- private static Predicate IGNORE = r ->
+ private static final Predicate IGNORE = r ->
r.getProtocolVersion() == PROTOCOL_VERSION &&
!isKnownRecordType(r.getRecordType());
diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml
index 988ea2b6f..87d431f37 100644
--- a/briar-android/src/main/AndroidManifest.xml
+++ b/briar-android/src/main/AndroidManifest.xml
@@ -342,7 +342,7 @@
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 437d25388..6d8507600 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
@@ -33,9 +33,9 @@ import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.contact.ContactListModule;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.introduction.IntroductionModule;
-import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.logging.LoggingModule;
import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
@@ -77,7 +77,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = {
SetupModule.class,
DozeHelperModule.class,
- ContactExchangeModule.class,
+ AddNearbyContactModule.class,
LoggingModule.class,
LoginModule.class,
NavDrawerModule.class,
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 e5914e1cf..1ce3bbd76 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
@@ -19,6 +19,10 @@ import org.briarproject.briar.android.blog.RssFeedImportActivity;
import org.briarproject.briar.android.blog.RssFeedManageActivity;
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.contact.ContactListFragment;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactErrorFragment;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactFragment;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactIntroFragment;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment;
import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
@@ -34,10 +38,6 @@ import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
-import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
-import org.briarproject.briar.android.keyagreement.ContactExchangeErrorFragment;
-import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
-import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
import org.briarproject.briar.android.login.ChangePasswordActivity;
import org.briarproject.briar.android.login.OpenDatabaseFragment;
import org.briarproject.briar.android.login.PasswordFragment;
@@ -105,9 +105,7 @@ public interface ActivityComponent {
void inject(PanicPreferencesActivity activity);
- void inject(ContactExchangeActivity activity);
-
- void inject(KeyAgreementActivity activity);
+ void inject(AddNearbyContactActivity activity);
void inject(ConversationActivity activity);
@@ -203,7 +201,9 @@ public interface ActivityComponent {
void inject(FeedFragment fragment);
- void inject(KeyAgreementFragment fragment);
+ void inject(AddNearbyContactIntroFragment fragment);
+
+ void inject(AddNearbyContactFragment fragment);
void inject(LinkExchangeFragment fragment);
@@ -221,7 +221,7 @@ public interface ActivityComponent {
void inject(ScreenFilterDialogFragment fragment);
- void inject(ContactExchangeErrorFragment fragment);
+ void inject(AddNearbyContactErrorFragment fragment);
void inject(AliasDialogFragment aliasDialogFragment);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java
index cb0133788..991aab447 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java
@@ -8,9 +8,7 @@ public interface RequestCodes {
int REQUEST_SHARE_FORUM = 4;
int REQUEST_SHARE_BLOG = 6;
int REQUEST_RINGTONE = 7;
- int REQUEST_PERMISSION_CAMERA_LOCATION = 8;
int REQUEST_DOZE_WHITELISTING = 9;
- int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
int REQUEST_UNLOCK = 11;
int REQUEST_KEYGUARD_UNLOCK = 12;
int REQUEST_ATTACH_IMAGE = 13;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
index 9d32e0cab..380deb968 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
@@ -15,11 +15,11 @@ 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.ActivityComponent;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.fragment.BaseFragment;
-import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
@@ -125,7 +125,8 @@ public class ContactListFragment extends BaseFragment
switch (itemId) {
case R.id.action_add_contact_nearby:
Intent intent =
- new Intent(getContext(), ContactExchangeActivity.class);
+ new Intent(getContext(),
+ AddNearbyContactActivity.class);
startActivity(intent);
return;
case R.id.action_add_contact_remotely:
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddContactState.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddContactState.java
new file mode 100644
index 000000000..df56be6bb
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddContactState.java
@@ -0,0 +1,76 @@
+package org.briarproject.briar.android.contact.add.nearby;
+
+import android.graphics.Bitmap;
+
+import org.briarproject.bramble.api.identity.Author;
+
+import androidx.annotation.Nullable;
+
+abstract class AddContactState {
+
+ static class KeyAgreementListening extends AddContactState {
+ final Bitmap qrCode;
+
+ KeyAgreementListening(Bitmap qrCode) {
+ this.qrCode = qrCode;
+ }
+ }
+
+ static class QrCodeScanned extends AddContactState {
+ }
+
+ static class KeyAgreementWaiting extends AddContactState {
+ }
+
+ static class KeyAgreementStarted extends AddContactState {
+ }
+
+ static class ContactExchangeStarted extends AddContactState {
+ }
+
+ static class ContactExchangeFinished extends AddContactState {
+ final ContactExchangeResult result;
+
+ ContactExchangeFinished(ContactExchangeResult result) {
+ this.result = result;
+ }
+ }
+
+ static class Failed extends AddContactState {
+ /**
+ * Non-null if failed due to the scanned QR code version.
+ * True if the app producing the code is too old.
+ * False if the scanning app is too old.
+ */
+ @Nullable
+ final Boolean qrCodeTooOld;
+
+ Failed(@Nullable Boolean qrCodeTooOld) {
+ this.qrCodeTooOld = qrCodeTooOld;
+ }
+
+ Failed() {
+ this(null);
+ }
+ }
+
+ abstract static class ContactExchangeResult {
+ static class Success extends ContactExchangeResult {
+ final Author remoteAuthor;
+
+ Success(Author remoteAuthor) {
+ this.remoteAuthor = remoteAuthor;
+ }
+ }
+
+ static class Error extends ContactExchangeResult {
+ @Nullable
+ final Author duplicateAuthor;
+
+ Error(@Nullable Author duplicateAuthor) {
+ this.duplicateAuthor = duplicateAuthor;
+ }
+ }
+ } // end ContactExchangeResult
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java
new file mode 100644
index 000000000..6dd6ca690
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java
@@ -0,0 +1,193 @@
+package org.briarproject.briar.android.contact.add.nearby;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import org.briarproject.bramble.api.identity.Author;
+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.ActivityComponent;
+import org.briarproject.briar.android.activity.BriarActivity;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeFinished;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.Failed;
+import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision;
+import org.briarproject.briar.android.fragment.BaseFragment;
+import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
+import org.briarproject.briar.android.util.RequestBluetoothDiscoverable;
+
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
+import static android.widget.Toast.LENGTH_LONG;
+import static java.util.Objects.requireNonNull;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.ACCEPTED;
+import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.REFUSED;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class AddNearbyContactActivity extends BriarActivity
+ implements BaseFragmentListener {
+
+ private static final Logger LOG =
+ getLogger(AddNearbyContactActivity.class.getName());
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private AddNearbyContactViewModel viewModel;
+ private final ActivityResultLauncher bluetoothLauncher =
+ registerForActivityResult(new RequestBluetoothDiscoverable(),
+ this::onBluetoothDiscoverableResult);
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ viewModel = new ViewModelProvider(this, viewModelFactory)
+ .get(AddNearbyContactViewModel.class);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle state) {
+ super.onCreate(state);
+ setContentView(R.layout.activity_fragment_container_toolbar);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
+ if (state == null) {
+ showInitialFragment(AddNearbyContactIntroFragment.newInstance());
+ }
+ viewModel.getRequestBluetoothDiscoverable().observeEvent(this, r ->
+ requestBluetoothDiscoverable()); // never false
+ viewModel.getShowQrCodeFragment().observeEvent(this, show -> {
+ if (show) showQrCodeFragment();
+ });
+ requireNonNull(getSupportActionBar())
+ .setTitle(R.string.add_contact_title);
+ viewModel.getState()
+ .observe(this, this::onAddContactStateChanged);
+ }
+
+ private void onBluetoothDiscoverableResult(boolean discoverable) {
+ if (discoverable) {
+ LOG.info("Bluetooth discoverability was accepted");
+ viewModel.setBluetoothDecision(ACCEPTED);
+ } else {
+ LOG.info("Bluetooth discoverability was refused");
+ viewModel.setBluetoothDecision(REFUSED);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (viewModel.getState().getValue() instanceof Failed) {
+ // Re-create this activity when going back in failed state.
+ // This will also re-create the ViewModel, so we start fresh.
+ Intent i = new Intent(this, AddNearbyContactActivity.class);
+ i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(i);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void requestBluetoothDiscoverable() {
+ Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
+ if (i.resolveActivity(getPackageManager()) != null) {
+ LOG.info("Asking for Bluetooth discoverability");
+ viewModel.setBluetoothDecision(BluetoothDecision.WAITING);
+ bluetoothLauncher.launch(120); // 2min discoverable
+ } else {
+ viewModel.setBluetoothDecision(BluetoothDecision.NO_ADAPTER);
+ }
+ }
+
+ private void showQrCodeFragment() {
+ // FIXME #824
+ FragmentManager fm = getSupportFragmentManager();
+ if (fm.findFragmentByTag(AddNearbyContactFragment.TAG) == null) {
+ BaseFragment f = AddNearbyContactFragment.newInstance();
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, f, f.getUniqueTag())
+ .addToBackStack(f.getUniqueTag())
+ .commit();
+ }
+ }
+
+ private void onAddContactStateChanged(@Nullable AddContactState state) {
+ if (state instanceof ContactExchangeFinished) {
+ ContactExchangeResult result =
+ ((ContactExchangeFinished) state).result;
+ onContactExchangeResult(result);
+ } else if (state instanceof Failed) {
+ Boolean qrCodeTooOld = ((Failed) state).qrCodeTooOld;
+ onAddingContactFailed(qrCodeTooOld);
+ }
+ }
+
+ private void onContactExchangeResult(ContactExchangeResult result) {
+ if (result instanceof ContactExchangeResult.Success) {
+ Author remoteAuthor =
+ ((ContactExchangeResult.Success) result).remoteAuthor;
+ String contactName = remoteAuthor.getName();
+ String text = getString(R.string.contact_added_toast, contactName);
+ Toast.makeText(this, text, LENGTH_LONG).show();
+ supportFinishAfterTransition();
+ } else if (result instanceof ContactExchangeResult.Error) {
+ Author duplicateAuthor =
+ ((ContactExchangeResult.Error) result).duplicateAuthor;
+ if (duplicateAuthor == null) {
+ showErrorFragment();
+ } else {
+ String contactName = duplicateAuthor.getName();
+ String text =
+ getString(R.string.contact_already_exists, contactName);
+ Toast.makeText(this, text, LENGTH_LONG).show();
+ supportFinishAfterTransition();
+ }
+ } else throw new AssertionError();
+ }
+
+ private void onAddingContactFailed(@Nullable Boolean qrCodeTooOld) {
+ if (qrCodeTooOld == null) {
+ showErrorFragment();
+ } else {
+ String msg;
+ if (qrCodeTooOld) {
+ msg = getString(R.string.qr_code_too_old,
+ getString(R.string.app_name));
+ } else {
+ msg = getString(R.string.qr_code_too_new,
+ getString(R.string.app_name));
+ }
+ showNextFragment(AddNearbyContactErrorFragment.newInstance(msg));
+ }
+ }
+
+ private void showErrorFragment() {
+ showNextFragment(new AddNearbyContactErrorFragment());
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactErrorFragment.java
similarity index 70%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactErrorFragment.java
index be708d73c..9944f3663 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeErrorFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactErrorFragment.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import android.content.Intent;
import android.os.Bundle;
@@ -15,8 +15,11 @@ import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.UiUtils;
+import javax.inject.Inject;
+
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.GONE;
@@ -24,14 +27,19 @@ import static org.briarproject.briar.android.util.UiUtils.onSingleLinkClick;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
-public class ContactExchangeErrorFragment extends BaseFragment {
+public class AddNearbyContactErrorFragment extends BaseFragment {
public static final String TAG =
- ContactExchangeErrorFragment.class.getName();
+ AddNearbyContactErrorFragment.class.getName();
private static final String ERROR_MSG = "errorMessage";
- public static ContactExchangeErrorFragment newInstance(String errorMsg) {
- ContactExchangeErrorFragment f = new ContactExchangeErrorFragment();
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private AddNearbyContactViewModel viewModel;
+
+ public static AddNearbyContactErrorFragment newInstance(String errorMsg) {
+ AddNearbyContactErrorFragment f = new AddNearbyContactErrorFragment();
Bundle args = new Bundle();
args.putString(ERROR_MSG, errorMsg);
f.setArguments(args);
@@ -46,6 +54,8 @@ public class ContactExchangeErrorFragment extends BaseFragment {
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
+ viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
+ .get(AddNearbyContactViewModel.class);
}
@Nullable
@@ -72,7 +82,7 @@ public class ContactExchangeErrorFragment extends BaseFragment {
tryAgain.setOnClickListener(view -> {
// Recreate the activity so we return to the intro fragment
FragmentActivity activity = requireActivity();
- Intent i = new Intent(activity, ContactExchangeActivity.class);
+ Intent i = new Intent(activity, AddNearbyContactActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
activity.startActivity(i);
});
@@ -81,6 +91,15 @@ public class ContactExchangeErrorFragment extends BaseFragment {
return v;
}
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // We don't do this in AddNearbyContactFragment#onDestroy()
+ // because it gets called when creating AddNearbyContactFragment
+ // in landscape orientation to force portrait orientation.
+ viewModel.stopListening();
+ }
+
private void triggerFeedback() {
UiUtils.triggerFeedback(requireContext());
finish();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java
new file mode 100644
index 000000000..fc3d43feb
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactFragment.java
@@ -0,0 +1,190 @@
+package org.briarproject.briar.android.contact.add.nearby;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+import android.widget.Toast;
+
+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.ActivityComponent;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeStarted;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.Failed;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementStarted;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementWaiting;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.QrCodeScanned;
+import org.briarproject.briar.android.fragment.BaseFragment;
+import org.briarproject.briar.android.view.QrCodeView;
+
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import androidx.annotation.UiThread;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.Toast.LENGTH_LONG;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class AddNearbyContactFragment extends BaseFragment
+ implements QrCodeView.FullscreenListener {
+
+ static final String TAG = AddNearbyContactFragment.class.getName();
+
+ private static final Logger LOG = Logger.getLogger(TAG);
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private AddNearbyContactViewModel viewModel;
+ private CameraView cameraView;
+ private LinearLayout cameraOverlay;
+ private View statusView;
+ private QrCodeView qrCodeView;
+ private TextView status;
+
+ public static AddNearbyContactFragment newInstance() {
+ Bundle args = new Bundle();
+ AddNearbyContactFragment fragment = new AddNearbyContactFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
+ .get(AddNearbyContactViewModel.class);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
+ false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ cameraView = view.findViewById(R.id.camera_view);
+ cameraView.setPreviewConsumer(viewModel.getQrCodeDecoder());
+ cameraOverlay = view.findViewById(R.id.camera_overlay);
+ statusView = view.findViewById(R.id.status_container);
+ status = view.findViewById(R.id.connect_status);
+ qrCodeView = view.findViewById(R.id.qr_code_view);
+ qrCodeView.setFullscreenListener(this);
+
+ requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
+
+ viewModel.getState().observe(getViewLifecycleOwner(),
+ this::onAddContactStateChanged);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ try {
+ cameraView.start();
+ } catch (CameraException e) {
+ logCameraExceptionAndFinish(e);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ try {
+ cameraView.stop();
+ } catch (CameraException e) {
+ logCameraExceptionAndFinish(e);
+ }
+ }
+
+ @Override
+ public void setFullscreen(boolean fullscreen) {
+ LinearLayout.LayoutParams statusParams, qrCodeParams;
+ if (fullscreen) {
+ // Grow the QR code view to fill its parent
+ statusParams = new LayoutParams(0, 0, 0f);
+ qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
+ } else {
+ // Shrink the QR code view to fill half its parent
+ if (cameraOverlay.getOrientation() == HORIZONTAL) {
+ statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
+ qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
+ } else {
+ statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
+ qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
+ }
+ }
+ statusView.setLayoutParams(statusParams);
+ qrCodeView.setLayoutParams(qrCodeParams);
+ cameraOverlay.invalidate();
+ }
+
+ @UiThread
+ private void onAddContactStateChanged(@Nullable AddContactState state) {
+ if (state instanceof AddContactState.KeyAgreementListening) {
+ Bitmap qrCode =
+ ((AddContactState.KeyAgreementListening) state).qrCode;
+ qrCodeView.setQrCode(qrCode);
+ } else if (state instanceof QrCodeScanned) {
+ try {
+ cameraView.stop();
+ } catch (CameraException e) {
+ logCameraExceptionAndFinish(e);
+ }
+ cameraView.setVisibility(INVISIBLE);
+ statusView.setVisibility(VISIBLE);
+ status.setText(R.string.connecting_to_device);
+ } else if (state instanceof KeyAgreementWaiting) {
+ status.setText(R.string.waiting_for_contact_to_scan);
+ } else if (state instanceof KeyAgreementStarted) {
+ qrCodeView.setVisibility(INVISIBLE);
+ status.setText(R.string.authenticating_with_device);
+ } else if (state instanceof ContactExchangeStarted) {
+ status.setText(R.string.exchanging_contact_details);
+ } else if (state instanceof Failed) {
+ // the activity will replace this fragment with an error fragment
+ statusView.setVisibility(INVISIBLE);
+ cameraView.setVisibility(INVISIBLE);
+ }
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+ @UiThread
+ private void logCameraExceptionAndFinish(CameraException e) {
+ logException(LOG, WARNING, e);
+ Toast.makeText(getActivity(), R.string.camera_error,
+ LENGTH_LONG).show();
+ finish();
+ }
+
+ @Override
+ protected void finish() {
+ requireActivity().getSupportFragmentManager().popBackStack();
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java
new file mode 100644
index 000000000..4e3e0c1d7
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java
@@ -0,0 +1,106 @@
+package org.briarproject.briar.android.contact.add.nearby;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+
+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.ActivityComponent;
+import org.briarproject.briar.android.fragment.BaseFragment;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.view.View.FOCUS_DOWN;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class AddNearbyContactIntroFragment extends BaseFragment {
+
+ public static final String TAG =
+ AddNearbyContactIntroFragment.class.getName();
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private AddNearbyContactViewModel viewModel;
+ private AddNearbyContactPermissionManager permissionManager;
+
+ private ScrollView scrollView;
+
+ private final ActivityResultLauncher permissionLauncher =
+ registerForActivityResult(new RequestMultiplePermissions(), r -> {
+ permissionManager.onRequestPermissionResult(r);
+ if (permissionManager.checkPermissions()) {
+ viewModel.showQrCodeFragmentIfAllowed();
+ }
+ });
+
+ public static AddNearbyContactIntroFragment newInstance() {
+ Bundle args = new Bundle();
+ AddNearbyContactIntroFragment
+ fragment = new AddNearbyContactIntroFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
+ .get(AddNearbyContactViewModel.class);
+ permissionManager = new AddNearbyContactPermissionManager(
+ requireActivity(), permissionLauncher::launch,
+ viewModel.isBluetoothSupported());
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+
+ View v = inflater.inflate(R.layout.fragment_keyagreement_id, container,
+ false);
+ scrollView = v.findViewById(R.id.scrollView);
+ View button = v.findViewById(R.id.continueButton);
+ button.setOnClickListener(view -> {
+ viewModel.onContinueClicked();
+ if (permissionManager.checkPermissions()) {
+ viewModel.showQrCodeFragmentIfAllowed();
+ }
+ });
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // We don't do this in AddNearbyContactFragment#onDestroy()
+ // because it gets called when creating AddNearbyContactFragment
+ // in landscape orientation to force portrait orientation.
+ viewModel.stopListening();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Permissions may have been granted manually while we were stopped
+ permissionManager.resetPermissions();
+ scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeModule.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactModule.java
similarity index 55%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeModule.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactModule.java
index 05a539d5c..79dfaa69c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeModule.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactModule.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
@@ -8,12 +8,12 @@ import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
-public abstract class ContactExchangeModule {
+public abstract class AddNearbyContactModule {
@Binds
@IntoMap
- @ViewModelKey(ContactExchangeViewModel.class)
+ @ViewModelKey(AddNearbyContactViewModel.class)
abstract ViewModel bindContactExchangeViewModel(
- ContactExchangeViewModel contactExchangeViewModel);
+ AddNearbyContactViewModel addNearbyContactViewModel);
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
new file mode 100644
index 000000000..b0bc75fbe
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
@@ -0,0 +1,201 @@
+package org.briarproject.briar.android.contact.add.nearby;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.location.LocationManager;
+import android.widget.Toast;
+
+import org.briarproject.briar.R;
+
+import java.util.Map;
+
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.util.Consumer;
+import androidx.fragment.app.FragmentActivity;
+
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.CAMERA;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS;
+import static android.widget.Toast.LENGTH_LONG;
+import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
+import static androidx.core.content.ContextCompat.checkSelfPermission;
+import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
+
+class AddNearbyContactPermissionManager {
+
+ private enum Permission {
+ UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
+ }
+
+ private Permission cameraPermission = Permission.UNKNOWN;
+ private Permission locationPermission = Permission.UNKNOWN;
+
+ private final FragmentActivity ctx;
+ private final Consumer requestPermissions;
+ private final boolean isBluetoothSupported;
+
+ AddNearbyContactPermissionManager(FragmentActivity ctx,
+ Consumer requestPermissions,
+ boolean isBluetoothSupported) {
+ this.ctx = ctx;
+ this.requestPermissions = requestPermissions;
+ this.isBluetoothSupported = isBluetoothSupported;
+ }
+
+ void resetPermissions() {
+ cameraPermission = Permission.UNKNOWN;
+ 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;
+ return checkSelfPermission(ctx, CAMERA) == ok &&
+ (SDK_INT < 23 ||
+ checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok ||
+ !isBluetoothSupported);
+ }
+
+ private boolean areEssentialPermissionsGranted() {
+ return cameraPermission == Permission.GRANTED &&
+ (SDK_INT < 23 || locationPermission == Permission.GRANTED ||
+ !isBluetoothSupported);
+ }
+
+ boolean checkPermissions() {
+ boolean locationEnabled = isLocationEnabled(ctx);
+ if (locationEnabled && areEssentialPermissionsGranted()) return true;
+ // If an essential permission has been permanently denied, ask the
+ // user to change the setting
+ if (cameraPermission == Permission.PERMANENTLY_DENIED) {
+ showDenialDialog(R.string.permission_camera_title,
+ R.string.permission_camera_denied_body);
+ return false;
+ }
+ if (isBluetoothSupported &&
+ locationPermission == Permission.PERMANENTLY_DENIED) {
+ showDenialDialog(R.string.permission_location_title,
+ R.string.permission_location_denied_body);
+ return false;
+ }
+ // Should we show the rationale for one or both permissions?
+ if (cameraPermission == Permission.SHOW_RATIONALE &&
+ locationPermission == Permission.SHOW_RATIONALE) {
+ showRationale(R.string.permission_camera_location_title,
+ R.string.permission_camera_location_request_body);
+ } else if (cameraPermission == Permission.SHOW_RATIONALE) {
+ showRationale(R.string.permission_camera_title,
+ R.string.permission_camera_request_body);
+ } else if (locationPermission == Permission.SHOW_RATIONALE) {
+ showRationale(R.string.permission_location_title,
+ R.string.permission_location_request_body);
+ } else if (isLocationEnabled(ctx)) {
+ requestPermissions();
+ } else {
+ showLocationDialog(ctx);
+ }
+ return false;
+ }
+
+ private void showDenialDialog(@StringRes int title, @StringRes int body) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
+ builder.setTitle(title);
+ builder.setMessage(body);
+ builder.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx));
+ builder.setNegativeButton(R.string.cancel,
+ (dialog, which) -> ctx.supportFinishAfterTransition());
+ builder.show();
+ }
+
+ private void showRationale(@StringRes int title, @StringRes int body) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
+ builder.setTitle(title);
+ builder.setMessage(body);
+ builder.setNeutralButton(R.string.continue_button,
+ (dialog, which) -> requestPermissions());
+ builder.show();
+ }
+
+ private 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) {
+ permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION};
+ } else {
+ permissions = new String[] {CAMERA};
+ }
+ requestPermissions.accept(permissions);
+ }
+
+ void onRequestPermissionResult(Map result) {
+ if (gotPermission(CAMERA, result)) {
+ cameraPermission = Permission.GRANTED;
+ } else if (shouldShowRationale(CAMERA)) {
+ cameraPermission = Permission.SHOW_RATIONALE;
+ } else {
+ cameraPermission = Permission.PERMANENTLY_DENIED;
+ }
+ if (isBluetoothSupported) {
+ if (gotPermission(ACCESS_FINE_LOCATION, result)) {
+ locationPermission = Permission.GRANTED;
+ } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
+ locationPermission = Permission.SHOW_RATIONALE;
+ } else {
+ locationPermission = Permission.PERMANENTLY_DENIED;
+ }
+ }
+ }
+
+ private boolean gotPermission(String permission,
+ Map result) {
+ Boolean permissionResult = result.get(permission);
+ return permissionResult == null ?
+ isGranted(permission) : permissionResult;
+ }
+
+ private boolean isGranted(String permission) {
+ return checkSelfPermission(ctx, permission) == PERMISSION_GRANTED;
+ }
+
+ private boolean shouldShowRationale(String permission) {
+ return shouldShowRequestPermissionRationale(ctx, permission);
+ }
+
+}
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
new file mode 100644
index 000000000..d79ef6b1b
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java
@@ -0,0 +1,517 @@
+package org.briarproject.briar.android.contact.add.nearby;
+
+import android.app.Application;
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.util.DisplayMetrics;
+import android.widget.Toast;
+
+import com.google.zxing.Result;
+
+import org.briarproject.bramble.api.UnsupportedVersionException;
+import org.briarproject.bramble.api.connection.ConnectionManager;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactExchangeManager;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.ContactExistsException;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
+import org.briarproject.bramble.api.keyagreement.Payload;
+import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
+import org.briarproject.bramble.api.keyagreement.PayloadParser;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.BluetoothConstants;
+import org.briarproject.bramble.api.plugin.LanTcpConstants;
+import org.briarproject.bramble.api.plugin.Plugin;
+import org.briarproject.bramble.api.plugin.Plugin.State;
+import org.briarproject.bramble.api.plugin.PluginManager;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
+import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
+import org.briarproject.bramble.api.system.AndroidExecutor;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeFinished;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult.Error;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult.Success;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeStarted;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementListening;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementStarted;
+import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementWaiting;
+import org.briarproject.briar.android.viewmodel.LiveEvent;
+import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
+import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+import static android.widget.Toast.LENGTH_LONG;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
+import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
+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;
+
+@NotNullByDefault
+class AddNearbyContactViewModel extends AndroidViewModel
+ implements EventListener, QrCodeDecoder.ResultCallback {
+
+ private static final Logger LOG =
+ getLogger(AddNearbyContactViewModel.class.getName());
+
+ enum BluetoothDecision {
+ /**
+ * We haven't asked the user about Bluetooth discoverability.
+ */
+ UNKNOWN,
+
+ /**
+ * The device doesn't have a Bluetooth adapter.
+ */
+ NO_ADAPTER,
+
+ /**
+ * We're waiting for the user to accept or refuse discoverability.
+ */
+ WAITING,
+
+ /**
+ * The user has accepted discoverability.
+ */
+ ACCEPTED,
+
+ /**
+ * The user has refused discoverability.
+ */
+ REFUSED
+ }
+
+ @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
+ private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+ private final EventBus eventBus;
+ private final AndroidExecutor androidExecutor;
+ private final Executor ioExecutor;
+ private final PluginManager pluginManager;
+ private final PayloadEncoder payloadEncoder;
+ private final PayloadParser payloadParser;
+ private final Provider keyAgreementTaskProvider;
+ private final ContactExchangeManager contactExchangeManager;
+ private final ConnectionManager connectionManager;
+
+ private final MutableLiveEvent requestBluetoothDiscoverable =
+ new MutableLiveEvent<>();
+ private final MutableLiveEvent showQrCodeFragment =
+ new MutableLiveEvent<>();
+ private final MutableLiveData state =
+ new MutableLiveData<>();
+
+ private final QrCodeDecoder qrCodeDecoder;
+ private final BroadcastReceiver bluetoothReceiver =
+ new BluetoothStateReceiver();
+
+ @Nullable
+ private final BluetoothAdapter bt;
+ @Nullable
+ private final Plugin wifiPlugin, bluetoothPlugin;
+
+ // UiThread
+ private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
+
+ private boolean wasContinueClicked = false;
+
+ /**
+ * Records whether we've enabled the wifi plugin so we don't enable it more
+ * than once.
+ */
+ private boolean hasEnabledWifi = false;
+
+ /**
+ * Records whether we've enabled the Bluetooth plugin so we don't enable it
+ * more than once.
+ */
+ private boolean hasEnabledBluetooth = false;
+
+ @Nullable
+ private volatile KeyAgreementTask task;
+ private volatile boolean gotLocalPayload = false, gotRemotePayload = false;
+
+ @Inject
+ AddNearbyContactViewModel(Application app,
+ EventBus eventBus,
+ AndroidExecutor androidExecutor,
+ @IoExecutor Executor ioExecutor,
+ PluginManager pluginManager,
+ PayloadEncoder payloadEncoder,
+ PayloadParser payloadParser,
+ Provider keyAgreementTaskProvider,
+ ContactExchangeManager contactExchangeManager,
+ ConnectionManager connectionManager) {
+ super(app);
+ this.eventBus = eventBus;
+ this.androidExecutor = androidExecutor;
+ this.ioExecutor = ioExecutor;
+ this.pluginManager = pluginManager;
+ this.payloadEncoder = payloadEncoder;
+ this.payloadParser = payloadParser;
+ this.keyAgreementTaskProvider = keyAgreementTaskProvider;
+ this.contactExchangeManager = contactExchangeManager;
+ this.connectionManager = connectionManager;
+ bt = BluetoothAdapter.getDefaultAdapter();
+ wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
+ bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
+ qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
+ eventBus.addListener(this);
+ IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
+ getApplication().registerReceiver(bluetoothReceiver, filter);
+ }
+
+ @Override
+ protected void onCleared() {
+ super.onCleared();
+ getApplication().unregisterReceiver(bluetoothReceiver);
+ eventBus.removeListener(this);
+ stopListening();
+ }
+
+ @UiThread
+ void onContinueClicked() {
+ if (bluetoothDecision == REFUSED) {
+ bluetoothDecision = UNKNOWN; // Ask again
+ }
+ wasContinueClicked = true;
+ }
+
+ @UiThread
+ boolean isBluetoothSupported() {
+ return bt != null && bluetoothPlugin != null;
+ }
+
+ @UiThread
+ private boolean isWifiReady() {
+ if (wifiPlugin == null) return true; // Continue without wifi
+ State state = wifiPlugin.getState();
+ // Wait for plugin to become enabled
+ return state == ACTIVE || state == INACTIVE;
+ }
+
+ @UiThread
+ private boolean isBluetoothReady() {
+ if (bt == null || bluetoothPlugin == null) {
+ // Continue without Bluetooth
+ return true;
+ }
+ if (bluetoothDecision == BluetoothDecision.UNKNOWN ||
+ bluetoothDecision == BluetoothDecision.WAITING ||
+ bluetoothDecision == BluetoothDecision.REFUSED) {
+ // Wait for user to accept
+ return false;
+ }
+ if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ // Wait for adapter to become discoverable
+ return false;
+ }
+ // Wait for plugin to become active
+ return bluetoothPlugin.getState() == ACTIVE;
+ }
+
+ @UiThread
+ private void enableWifiIfWeShould() {
+ if (hasEnabledWifi) return;
+ if (wifiPlugin == null) return;
+ State state = wifiPlugin.getState();
+ if (state == STARTING_STOPPING || state == DISABLED) {
+ LOG.info("Enabling wifi plugin");
+ hasEnabledWifi = true;
+ pluginManager.setPluginEnabled(LanTcpConstants.ID, true);
+ }
+ }
+
+ @UiThread
+ private void enableBluetoothIfWeShould() {
+ if (bluetoothDecision != BluetoothDecision.ACCEPTED) return;
+ if (hasEnabledBluetooth) return;
+ if (bluetoothPlugin == null || !isBluetoothSupported()) return;
+ State state = bluetoothPlugin.getState();
+ if (state == STARTING_STOPPING || state == DISABLED) {
+ LOG.info("Enabling Bluetooth plugin");
+ hasEnabledBluetooth = true;
+ pluginManager.setPluginEnabled(BluetoothConstants.ID, true);
+ }
+ }
+
+ @UiThread
+ private void startAddingContact() {
+ // If we return to the intro fragment, the continue button needs to be
+ // clicked again before showing the QR code fragment
+ wasContinueClicked = false;
+ // If we return to the intro fragment, ask for Bluetooth
+ // discoverability again before showing the QR code fragment
+ bluetoothDecision = UNKNOWN;
+ // If we return to the intro fragment, we may need to enable wifi and
+ // Bluetooth again
+ hasEnabledWifi = false;
+ hasEnabledBluetooth = false;
+ // reset state, so we don't show an old QR code again
+ state.setValue(null);
+ resetPayloadFlags();
+ // start to listen with a KeyAgreementTask
+ startListening();
+ showQrCodeFragment.setEvent(true);
+ }
+
+ /**
+ * Call this once Bluetooth and Wi-Fi are ready to be used.
+ * It is possible to call this more than once over the ViewModel's lifetime.
+ */
+ @UiThread
+ private void startListening() {
+ KeyAgreementTask oldTask = task;
+ KeyAgreementTask newTask = keyAgreementTaskProvider.get();
+ task = newTask;
+ ioExecutor.execute(() -> {
+ if (oldTask != null) oldTask.stopListening();
+ newTask.listen();
+ });
+ }
+
+ @UiThread
+ void stopListening() {
+ KeyAgreementTask oldTask = task;
+ ioExecutor.execute(() -> {
+ if (oldTask != null) {
+ oldTask.stopListening();
+ task = null;
+ }
+ });
+ }
+
+ @Override
+ public void eventOccurred(Event e) {
+ if (e instanceof TransportStateEvent) {
+ TransportStateEvent t = (TransportStateEvent) e;
+ if (t.getTransportId().equals(BluetoothConstants.ID)) {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("Bluetooth state changed to " + t.getState());
+ }
+ showQrCodeFragmentIfAllowed();
+ } else if (t.getTransportId().equals(LanTcpConstants.ID)) {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("Wifi state changed to " + t.getState());
+ }
+ showQrCodeFragmentIfAllowed();
+ }
+ } else if (e instanceof KeyAgreementListeningEvent) {
+ LOG.info("KeyAgreementListeningEvent received");
+ KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
+ onLocalPayloadReceived(event.getLocalPayload());
+ } else if (e instanceof KeyAgreementWaitingEvent) {
+ LOG.info("KeyAgreementWaitingEvent received");
+ state.setValue(new KeyAgreementWaiting());
+ } else if (e instanceof KeyAgreementStartedEvent) {
+ LOG.info("KeyAgreementStartedEvent received");
+ state.setValue(new KeyAgreementStarted());
+ } else if (e instanceof KeyAgreementFinishedEvent) {
+ LOG.info("KeyAgreementFinishedEvent received");
+ KeyAgreementResult result =
+ ((KeyAgreementFinishedEvent) e).getResult();
+ startContactExchange(result);
+ state.setValue(new ContactExchangeStarted());
+ } else if (e instanceof KeyAgreementAbortedEvent) {
+ LOG.info("KeyAgreementAbortedEvent received");
+ resetPayloadFlags();
+ state.setValue(new AddContactState.Failed());
+ } else if (e instanceof KeyAgreementFailedEvent) {
+ LOG.info("KeyAgreementFailedEvent received");
+ resetPayloadFlags();
+ state.setValue(new AddContactState.Failed());
+ }
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ @UiThread
+ void showQrCodeFragmentIfAllowed() {
+ boolean permissionsGranted = areEssentialPermissionsGranted(
+ getApplication(), isBluetoothSupported());
+ boolean locationEnabled = isLocationEnabled(getApplication());
+ if (wasContinueClicked && permissionsGranted && locationEnabled) {
+ if (isWifiReady() && isBluetoothReady()) {
+ LOG.info("Wifi and Bluetooth are ready");
+ startAddingContact();
+ } else {
+ enableWifiIfWeShould();
+ if (bluetoothDecision == UNKNOWN) {
+ if (isBluetoothSupported()) {
+ requestBluetoothDiscoverable.setEvent(true);
+ } else {
+ bluetoothDecision = NO_ADAPTER;
+ }
+ } else if (bluetoothDecision == REFUSED) {
+ // Ask again when the user clicks "continue"
+ } else {
+ enableBluetoothIfWeShould();
+ }
+ }
+ }
+ }
+
+ /**
+ * This sets the QR code by setting the state to KeyAgreementListening.
+ */
+ private void onLocalPayloadReceived(Payload localPayload) {
+ if (gotLocalPayload) return;
+ DisplayMetrics dm = getApplication().getResources().getDisplayMetrics();
+ ioExecutor.execute(() -> {
+ byte[] payloadBytes = payloadEncoder.encode(localPayload);
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("Local payload is " + payloadBytes.length
+ + " bytes");
+ }
+ // Use ISO 8859-1 to encode bytes directly as a string
+ String content = new String(payloadBytes, ISO_8859_1);
+ Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
+ gotLocalPayload = true;
+ state.postValue(new KeyAgreementListening(qrCode));
+ });
+ }
+
+ @Override
+ @IoExecutor
+ public void onQrCodeDecoded(Result result) {
+ LOG.info("Got result from decoder");
+ KeyAgreementTask currentTask = task;
+ // Ignore results until the KeyAgreementTask is ready
+ if (!gotLocalPayload || gotRemotePayload || currentTask == null) return;
+ try {
+ byte[] payloadBytes = result.getText().getBytes(ISO_8859_1);
+ if (LOG.isLoggable(INFO))
+ LOG.info("Remote payload is " + payloadBytes.length + " bytes");
+ Payload remotePayload = payloadParser.parse(payloadBytes);
+ gotRemotePayload = true;
+ currentTask.connectAndRunProtocol(remotePayload);
+ state.postValue(new AddContactState.QrCodeScanned());
+ } catch (UnsupportedVersionException e) {
+ resetPayloadFlags();
+ state.postValue(new AddContactState.Failed(e.isTooOld()));
+ } catch (IOException | IllegalArgumentException e) {
+ LOG.log(WARNING, "QR Code Invalid", e);
+ androidExecutor.runOnUiThread(() -> Toast.makeText(getApplication(),
+ R.string.qr_code_invalid, LENGTH_LONG).show());
+ resetPayloadFlags();
+ state.postValue(new AddContactState.Failed());
+ }
+ }
+
+ private void resetPayloadFlags() {
+ gotRemotePayload = false;
+ gotLocalPayload = false;
+ }
+
+ @UiThread
+ private void startContactExchange(KeyAgreementResult result) {
+ TransportId t = result.getTransportId();
+ DuplexTransportConnection conn = result.getConnection();
+ SecretKey masterKey = result.getMasterKey();
+ boolean alice = result.wasAlice();
+ ioExecutor.execute(() -> {
+ try {
+ Contact contact = contactExchangeManager.exchangeContacts(conn,
+ masterKey, alice, true);
+ // Reuse the connection as a transport connection
+ connectionManager
+ .manageOutgoingConnection(contact.getId(), t, conn);
+ Success success = new Success(contact.getAuthor());
+ state.postValue(new ContactExchangeFinished(success));
+ } catch (ContactExistsException e) {
+ tryToClose(conn);
+ Error error = new Error(e.getRemoteAuthor());
+ state.postValue(new ContactExchangeFinished(error));
+ } catch (DbException | IOException e) {
+ tryToClose(conn);
+ logException(LOG, WARNING, e);
+ Error error = new Error(null);
+ state.postValue(new ContactExchangeFinished(error));
+ }
+ });
+ }
+
+ private class BluetoothStateReceiver extends BroadcastReceiver {
+ @UiThread
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, -1);
+ LOG.info("Bluetooth scan mode changed: " + scanMode);
+ showQrCodeFragmentIfAllowed();
+ }
+ }
+
+ private void tryToClose(DuplexTransportConnection conn) {
+ try {
+ conn.getReader().dispose(true, true);
+ conn.getWriter().dispose(true);
+ } catch (IOException e) {
+ logException(LOG, WARNING, e);
+ }
+ }
+
+ @UiThread
+ void setBluetoothDecision(BluetoothDecision decision) {
+ bluetoothDecision = decision;
+ showQrCodeFragmentIfAllowed();
+ }
+
+ QrCodeDecoder getQrCodeDecoder() {
+ return qrCodeDecoder;
+ }
+
+ LiveEvent getRequestBluetoothDiscoverable() {
+ return requestBluetoothDiscoverable;
+ }
+
+ LiveEvent getShowQrCodeFragment() {
+ return showQrCodeFragment;
+ }
+
+ /**
+ * This LiveData will be null initially.
+ */
+ LiveData getState() {
+ return state;
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraException.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/CameraException.java
similarity index 76%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraException.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/CameraException.java
index 0bff14ed7..04f7a0612 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraException.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/CameraException.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import java.io.IOException;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/CameraView.java
similarity index 98%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/CameraView.java
index f21f4bfb8..7a5720459 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/CameraView.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import android.content.Context;
import android.hardware.Camera;
@@ -40,7 +40,6 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
-@SuppressWarnings("deprecation")
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@@ -126,8 +125,14 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
throw new CameraException(e);
}
setDisplayOrientation(getScreenRotationDegrees());
+ if (camera == null) throw new CameraException("No camera found");
// Use barcode scene mode if it's available
- Parameters params = camera.getParameters();
+ Parameters params;
+ try {
+ params = camera.getParameters();
+ } catch (RuntimeException e) {
+ throw new CameraException(e);
+ }
params = setSceneMode(camera, params);
if (SCENE_MODE_BARCODE.equals(params.getSceneMode())) {
// If the scene mode enabled the flash, try to disable it
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/PreviewConsumer.java
similarity index 75%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/PreviewConsumer.java
index bea7b65b8..f94043255 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/PreviewConsumer.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import android.hardware.Camera;
@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import androidx.annotation.UiThread;
-@SuppressWarnings("deprecation")
@NotNullByDefault
interface PreviewConsumer {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java
similarity index 78%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java
index 91f6f9dda..bbb9dc875 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java
@@ -1,10 +1,9 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
-import android.os.AsyncTask;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
@@ -15,10 +14,13 @@ import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.bramble.api.system.AndroidExecutor;
+import java.util.concurrent.Executor;
import java.util.logging.Logger;
import androidx.annotation.UiThread;
@@ -26,22 +28,26 @@ import androidx.annotation.UiThread;
import static com.google.zxing.DecodeHintType.CHARACTER_SET;
import static java.util.Collections.singletonMap;
import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
-@SuppressWarnings("deprecation")
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
- private static final Logger LOG =
- Logger.getLogger(QrCodeDecoder.class.getName());
+ private static final Logger LOG = getLogger(QrCodeDecoder.class.getName());
+ private final AndroidExecutor androidExecutor;
+ private final Executor ioExecutor;
private final Reader reader = new QRCodeReader();
private final ResultCallback callback;
private Camera camera = null;
private int cameraIndex = 0;
- QrCodeDecoder(ResultCallback callback) {
+ QrCodeDecoder(AndroidExecutor androidExecutor,
+ @IoExecutor Executor ioExecutor, ResultCallback callback) {
+ this.androidExecutor = androidExecutor;
+ this.ioExecutor = ioExecutor;
this.callback = callback;
}
@@ -74,8 +80,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
if (data.length == size.width * size.height * 3 / 2) {
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(cameraIndex, info);
- new DecoderTask(data, size.width, size.height,
- info.orientation).execute();
+ decode(data, size.width, size.height, info.orientation);
} else {
// Camera parameters have changed - ask for a new preview
LOG.info("Preview size does not match camera parameters");
@@ -89,43 +94,23 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
}
}
- private class DecoderTask extends AsyncTask {
-
- private final byte[] data;
- private final int width, height, orientation;
-
- private DecoderTask(byte[] data, int width, int height,
- int orientation) {
- this.data = data;
- this.width = width;
- this.height = height;
- this.orientation = orientation;
- }
-
- @Override
- protected Void doInBackground(Void... params) {
+ private void decode(byte[] data, int width, int height, int orientation) {
+ ioExecutor.execute(() -> {
BinaryBitmap bitmap = binarize(data, width, height, orientation);
Result result;
try {
result = reader.decode(bitmap,
singletonMap(CHARACTER_SET, "ISO8859_1"));
+ callback.onQrCodeDecoded(result);
} catch (ReaderException e) {
// No barcode found
- return null;
} catch (RuntimeException e) {
LOG.warning("Invalid preview frame");
- return null;
} finally {
reader.reset();
+ androidExecutor.runOnUiThread(this::askForPreviewFrame);
}
- callback.handleResult(result);
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- askForPreviewFrame();
- }
+ });
}
private static BinaryBitmap binarize(byte[] data, int width, int height,
@@ -143,7 +128,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@NotNullByDefault
interface ResultCallback {
-
- void handleResult(Result result);
+ @IoExecutor
+ void onQrCodeDecoded(Result result);
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeUtils.java
similarity index 89%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java
rename to briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeUtils.java
index 617f84b99..26f2bf050 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeUtils.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.keyagreement;
+package org.briarproject.briar.android.contact.add.nearby;
import android.graphics.Bitmap;
import android.util.DisplayMetrics;
@@ -18,13 +18,13 @@ import static android.graphics.Color.BLACK;
import static android.graphics.Color.WHITE;
import static com.google.zxing.BarcodeFormat.QR_CODE;
import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class QrCodeUtils {
- private static final Logger LOG =
- Logger.getLogger(QrCodeUtils.class.getName());
+ private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
@Nullable
static Bitmap createQrCode(DisplayMetrics dm, String input) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java
deleted file mode 100644
index 222fba77b..000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package org.briarproject.briar.android.keyagreement;
-
-import android.os.Bundle;
-import android.widget.Toast;
-
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
-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.ActivityComponent;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-
-import androidx.annotation.UiThread;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.lifecycle.ViewModelProviders;
-
-import static android.widget.Toast.LENGTH_LONG;
-import static java.util.Objects.requireNonNull;
-
-@MethodsNotNullByDefault
-@ParametersNotNullByDefault
-public class ContactExchangeActivity extends KeyAgreementActivity {
-
- @Inject
- ViewModelProvider.Factory viewModelFactory;
-
- private ContactExchangeViewModel viewModel;
-
- @Override
- public void injectActivity(ActivityComponent component) {
- component.inject(this);
- }
-
- @Override
- public void onCreate(@Nullable Bundle state) {
- super.onCreate(state);
- requireNonNull(getSupportActionBar())
- .setTitle(R.string.add_contact_title);
- viewModel = ViewModelProviders.of(this, viewModelFactory)
- .get(ContactExchangeViewModel.class);
- }
-
- private void startContactExchange(KeyAgreementResult result) {
- viewModel.getSucceeded().observe(this, succeeded -> {
- if (succeeded == null) return;
- if (succeeded) {
- Author remote = requireNonNull(viewModel.getRemoteAuthor());
- contactExchangeSucceeded(remote);
- } else {
- Author duplicate = viewModel.getDuplicateAuthor();
- if (duplicate == null) contactExchangeFailed();
- else duplicateContact(duplicate);
- }
- });
- viewModel.startContactExchange(result.getTransportId(),
- result.getConnection(), result.getMasterKey(),
- result.wasAlice());
- }
-
- @UiThread
- private void contactExchangeSucceeded(Author remoteAuthor) {
- String contactName = remoteAuthor.getName();
- String text = getString(R.string.contact_added_toast, contactName);
- Toast.makeText(this, text, LENGTH_LONG).show();
- supportFinishAfterTransition();
- }
-
- @UiThread
- private void duplicateContact(Author remoteAuthor) {
- String contactName = remoteAuthor.getName();
- String format = getString(R.string.contact_already_exists);
- String text = String.format(format, contactName);
- Toast.makeText(this, text, LENGTH_LONG).show();
- finish();
- }
-
- @UiThread
- private void contactExchangeFailed() {
- showErrorFragment();
- }
-
- @UiThread
- @Override
- public void keyAgreementFailed() {
- showErrorFragment();
- }
-
- @UiThread
- @Override
- public String keyAgreementWaiting() {
- return getString(R.string.waiting_for_contact_to_scan);
- }
-
- @UiThread
- @Override
- public String keyAgreementStarted() {
- return getString(R.string.authenticating_with_device);
- }
-
- @UiThread
- @Override
- public void keyAgreementAborted(boolean remoteAborted) {
- showErrorFragment();
- }
-
- @UiThread
- @Override
- public String keyAgreementFinished(KeyAgreementResult result) {
- startContactExchange(result);
- return getString(R.string.exchanging_contact_details);
- }
-
- private void showErrorFragment() {
- showNextFragment(new ContactExchangeErrorFragment());
- }
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java
deleted file mode 100644
index 054121cac..000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.briarproject.briar.android.keyagreement;
-
-import android.app.Application;
-
-import org.briarproject.bramble.api.connection.ConnectionManager;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactExchangeManager;
-import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.api.db.ContactExistsException;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.plugin.TransportId;
-import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
-
-import java.io.IOException;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-
-import androidx.annotation.UiThread;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-
-import static java.util.logging.Level.WARNING;
-import static java.util.logging.Logger.getLogger;
-import static org.briarproject.bramble.util.LogUtils.logException;
-
-@NotNullByDefault
-class ContactExchangeViewModel extends AndroidViewModel {
-
- private static final Logger LOG =
- getLogger(ContactExchangeViewModel.class.getName());
-
- private final Executor ioExecutor;
- private final ContactExchangeManager contactExchangeManager;
- private final ConnectionManager connectionManager;
- private final MutableLiveData succeeded = new MutableLiveData<>();
-
- @Nullable
- private volatile Author remoteAuthor, duplicateAuthor;
-
- @Inject
- ContactExchangeViewModel(Application app, @IoExecutor Executor ioExecutor,
- ContactExchangeManager contactExchangeManager,
- ConnectionManager connectionManager) {
- super(app);
- this.ioExecutor = ioExecutor;
- this.contactExchangeManager = contactExchangeManager;
- this.connectionManager = connectionManager;
- }
-
- @UiThread
- void startContactExchange(TransportId t, DuplexTransportConnection conn,
- SecretKey masterKey, boolean alice) {
- ioExecutor.execute(() -> {
- try {
- Contact contact = contactExchangeManager.exchangeContacts(conn,
- masterKey, alice, true);
- // Reuse the connection as a transport connection
- connectionManager.manageOutgoingConnection(contact.getId(),
- t, conn);
- remoteAuthor = contact.getAuthor();
- succeeded.postValue(true);
- } catch (ContactExistsException e) {
- tryToClose(conn);
- duplicateAuthor = e.getRemoteAuthor();
- succeeded.postValue(false);
- } catch (DbException | IOException e) {
- tryToClose(conn);
- logException(LOG, WARNING, e);
- succeeded.postValue(false);
- }
- });
- }
-
- private void tryToClose(DuplexTransportConnection conn) {
- try {
- conn.getReader().dispose(true, true);
- conn.getWriter().dispose(true);
- } catch (IOException e) {
- logException(LOG, WARNING, e);
- }
- }
-
- @UiThread
- @Nullable
- Author getRemoteAuthor() {
- return remoteAuthor;
- }
-
- @UiThread
- @Nullable
- Author getDuplicateAuthor() {
- return duplicateAuthor;
- }
-
- LiveData getSucceeded() {
- return succeeded;
- }
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/IntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/IntroFragment.java
deleted file mode 100644
index 6ee1f45c2..000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/IntroFragment.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.briarproject.briar.android.keyagreement;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ScrollView;
-
-import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
-import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
-import org.briarproject.briar.R;
-import org.briarproject.briar.android.fragment.BaseFragment;
-
-import javax.annotation.Nullable;
-
-import static android.view.View.FOCUS_DOWN;
-
-@MethodsNotNullByDefault
-@ParametersNotNullByDefault
-public class IntroFragment extends BaseFragment {
-
- interface IntroScreenSeenListener {
- void showNextScreen();
- }
-
- public static final String TAG = IntroFragment.class.getName();
-
- private IntroScreenSeenListener screenSeenListener;
- private ScrollView scrollView;
-
- public static IntroFragment newInstance() {
-
- Bundle args = new Bundle();
-
- IntroFragment fragment = new IntroFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- screenSeenListener = (IntroScreenSeenListener) context;
- }
-
- @Override
- public String getUniqueTag() {
- return TAG;
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
-
- View v = inflater.inflate(R.layout.fragment_keyagreement_id, container,
- false);
- scrollView = v.findViewById(R.id.scrollView);
- View button = v.findViewById(R.id.continueButton);
- button.setOnClickListener(view -> screenSeenListener.showNextScreen());
- return v;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
- }
-
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java
deleted file mode 100644
index 19ea51cbf..000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java
+++ /dev/null
@@ -1,484 +0,0 @@
-package org.briarproject.briar.android.keyagreement;
-
-import android.bluetooth.BluetoothAdapter;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.view.MenuItem;
-
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.event.EventBus;
-import org.briarproject.bramble.api.event.EventListener;
-import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
-import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
-import org.briarproject.bramble.api.plugin.BluetoothConstants;
-import org.briarproject.bramble.api.plugin.LanTcpConstants;
-import org.briarproject.bramble.api.plugin.Plugin;
-import org.briarproject.bramble.api.plugin.Plugin.State;
-import org.briarproject.bramble.api.plugin.PluginManager;
-import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
-import org.briarproject.briar.R;
-import org.briarproject.briar.android.activity.ActivityComponent;
-import org.briarproject.briar.android.activity.BriarActivity;
-import org.briarproject.briar.android.fragment.BaseFragment;
-import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
-import org.briarproject.briar.android.keyagreement.IntroFragment.IntroScreenSeenListener;
-import org.briarproject.briar.android.keyagreement.KeyAgreementFragment.KeyAgreementEventListener;
-
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-
-import androidx.annotation.StringRes;
-import androidx.annotation.UiThread;
-import androidx.appcompat.app.AlertDialog.Builder;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.app.ActivityCompat;
-import androidx.fragment.app.FragmentManager;
-
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
-import static android.Manifest.permission.CAMERA;
-import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
-import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
-import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.os.Build.VERSION.SDK_INT;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Logger.getLogger;
-import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
-import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
-import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
-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.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE;
-import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION;
-import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
-
-@MethodsNotNullByDefault
-@ParametersNotNullByDefault
-public abstract class KeyAgreementActivity extends BriarActivity implements
- BaseFragmentListener, IntroScreenSeenListener,
- KeyAgreementEventListener, EventListener {
-
- private enum BluetoothDecision {
- /**
- * We haven't asked the user about Bluetooth discoverability.
- */
- UNKNOWN,
-
- /**
- * The device doesn't have a Bluetooth adapter.
- */
- NO_ADAPTER,
-
- /**
- * We're waiting for the user to accept or refuse discoverability.
- */
- WAITING,
-
- /**
- * The user has accepted discoverability.
- */
- ACCEPTED,
-
- /**
- * The user has refused discoverability.
- */
- REFUSED
- }
-
- private enum Permission {
- UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
- }
-
- private static final Logger LOG =
- getLogger(KeyAgreementActivity.class.getName());
-
- @Inject
- EventBus eventBus;
-
- @Inject
- PluginManager pluginManager;
-
- /**
- * Set to true in onPostResume() and false in onPause(). This prevents the
- * QR code fragment from being shown if onRequestPermissionsResult() is
- * called while the activity is paused, which could cause a crash due to
- * https://issuetracker.google.com/issues/37067655.
- */
- private boolean isResumed = false;
-
- /**
- * Set to true when the continue button is clicked, and false when the QR
- * code fragment is shown. This prevents the QR code fragment from being
- * shown automatically before the continue button has been clicked.
- */
- private boolean continueClicked = false;
-
- /**
- * Records whether we've enabled the wifi plugin so we don't enable it more
- * than once.
- */
- private boolean hasEnabledWifi = false;
-
- /**
- * Records whether we've enabled the Bluetooth plugin so we don't enable it
- * more than once.
- */
- private boolean hasEnabledBluetooth = false;
-
- private Permission cameraPermission = Permission.UNKNOWN;
- private Permission locationPermission = Permission.UNKNOWN;
- private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
- private BroadcastReceiver bluetoothReceiver = null;
- private Plugin wifiPlugin = null, bluetoothPlugin = null;
- private BluetoothAdapter bt = null;
-
- @Override
- public void injectActivity(ActivityComponent component) {
- component.inject(this);
- }
-
- @Override
- public void onCreate(@Nullable Bundle state) {
- super.onCreate(state);
- setContentView(R.layout.activity_fragment_container_toolbar);
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
- if (state == null) {
- showInitialFragment(IntroFragment.newInstance());
- }
- IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
- bluetoothReceiver = new BluetoothStateReceiver();
- registerReceiver(bluetoothReceiver, filter);
- wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
- bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
- bt = BluetoothAdapter.getDefaultAdapter();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (bluetoothReceiver != null) unregisterReceiver(bluetoothReceiver);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- onBackPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- eventBus.addListener(this);
- // Permissions may have been granted manually while we were stopped
- cameraPermission = Permission.UNKNOWN;
- locationPermission = Permission.UNKNOWN;
- }
-
- @Override
- protected void onPostResume() {
- super.onPostResume();
- isResumed = true;
- // Workaround for
- // https://code.google.com/p/android/issues/detail?id=190966
- showQrCodeFragmentIfAllowed();
- }
-
- @SuppressWarnings("StatementWithEmptyBody")
- private void showQrCodeFragmentIfAllowed() {
- if (isResumed && continueClicked && areEssentialPermissionsGranted()) {
- if (isWifiReady() && isBluetoothReady()) {
- LOG.info("Wifi and Bluetooth are ready");
- showQrCodeFragment();
- } else {
- if (shouldEnableWifi()) {
- LOG.info("Enabling wifi plugin");
- hasEnabledWifi = true;
- pluginManager.setPluginEnabled(LanTcpConstants.ID, true);
- }
- if (bluetoothDecision == BluetoothDecision.UNKNOWN) {
- requestBluetoothDiscoverable();
- } else if (bluetoothDecision == BluetoothDecision.REFUSED) {
- // Ask again when the user clicks "continue"
- } else if (shouldEnableBluetooth()) {
- LOG.info("Enabling Bluetooth plugin");
- hasEnabledBluetooth = true;
- pluginManager.setPluginEnabled(BluetoothConstants.ID, true);
- }
- }
- }
- }
-
- private boolean areEssentialPermissionsGranted() {
- return cameraPermission == Permission.GRANTED &&
- (SDK_INT < 23 || locationPermission == Permission.GRANTED ||
- !isBluetoothSupported());
- }
-
- private boolean isBluetoothSupported() {
- return bt != null && bluetoothPlugin != null;
- }
-
- private boolean isWifiReady() {
- if (wifiPlugin == null) return true; // Continue without wifi
- State state = wifiPlugin.getState();
- // Wait for plugin to become enabled
- return state == ACTIVE || state == INACTIVE;
- }
-
- private boolean isBluetoothReady() {
- if (!isBluetoothSupported()) {
- // Continue without Bluetooth
- return true;
- }
- if (bluetoothDecision == BluetoothDecision.UNKNOWN ||
- bluetoothDecision == BluetoothDecision.WAITING ||
- bluetoothDecision == BluetoothDecision.REFUSED) {
- // Wait for user to accept
- return false;
- }
- if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
- // Wait for adapter to become discoverable
- return false;
- }
- // Wait for plugin to become active
- return bluetoothPlugin.getState() == ACTIVE;
- }
-
- private boolean shouldEnableWifi() {
- if (hasEnabledWifi) return false;
- if (wifiPlugin == null) return false;
- State state = wifiPlugin.getState();
- return state == STARTING_STOPPING || state == DISABLED;
- }
-
- private void requestBluetoothDiscoverable() {
- if (!isBluetoothSupported()) {
- bluetoothDecision = BluetoothDecision.NO_ADAPTER;
- showQrCodeFragmentIfAllowed();
- } else {
- Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
- if (i.resolveActivity(getPackageManager()) != null) {
- LOG.info("Asking for Bluetooth discoverability");
- bluetoothDecision = BluetoothDecision.WAITING;
- startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
- } else {
- bluetoothDecision = BluetoothDecision.NO_ADAPTER;
- showQrCodeFragmentIfAllowed();
- }
- }
- }
-
- private boolean shouldEnableBluetooth() {
- if (bluetoothDecision != BluetoothDecision.ACCEPTED) return false;
- if (hasEnabledBluetooth) return false;
- if (!isBluetoothSupported()) return false;
- State state = bluetoothPlugin.getState();
- return state == STARTING_STOPPING || state == DISABLED;
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- isResumed = false;
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- eventBus.removeListener(this);
- }
-
- @Override
- public void showNextScreen() {
- continueClicked = true;
- if (bluetoothDecision == BluetoothDecision.REFUSED) {
- bluetoothDecision = BluetoothDecision.UNKNOWN; // Ask again
- }
- if (checkPermissions()) showQrCodeFragmentIfAllowed();
- }
-
- @Override
- public void onActivityResult(int request, int result,
- @Nullable Intent data) {
- if (request == REQUEST_BLUETOOTH_DISCOVERABLE) {
- if (result == RESULT_CANCELED) {
- LOG.info("Bluetooth discoverability was refused");
- bluetoothDecision = BluetoothDecision.REFUSED;
- } else {
- LOG.info("Bluetooth discoverability was accepted");
- bluetoothDecision = BluetoothDecision.ACCEPTED;
- }
- showQrCodeFragmentIfAllowed();
- } else super.onActivityResult(request, result, data);
- }
-
- private void showQrCodeFragment() {
- // If we return to the intro fragment, the continue button needs to be
- // clicked again before showing the QR code fragment
- continueClicked = false;
- // If we return to the intro fragment, ask for Bluetooth
- // discoverability again before showing the QR code fragment
- bluetoothDecision = BluetoothDecision.UNKNOWN;
- // If we return to the intro fragment, we may need to enable wifi and
- // Bluetooth again
- hasEnabledWifi = false;
- hasEnabledBluetooth = false;
-
- // FIXME #824
- FragmentManager fm = getSupportFragmentManager();
- if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) {
- BaseFragment f = KeyAgreementFragment.newInstance();
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, f, f.getUniqueTag())
- .addToBackStack(f.getUniqueTag())
- .commit();
- }
- }
-
- private boolean checkPermissions() {
- if (areEssentialPermissionsGranted()) return true;
- // If an essential permission has been permanently denied, ask the
- // user to change the setting
- if (cameraPermission == Permission.PERMANENTLY_DENIED) {
- showDenialDialog(R.string.permission_camera_title,
- R.string.permission_camera_denied_body);
- return false;
- }
- if (isBluetoothSupported() &&
- locationPermission == Permission.PERMANENTLY_DENIED) {
- showDenialDialog(R.string.permission_location_title,
- R.string.permission_location_denied_body);
- return false;
- }
- // Should we show the rationale for one or both permissions?
- if (cameraPermission == Permission.SHOW_RATIONALE &&
- locationPermission == Permission.SHOW_RATIONALE) {
- showRationale(R.string.permission_camera_location_title,
- R.string.permission_camera_location_request_body);
- } else if (cameraPermission == Permission.SHOW_RATIONALE) {
- showRationale(R.string.permission_camera_title,
- R.string.permission_camera_request_body);
- } else if (locationPermission == Permission.SHOW_RATIONALE) {
- showRationale(R.string.permission_location_title,
- R.string.permission_location_request_body);
- } else {
- requestPermissions();
- }
- return false;
- }
-
- private void showDenialDialog(@StringRes int title, @StringRes int body) {
- Builder builder = new Builder(this, R.style.BriarDialogTheme);
- builder.setTitle(title);
- builder.setMessage(body);
- builder.setPositiveButton(R.string.ok, getGoToSettingsListener(this));
- builder.setNegativeButton(R.string.cancel,
- (dialog, which) -> supportFinishAfterTransition());
- builder.show();
- }
-
- private void showRationale(@StringRes int title, @StringRes int body) {
- Builder builder = new Builder(this, R.style.BriarDialogTheme);
- builder.setTitle(title);
- builder.setMessage(body);
- builder.setNeutralButton(R.string.continue_button,
- (dialog, which) -> requestPermissions());
- builder.show();
- }
-
- private void requestPermissions() {
- String[] permissions;
- if (isBluetoothSupported()) {
- permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION};
- } else {
- permissions = new String[] {CAMERA};
- }
- ActivityCompat.requestPermissions(this, permissions,
- REQUEST_PERMISSION_CAMERA_LOCATION);
- }
-
- @Override
- @UiThread
- public void onRequestPermissionsResult(int requestCode,
- String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions,
- grantResults);
- if (requestCode != REQUEST_PERMISSION_CAMERA_LOCATION)
- throw new AssertionError();
- if (gotPermission(CAMERA, permissions, grantResults)) {
- cameraPermission = Permission.GRANTED;
- } else if (shouldShowRationale(CAMERA)) {
- cameraPermission = Permission.SHOW_RATIONALE;
- } else {
- cameraPermission = Permission.PERMANENTLY_DENIED;
- }
- if (isBluetoothSupported()) {
- if (gotPermission(ACCESS_FINE_LOCATION, permissions,
- grantResults)) {
- locationPermission = Permission.GRANTED;
- } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
- locationPermission = Permission.SHOW_RATIONALE;
- } else {
- locationPermission = Permission.PERMANENTLY_DENIED;
- }
- }
- // If a permission dialog has been shown, showing the QR code fragment
- // on this call path would cause a crash due to
- // https://code.google.com/p/android/issues/detail?id=190966.
- // In that case the isResumed flag prevents the fragment from being
- // shown here, and showQrCodeFragmentIfAllowed() will be called again
- // from onPostResume().
- if (checkPermissions()) showQrCodeFragmentIfAllowed();
- }
-
- private boolean gotPermission(String permission, String[] permissions,
- int[] grantResults) {
- for (int i = 0; i < permissions.length; i++) {
- if (permission.equals(permissions[i]))
- return grantResults[i] == PERMISSION_GRANTED;
- }
- return false;
- }
-
- private boolean shouldShowRationale(String permission) {
- return ActivityCompat.shouldShowRequestPermissionRationale(this,
- permission);
- }
-
- @Override
- public void eventOccurred(Event e) {
- if (e instanceof TransportStateEvent) {
- TransportStateEvent t = (TransportStateEvent) e;
- if (t.getTransportId().equals(BluetoothConstants.ID)) {
- if (LOG.isLoggable(INFO)) {
- LOG.info("Bluetooth state changed to " + t.getState());
- }
- showQrCodeFragmentIfAllowed();
- } else if (t.getTransportId().equals(LanTcpConstants.ID)) {
- if (LOG.isLoggable(INFO)) {
- LOG.info("Wifi state changed to " + t.getState());
- }
- showQrCodeFragmentIfAllowed();
- }
- }
- }
-
- private class BluetoothStateReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- LOG.info("Bluetooth scan mode changed");
- showQrCodeFragmentIfAllowed();
- }
- }
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java
deleted file mode 100644
index eb8f1bced..000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java
+++ /dev/null
@@ -1,375 +0,0 @@
-package org.briarproject.briar.android.keyagreement;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.Bundle;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.LinearLayout.LayoutParams;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.google.zxing.Result;
-
-import org.briarproject.bramble.api.UnsupportedVersionException;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.event.EventBus;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
-import org.briarproject.bramble.api.keyagreement.Payload;
-import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
-import org.briarproject.bramble.api.keyagreement.PayloadParser;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
-import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
-import org.briarproject.briar.R;
-import org.briarproject.briar.android.activity.ActivityComponent;
-import org.briarproject.briar.android.fragment.BaseEventFragment;
-import org.briarproject.briar.android.view.QrCodeView;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-import javax.inject.Provider;
-
-import androidx.annotation.UiThread;
-
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.Toast.LENGTH_LONG;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.bramble.util.LogUtils.logException;
-
-@MethodsNotNullByDefault
-@ParametersNotNullByDefault
-public class KeyAgreementFragment extends BaseEventFragment
- implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener {
-
- static final String TAG = KeyAgreementFragment.class.getName();
-
- private static final Logger LOG = Logger.getLogger(TAG);
- @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
- private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
-
- @Inject
- Provider keyAgreementTaskProvider;
- @Inject
- PayloadEncoder payloadEncoder;
- @Inject
- PayloadParser payloadParser;
- @Inject
- @IoExecutor
- Executor ioExecutor;
- @Inject
- EventBus eventBus;
-
- private CameraView cameraView;
- private LinearLayout cameraOverlay;
- private View statusView;
- private QrCodeView qrCodeView;
- private TextView status;
-
- private boolean gotRemotePayload;
- private volatile boolean gotLocalPayload;
- private KeyAgreementTask task;
- private KeyAgreementEventListener listener;
-
- public static KeyAgreementFragment newInstance() {
- Bundle args = new Bundle();
- KeyAgreementFragment fragment = new KeyAgreementFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- listener = (KeyAgreementEventListener) context;
- }
-
- @Override
- public void injectFragment(ActivityComponent component) {
- component.inject(this);
- }
-
- @Override
- public String getUniqueTag() {
- return TAG;
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
- false);
- }
-
- @Override
- public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- cameraView = view.findViewById(R.id.camera_view);
- cameraOverlay = view.findViewById(R.id.camera_overlay);
- statusView = view.findViewById(R.id.status_container);
- status = view.findViewById(R.id.connect_status);
- qrCodeView = view.findViewById(R.id.qr_code_view);
- qrCodeView.setFullscreenListener(this);
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
- cameraView.setPreviewConsumer(new QrCodeDecoder(this));
- }
-
- @Override
- public void onStart() {
- super.onStart();
- try {
- cameraView.start();
- } catch (CameraException e) {
- logCameraExceptionAndFinish(e);
- }
- startListening();
- }
-
- @Override
- public void setFullscreen(boolean fullscreen) {
- LinearLayout.LayoutParams statusParams, qrCodeParams;
- if (fullscreen) {
- // Grow the QR code view to fill its parent
- statusParams = new LayoutParams(0, 0, 0f);
- qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
- } else {
- // Shrink the QR code view to fill half its parent
- if (cameraOverlay.getOrientation() == HORIZONTAL) {
- statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
- qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
- } else {
- statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
- qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
- }
- }
- statusView.setLayoutParams(statusParams);
- qrCodeView.setLayoutParams(qrCodeParams);
- cameraOverlay.invalidate();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- stopListening();
- try {
- cameraView.stop();
- } catch (CameraException e) {
- logCameraExceptionAndFinish(e);
- }
- }
-
- @UiThread
- private void logCameraExceptionAndFinish(CameraException e) {
- logException(LOG, WARNING, e);
- Toast.makeText(getActivity(), R.string.camera_error,
- LENGTH_LONG).show();
- finish();
- }
-
- @UiThread
- private void startListening() {
- KeyAgreementTask oldTask = task;
- KeyAgreementTask newTask = keyAgreementTaskProvider.get();
- task = newTask;
- ioExecutor.execute(() -> {
- if (oldTask != null) oldTask.stopListening();
- newTask.listen();
- });
- }
-
- @UiThread
- private void stopListening() {
- KeyAgreementTask oldTask = task;
- ioExecutor.execute(() -> {
- if (oldTask != null) oldTask.stopListening();
- });
- }
-
- @UiThread
- private void reset() {
- // If we've stopped the camera view, restart it
- if (gotRemotePayload) {
- try {
- cameraView.start();
- } catch (CameraException e) {
- logCameraExceptionAndFinish(e);
- return;
- }
- }
- statusView.setVisibility(INVISIBLE);
- cameraView.setVisibility(VISIBLE);
- gotRemotePayload = false;
- gotLocalPayload = false;
- startListening();
- }
-
- @UiThread
- private void qrCodeScanned(String content) {
- try {
- byte[] payloadBytes = content.getBytes(ISO_8859_1);
- if (LOG.isLoggable(INFO))
- LOG.info("Remote payload is " + payloadBytes.length + " bytes");
- Payload remotePayload = payloadParser.parse(payloadBytes);
- gotRemotePayload = true;
- cameraView.stop();
- cameraView.setVisibility(INVISIBLE);
- statusView.setVisibility(VISIBLE);
- status.setText(R.string.connecting_to_device);
- task.connectAndRunProtocol(remotePayload);
- } catch (UnsupportedVersionException e) {
- reset();
- String msg;
- if (e.isTooOld()) {
- msg = getString(R.string.qr_code_too_old,
- getString(R.string.app_name));
- } else {
- msg = getString(R.string.qr_code_too_new,
- getString(R.string.app_name));
- }
- showNextFragment(ContactExchangeErrorFragment.newInstance(msg));
- } catch (CameraException e) {
- logCameraExceptionAndFinish(e);
- } catch (IOException | IllegalArgumentException e) {
- LOG.log(WARNING, "QR Code Invalid", e);
- reset();
- Toast.makeText(getActivity(), R.string.qr_code_invalid,
- LENGTH_LONG).show();
- }
- }
-
- @Override
- public void eventOccurred(Event e) {
- if (e instanceof KeyAgreementListeningEvent) {
- KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
- gotLocalPayload = true;
- setQrCode(event.getLocalPayload());
- } else if (e instanceof KeyAgreementFailedEvent) {
- keyAgreementFailed();
- } else if (e instanceof KeyAgreementWaitingEvent) {
- keyAgreementWaiting();
- } else if (e instanceof KeyAgreementStartedEvent) {
- keyAgreementStarted();
- } else if (e instanceof KeyAgreementAbortedEvent) {
- KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e;
- keyAgreementAborted(event.didRemoteAbort());
- } else if (e instanceof KeyAgreementFinishedEvent) {
- keyAgreementFinished(((KeyAgreementFinishedEvent) e).getResult());
- }
- }
-
- @UiThread
- private void keyAgreementFailed() {
- reset();
- listener.keyAgreementFailed();
- }
-
- @UiThread
- private void keyAgreementWaiting() {
- status.setText(listener.keyAgreementWaiting());
- }
-
- @UiThread
- private void keyAgreementStarted() {
- qrCodeView.setVisibility(INVISIBLE);
- statusView.setVisibility(VISIBLE);
- status.setText(listener.keyAgreementStarted());
- }
-
- @UiThread
- private void keyAgreementAborted(boolean remoteAborted) {
- reset();
- listener.keyAgreementAborted(remoteAborted);
- }
-
- @UiThread
- private void keyAgreementFinished(KeyAgreementResult result) {
- statusView.setVisibility(VISIBLE);
- status.setText(listener.keyAgreementFinished(result));
- }
-
- private void setQrCode(Payload localPayload) {
- Context context = getContext();
- if (context == null) return;
- DisplayMetrics dm = context.getResources().getDisplayMetrics();
- ioExecutor.execute(() -> {
- byte[] payloadBytes = payloadEncoder.encode(localPayload);
- if (LOG.isLoggable(INFO)) {
- LOG.info("Local payload is " + payloadBytes.length
- + " bytes");
- }
- // Use ISO 8859-1 to encode bytes directly as a string
- String content = new String(payloadBytes, ISO_8859_1);
- Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
- runOnUiThreadUnlessDestroyed(() -> qrCodeView.setQrCode(qrCode));
- });
- }
-
- @Override
- public void handleResult(Result result) {
- runOnUiThreadUnlessDestroyed(() -> {
- LOG.info("Got result from decoder");
- // Ignore results until the KeyAgreementTask is ready
- if (!gotLocalPayload) return;
- if (!gotRemotePayload) qrCodeScanned(result.getText());
- });
- }
-
- @Override
- protected void finish() {
- getActivity().getSupportFragmentManager().popBackStack();
- }
-
- @NotNullByDefault
- interface KeyAgreementEventListener {
-
- @UiThread
- void keyAgreementFailed();
-
- // Should return a string to be displayed as status.
- @UiThread
- @Nullable
- String keyAgreementWaiting();
-
- // Should return a string to be displayed as status.
- @UiThread
- @Nullable
- String keyAgreementStarted();
-
- // Will show an error fragment.
- @UiThread
- void keyAgreementAborted(boolean remoteAborted);
-
- // Should return a string to be displayed as status.
- @UiThread
- @Nullable
- String keyAgreementFinished(KeyAgreementResult result);
- }
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/RequestBluetoothDiscoverable.java b/briar-android/src/main/java/org/briarproject/briar/android/util/RequestBluetoothDiscoverable.java
new file mode 100644
index 000000000..8288aedac
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/RequestBluetoothDiscoverable.java
@@ -0,0 +1,31 @@
+package org.briarproject.briar.android.util;
+
+
+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
+public 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/layout/activity_fragment_container_toolbar.xml b/briar-android/src/main/res/layout/activity_fragment_container_toolbar.xml
index 8718e0b18..bab5a0b80 100644
--- a/briar-android/src/main/res/layout/activity_fragment_container_toolbar.xml
+++ b/briar-android/src/main/res/layout/activity_fragment_container_toolbar.xml
@@ -4,11 +4,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- tools:context=".android.keyagreement.KeyAgreementActivity">
+ tools:context=".android.contact.add.nearby.AddNearbyContactActivity">
-
diff --git a/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml b/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml
index 33227017d..6eb3d992e 100644
--- a/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml
+++ b/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml
@@ -5,7 +5,7 @@
android:layout_height="match_parent"
android:keepScreenOn="true">
-
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index a5887a24a..34314f7fc 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -609,6 +609,9 @@
To scan the QR code, Briar needs access to the camera.\n\nTo discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.
You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access.
You have denied access to your location, but Briar needs this permission to discover Bluetooth devices.\n\nPlease consider granting access.
+ Location setting
+ Your device\'s location setting must be turned on to find other devices via Bluetooth. Please enable location to continue. You can disable it again afterwards.
+ Enable location
QR code
Show QR code fullscreen