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
|
# Android Studio
|
||||||
.idea/*
|
.idea/*
|
||||||
|
!.idea/inspectionProfiles/
|
||||||
!.idea/runConfigurations/
|
!.idea/runConfigurations/
|
||||||
!.idea/codeStyleSettings.xml
|
!.idea/codeStyleSettings.xml
|
||||||
!.idea/codeStyles
|
!.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());
|
Logger.getLogger(KeyAgreementTransport.class.getName());
|
||||||
|
|
||||||
// Accept records with current protocol version, known record type
|
// 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 &&
|
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
||||||
isKnownRecordType(r.getRecordType());
|
isKnownRecordType(r.getRecordType());
|
||||||
|
|
||||||
// Ignore records with current protocol version, unknown record type
|
// 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 &&
|
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
||||||
!isKnownRecordType(r.getRecordType());
|
!isKnownRecordType(r.getRecordType());
|
||||||
|
|
||||||
|
|||||||
@@ -342,7 +342,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<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:label="@string/add_contact_title"
|
||||||
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
|
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
|
||||||
android:theme="@style/BriarTheme.NoActionBar">
|
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.account.SetupModule;
|
||||||
import org.briarproject.briar.android.blog.BlogModule;
|
import org.briarproject.briar.android.blog.BlogModule;
|
||||||
import org.briarproject.briar.android.contact.ContactListModule;
|
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.forum.ForumModule;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionModule;
|
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.logging.LoggingModule;
|
||||||
import org.briarproject.briar.android.login.LoginModule;
|
import org.briarproject.briar.android.login.LoginModule;
|
||||||
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
|
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
|
||||||
@@ -77,7 +77,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
|||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
SetupModule.class,
|
SetupModule.class,
|
||||||
DozeHelperModule.class,
|
DozeHelperModule.class,
|
||||||
ContactExchangeModule.class,
|
AddNearbyContactModule.class,
|
||||||
LoggingModule.class,
|
LoggingModule.class,
|
||||||
LoginModule.class,
|
LoginModule.class,
|
||||||
NavDrawerModule.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.RssFeedManageActivity;
|
||||||
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
|
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
|
||||||
import org.briarproject.briar.android.contact.ContactListFragment;
|
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.AddContactActivity;
|
||||||
import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment;
|
import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment;
|
||||||
import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
|
import org.briarproject.briar.android.contact.add.remote.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.ContactChooserFragment;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
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.ChangePasswordActivity;
|
||||||
import org.briarproject.briar.android.login.OpenDatabaseFragment;
|
import org.briarproject.briar.android.login.OpenDatabaseFragment;
|
||||||
import org.briarproject.briar.android.login.PasswordFragment;
|
import org.briarproject.briar.android.login.PasswordFragment;
|
||||||
@@ -105,9 +105,7 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(PanicPreferencesActivity activity);
|
void inject(PanicPreferencesActivity activity);
|
||||||
|
|
||||||
void inject(ContactExchangeActivity activity);
|
void inject(AddNearbyContactActivity activity);
|
||||||
|
|
||||||
void inject(KeyAgreementActivity activity);
|
|
||||||
|
|
||||||
void inject(ConversationActivity activity);
|
void inject(ConversationActivity activity);
|
||||||
|
|
||||||
@@ -203,7 +201,9 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(FeedFragment fragment);
|
void inject(FeedFragment fragment);
|
||||||
|
|
||||||
void inject(KeyAgreementFragment fragment);
|
void inject(AddNearbyContactIntroFragment fragment);
|
||||||
|
|
||||||
|
void inject(AddNearbyContactFragment fragment);
|
||||||
|
|
||||||
void inject(LinkExchangeFragment fragment);
|
void inject(LinkExchangeFragment fragment);
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(ScreenFilterDialogFragment fragment);
|
void inject(ScreenFilterDialogFragment fragment);
|
||||||
|
|
||||||
void inject(ContactExchangeErrorFragment fragment);
|
void inject(AddNearbyContactErrorFragment fragment);
|
||||||
|
|
||||||
void inject(AliasDialogFragment aliasDialogFragment);
|
void inject(AliasDialogFragment aliasDialogFragment);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ public interface RequestCodes {
|
|||||||
int REQUEST_SHARE_FORUM = 4;
|
int REQUEST_SHARE_FORUM = 4;
|
||||||
int REQUEST_SHARE_BLOG = 6;
|
int REQUEST_SHARE_BLOG = 6;
|
||||||
int REQUEST_RINGTONE = 7;
|
int REQUEST_RINGTONE = 7;
|
||||||
int REQUEST_PERMISSION_CAMERA_LOCATION = 8;
|
|
||||||
int REQUEST_DOZE_WHITELISTING = 9;
|
int REQUEST_DOZE_WHITELISTING = 9;
|
||||||
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
|
|
||||||
int REQUEST_UNLOCK = 11;
|
int REQUEST_UNLOCK = 11;
|
||||||
int REQUEST_KEYGUARD_UNLOCK = 12;
|
int REQUEST_KEYGUARD_UNLOCK = 12;
|
||||||
int REQUEST_ATTACH_IMAGE = 13;
|
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.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
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.AddContactActivity;
|
||||||
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
||||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
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.util.BriarSnackbarBuilder;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
|
|
||||||
@@ -125,7 +125,8 @@ public class ContactListFragment extends BaseFragment
|
|||||||
switch (itemId) {
|
switch (itemId) {
|
||||||
case R.id.action_add_contact_nearby:
|
case R.id.action_add_contact_nearby:
|
||||||
Intent intent =
|
Intent intent =
|
||||||
new Intent(getContext(), ContactExchangeActivity.class);
|
new Intent(getContext(),
|
||||||
|
AddNearbyContactActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return;
|
return;
|
||||||
case R.id.action_add_contact_remotely:
|
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.content.Intent;
|
||||||
import android.os.Bundle;
|
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.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.util.UiUtils;
|
import org.briarproject.briar.android.util.UiUtils;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
@@ -24,14 +27,19 @@ import static org.briarproject.briar.android.util.UiUtils.onSingleLinkClick;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ContactExchangeErrorFragment extends BaseFragment {
|
public class AddNearbyContactErrorFragment extends BaseFragment {
|
||||||
|
|
||||||
public static final String TAG =
|
public static final String TAG =
|
||||||
ContactExchangeErrorFragment.class.getName();
|
AddNearbyContactErrorFragment.class.getName();
|
||||||
private static final String ERROR_MSG = "errorMessage";
|
private static final String ERROR_MSG = "errorMessage";
|
||||||
|
|
||||||
public static ContactExchangeErrorFragment newInstance(String errorMsg) {
|
@Inject
|
||||||
ContactExchangeErrorFragment f = new ContactExchangeErrorFragment();
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
|
|
||||||
|
private AddNearbyContactViewModel viewModel;
|
||||||
|
|
||||||
|
public static AddNearbyContactErrorFragment newInstance(String errorMsg) {
|
||||||
|
AddNearbyContactErrorFragment f = new AddNearbyContactErrorFragment();
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putString(ERROR_MSG, errorMsg);
|
args.putString(ERROR_MSG, errorMsg);
|
||||||
f.setArguments(args);
|
f.setArguments(args);
|
||||||
@@ -46,6 +54,8 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
|||||||
@Override
|
@Override
|
||||||
public void injectFragment(ActivityComponent component) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
|
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||||
|
.get(AddNearbyContactViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -72,7 +82,7 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
|||||||
tryAgain.setOnClickListener(view -> {
|
tryAgain.setOnClickListener(view -> {
|
||||||
// Recreate the activity so we return to the intro fragment
|
// Recreate the activity so we return to the intro fragment
|
||||||
FragmentActivity activity = requireActivity();
|
FragmentActivity activity = requireActivity();
|
||||||
Intent i = new Intent(activity, ContactExchangeActivity.class);
|
Intent i = new Intent(activity, AddNearbyContactActivity.class);
|
||||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
activity.startActivity(i);
|
activity.startActivity(i);
|
||||||
});
|
});
|
||||||
@@ -81,6 +91,15 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
|||||||
return v;
|
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() {
|
private void triggerFeedback() {
|
||||||
UiUtils.triggerFeedback(requireContext());
|
UiUtils.triggerFeedback(requireContext());
|
||||||
finish();
|
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;
|
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||||
|
|
||||||
@@ -8,12 +8,12 @@ import dagger.Module;
|
|||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
public abstract class ContactExchangeModule {
|
public abstract class AddNearbyContactModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(ContactExchangeViewModel.class)
|
@ViewModelKey(AddNearbyContactViewModel.class)
|
||||||
abstract ViewModel bindContactExchangeViewModel(
|
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;
|
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.content.Context;
|
||||||
import android.hardware.Camera;
|
import android.hardware.Camera;
|
||||||
@@ -40,7 +40,6 @@ import static java.util.logging.Level.INFO;
|
|||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||||
@@ -126,8 +125,14 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
|||||||
throw new CameraException(e);
|
throw new CameraException(e);
|
||||||
}
|
}
|
||||||
setDisplayOrientation(getScreenRotationDegrees());
|
setDisplayOrientation(getScreenRotationDegrees());
|
||||||
|
if (camera == null) throw new CameraException("No camera found");
|
||||||
// Use barcode scene mode if it's available
|
// 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);
|
params = setSceneMode(camera, params);
|
||||||
if (SCENE_MODE_BARCODE.equals(params.getSceneMode())) {
|
if (SCENE_MODE_BARCODE.equals(params.getSceneMode())) {
|
||||||
// If the scene mode enabled the flash, try to disable it
|
// 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;
|
import android.hardware.Camera;
|
||||||
|
|
||||||
@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
interface PreviewConsumer {
|
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;
|
||||||
import android.hardware.Camera.CameraInfo;
|
import android.hardware.Camera.CameraInfo;
|
||||||
import android.hardware.Camera.PreviewCallback;
|
import android.hardware.Camera.PreviewCallback;
|
||||||
import android.hardware.Camera.Size;
|
import android.hardware.Camera.Size;
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import com.google.zxing.BinaryBitmap;
|
import com.google.zxing.BinaryBitmap;
|
||||||
import com.google.zxing.LuminanceSource;
|
import com.google.zxing.LuminanceSource;
|
||||||
@@ -15,10 +14,13 @@ import com.google.zxing.Result;
|
|||||||
import com.google.zxing.common.HybridBinarizer;
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
import com.google.zxing.qrcode.QRCodeReader;
|
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.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
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 java.util.logging.Logger;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
@@ -26,22 +28,26 @@ import androidx.annotation.UiThread;
|
|||||||
import static com.google.zxing.DecodeHintType.CHARACTER_SET;
|
import static com.google.zxing.DecodeHintType.CHARACTER_SET;
|
||||||
import static java.util.Collections.singletonMap;
|
import static java.util.Collections.singletonMap;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG = getLogger(QrCodeDecoder.class.getName());
|
||||||
Logger.getLogger(QrCodeDecoder.class.getName());
|
|
||||||
|
|
||||||
|
private final AndroidExecutor androidExecutor;
|
||||||
|
private final Executor ioExecutor;
|
||||||
private final Reader reader = new QRCodeReader();
|
private final Reader reader = new QRCodeReader();
|
||||||
private final ResultCallback callback;
|
private final ResultCallback callback;
|
||||||
|
|
||||||
private Camera camera = null;
|
private Camera camera = null;
|
||||||
private int cameraIndex = 0;
|
private int cameraIndex = 0;
|
||||||
|
|
||||||
QrCodeDecoder(ResultCallback callback) {
|
QrCodeDecoder(AndroidExecutor androidExecutor,
|
||||||
|
@IoExecutor Executor ioExecutor, ResultCallback callback) {
|
||||||
|
this.androidExecutor = androidExecutor;
|
||||||
|
this.ioExecutor = ioExecutor;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +80,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
|||||||
if (data.length == size.width * size.height * 3 / 2) {
|
if (data.length == size.width * size.height * 3 / 2) {
|
||||||
CameraInfo info = new CameraInfo();
|
CameraInfo info = new CameraInfo();
|
||||||
Camera.getCameraInfo(cameraIndex, info);
|
Camera.getCameraInfo(cameraIndex, info);
|
||||||
new DecoderTask(data, size.width, size.height,
|
decode(data, size.width, size.height, info.orientation);
|
||||||
info.orientation).execute();
|
|
||||||
} else {
|
} else {
|
||||||
// Camera parameters have changed - ask for a new preview
|
// Camera parameters have changed - ask for a new preview
|
||||||
LOG.info("Preview size does not match camera parameters");
|
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 void decode(byte[] data, int width, int height, int orientation) {
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
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) {
|
|
||||||
BinaryBitmap bitmap = binarize(data, width, height, orientation);
|
BinaryBitmap bitmap = binarize(data, width, height, orientation);
|
||||||
Result result;
|
Result result;
|
||||||
try {
|
try {
|
||||||
result = reader.decode(bitmap,
|
result = reader.decode(bitmap,
|
||||||
singletonMap(CHARACTER_SET, "ISO8859_1"));
|
singletonMap(CHARACTER_SET, "ISO8859_1"));
|
||||||
|
callback.onQrCodeDecoded(result);
|
||||||
} catch (ReaderException e) {
|
} catch (ReaderException e) {
|
||||||
// No barcode found
|
// No barcode found
|
||||||
return null;
|
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
LOG.warning("Invalid preview frame");
|
LOG.warning("Invalid preview frame");
|
||||||
return null;
|
|
||||||
} finally {
|
} finally {
|
||||||
reader.reset();
|
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,
|
private static BinaryBitmap binarize(byte[] data, int width, int height,
|
||||||
@@ -143,7 +128,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
|||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
interface ResultCallback {
|
interface ResultCallback {
|
||||||
|
@IoExecutor
|
||||||
void handleResult(Result result);
|
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.graphics.Bitmap;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
@@ -18,13 +18,13 @@ import static android.graphics.Color.BLACK;
|
|||||||
import static android.graphics.Color.WHITE;
|
import static android.graphics.Color.WHITE;
|
||||||
import static com.google.zxing.BarcodeFormat.QR_CODE;
|
import static com.google.zxing.BarcodeFormat.QR_CODE;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class QrCodeUtils {
|
class QrCodeUtils {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
|
||||||
Logger.getLogger(QrCodeUtils.class.getName());
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
tools:context=".android.keyagreement.KeyAgreementActivity">
|
tools:context=".android.contact.add.nearby.AddNearbyContactActivity">
|
||||||
|
|
||||||
<include layout="@layout/toolbar" />
|
<include layout="@layout/toolbar" />
|
||||||
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout
|
||||||
android:id="@+id/fragmentContainer"
|
android:id="@+id/fragmentContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:keepScreenOn="true">
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
<org.briarproject.briar.android.keyagreement.CameraView
|
<org.briarproject.briar.android.contact.add.nearby.CameraView
|
||||||
android:id="@+id/camera_view"
|
android:id="@+id/camera_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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_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_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_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="qr_code">QR code</string>
|
||||||
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>
|
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user