mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Merge branch '1872-key-agreement' into 'master'
Finish migrating KeyAgreementActivity to ViewModel Closes #1982 and #1872 See merge request briar/briar!1357
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,6 +18,7 @@ local.properties
|
||||
|
||||
# Android Studio
|
||||
.idea/*
|
||||
!.idea/inspectionProfiles/
|
||||
!.idea/runConfigurations/
|
||||
!.idea/codeStyleSettings.xml
|
||||
!.idea/codeStyles
|
||||
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="WeakerAccess" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS" value="true" />
|
||||
<option name="SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES" value="true" />
|
||||
<option name="SUGGEST_PRIVATE_FOR_INNERS" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -34,12 +34,12 @@ class KeyAgreementTransport {
|
||||
Logger.getLogger(KeyAgreementTransport.class.getName());
|
||||
|
||||
// Accept records with current protocol version, known record type
|
||||
private static Predicate<Record> ACCEPT = r ->
|
||||
private static final Predicate<Record> ACCEPT = r ->
|
||||
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
||||
isKnownRecordType(r.getRecordType());
|
||||
|
||||
// Ignore records with current protocol version, unknown record type
|
||||
private static Predicate<Record> IGNORE = r ->
|
||||
private static final Predicate<Record> IGNORE = r ->
|
||||
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
||||
!isKnownRecordType(r.getRecordType());
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.keyagreement.ContactExchangeActivity"
|
||||
android:name="org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity"
|
||||
android:label="@string/add_contact_title"
|
||||
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
|
||||
android:theme="@style/BriarTheme.NoActionBar">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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<Integer> 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String[]> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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<String[]> requestPermissions;
|
||||
private final boolean isBluetoothSupported;
|
||||
|
||||
AddNearbyContactPermissionManager(FragmentActivity ctx,
|
||||
Consumer<String[]> 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<String, Boolean> 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<String, Boolean> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<KeyAgreementTask> keyAgreementTaskProvider;
|
||||
private final ContactExchangeManager contactExchangeManager;
|
||||
private final ConnectionManager connectionManager;
|
||||
|
||||
private final MutableLiveEvent<Boolean> requestBluetoothDiscoverable =
|
||||
new MutableLiveEvent<>();
|
||||
private final MutableLiveEvent<Boolean> showQrCodeFragment =
|
||||
new MutableLiveEvent<>();
|
||||
private final MutableLiveData<AddContactState> 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<KeyAgreementTask> 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<Boolean> getRequestBluetoothDiscoverable() {
|
||||
return requestBluetoothDiscoverable;
|
||||
}
|
||||
|
||||
LiveEvent<Boolean> getShowQrCodeFragment() {
|
||||
return showQrCodeFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* This LiveData will be null initially.
|
||||
*/
|
||||
LiveData<AddContactState> getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.android.keyagreement;
|
||||
package org.briarproject.briar.android.contact.add.nearby;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Void, Void, Void> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean> 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<Boolean> getSucceeded() {
|
||||
return succeeded;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<KeyAgreementTask> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, Boolean> {
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
<include layout="@layout/toolbar" />
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<org.briarproject.briar.android.keyagreement.CameraView
|
||||
<org.briarproject.briar.android.contact.add.nearby.CameraView
|
||||
android:id="@+id/camera_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@@ -609,6 +609,9 @@
|
||||
<string name="permission_camera_location_request_body">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.</string>
|
||||
<string name="permission_camera_denied_body">You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access.</string>
|
||||
<string name="permission_location_denied_body">You have denied access to your location, but Briar needs this permission to discover Bluetooth devices.\n\nPlease consider granting access.</string>
|
||||
<string name="permission_location_setting_title">Location setting</string>
|
||||
<string name="permission_location_setting_body">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.</string>
|
||||
<string name="permission_location_setting_button">Enable location</string>
|
||||
<string name="qr_code">QR code</string>
|
||||
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user