return shard activity and view model

This commit is contained in:
ameba23
2021-03-25 20:43:34 +01:00
parent fc909a317e
commit c2cbba451d
8 changed files with 860 additions and 20 deletions

View File

@@ -157,7 +157,7 @@
</activity>
<activity
android:name="org.briarproject.briar.android.socialbackup.RecoverActivity"
android:name="org.briarproject.briar.android.socialbackup.recover.RecoverActivity"
android:label="@string/activity_name_recovery"
android:parentActivityName="org.briarproject.briar.android.account.NewOrRecoverActivity">
<meta-data

View File

@@ -7,7 +7,8 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.socialbackup.RecoverActivity;
import org.briarproject.briar.android.socialbackup.recover.RecoverActivity;
import org.briarproject.briar.android.socialbackup.recover.ReturnShardActivity;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
@@ -45,7 +46,7 @@ public class NewOrRecoverActivity extends BaseActivity implements
@Override
public void recoverAccountChosen() {
finish();
Intent i = new Intent(this, RecoverActivity.class);
Intent i = new Intent(this, ReturnShardActivity.class);
i.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP |
FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_TASK_ON_HOME);
startActivity(i);

View File

@@ -85,11 +85,12 @@ import org.briarproject.briar.android.socialbackup.CustodianHelpRecoverActivity;
import org.briarproject.briar.android.socialbackup.CustodianSelectorFragment;
import org.briarproject.briar.android.socialbackup.DistributedBackupActivity;
import org.briarproject.briar.android.socialbackup.ExistingBackupFragment;
import org.briarproject.briar.android.socialbackup.OwnerRecoveryModeExplainerFragment;
import org.briarproject.briar.android.socialbackup.RecoverActivity;
import org.briarproject.briar.android.socialbackup.recover.OwnerRecoveryModeExplainerFragment;
import org.briarproject.briar.android.socialbackup.recover.RecoverActivity;
import org.briarproject.briar.android.socialbackup.ShardsSentFragment;
import org.briarproject.briar.android.socialbackup.ThresholdSelectorFragment;
import org.briarproject.briar.android.socialbackup.creation.CreateBackupModule;
import org.briarproject.briar.android.socialbackup.recover.ReturnShardActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.test.TestDataActivity;
@@ -198,6 +199,8 @@ public interface ActivityComponent {
void inject(CustodianHelpRecoverActivity custodianHelpRecoverActivity);
void inject(ReturnShardActivity returnShardActivity);
// Fragments
void inject(AuthorNameFragment fragment);

View File

@@ -1,8 +1,6 @@
package org.briarproject.briar.android.socialbackup;
package org.briarproject.briar.android.socialbackup.recover;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -12,15 +10,22 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
public class OwnerRecoveryModeExplainerFragment extends BaseFragment {
protected ExplainerDismissedListener listener;
public static final String TAG =
OwnerRecoveryModeExplainerFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private ReturnShardViewModel viewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -35,20 +40,16 @@ public class OwnerRecoveryModeExplainerFragment extends BaseFragment {
View view = inflater.inflate(R.layout.fragment_recovery_owner_explainer,
container, false);
Button button = view.findViewById(R.id.beginButton);
button.setOnClickListener(e -> listener.explainerDismissed());
button.setOnClickListener(e -> viewModel.onContinueClicked());
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
listener = (ExplainerDismissedListener) context;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(ReturnShardViewModel.class);
}
@Override

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.socialbackup;
package org.briarproject.briar.android.socialbackup.recover;
import android.os.Bundle;
import android.widget.Toast;
@@ -7,6 +7,9 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.socialbackup.ExplainerDismissedListener;
import org.briarproject.briar.android.socialbackup.OwnerRecoveryModeMainFragment;
import org.briarproject.briar.android.socialbackup.ScanQrButtonListener;
public class RecoverActivity extends BaseActivity implements
BaseFragment.BaseFragmentListener, ExplainerDismissedListener,

View File

@@ -1,4 +1,238 @@
package org.briarproject.briar.android.socialbackup.recover;
public class ReturnShardActivity {
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.AddNearbyContactErrorFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactPermissionManager;
import org.briarproject.briar.android.fragment.BaseFragment;
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.activity.result.contract.ActivityResultContracts;
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;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ReturnShardActivity extends BriarActivity
implements BaseFragment.BaseFragmentListener {
private static final Logger LOG =
getLogger(
org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity.class
.getName());
@Inject
ViewModelProvider.Factory viewModelFactory;
private ReturnShardViewModel viewModel;
private AddNearbyContactPermissionManager permissionManager;
private final ActivityResultLauncher<String[]> permissionLauncher =
registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
r ->
permissionManager.onRequestPermissionResult(r,
viewModel::showQrCodeFragmentIfAllowed));
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(ReturnShardViewModel.class);
permissionManager = new AddNearbyContactPermissionManager(this,
permissionLauncher::launch, viewModel.isBluetoothSupported());
}
@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(getExplainerFragment());
}
viewModel.getCheckPermissions().observeEvent(this, check ->
permissionManager.checkPermissions());
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::onReturnShardStateChanged);
}
public BaseFragment getExplainerFragment() {
return new OwnerRecoveryModeExplainerFragment();
}
@Override
public void onStart() {
super.onStart();
// Permissions may have been granted manually while we were stopped
permissionManager.resetPermissions();
}
@Override
protected void onPostResume() {
super.onPostResume();
viewModel.setIsActivityResumed(true);
}
@Override
protected void onPause() {
super.onPause();
viewModel.setIsActivityResumed(false);
}
private void onBluetoothDiscoverableResult(boolean discoverable) {
if (discoverable) {
LOG.info("Bluetooth discoverability was accepted");
viewModel.setBluetoothDecision(
ReturnShardViewModel.BluetoothDecision.ACCEPTED);
} else {
LOG.info("Bluetooth discoverability was refused");
viewModel.setBluetoothDecision(
ReturnShardViewModel.BluetoothDecision.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 ReturnShardState.Failed) {
// re-create this activity when going back in failed state
Intent i = new Intent(this, ReturnShardActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
} else {
super.onBackPressed();
}
}
private void requestBluetoothDiscoverable() {
if (!viewModel.isBluetoothSupported()) {
viewModel.setBluetoothDecision(
ReturnShardViewModel.BluetoothDecision.NO_ADAPTER);
} else {
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
if (i.resolveActivity(getPackageManager()) != null) {
LOG.info("Asking for Bluetooth discoverability");
viewModel.setBluetoothDecision(
ReturnShardViewModel.BluetoothDecision.WAITING);
bluetoothLauncher.launch(120); // 2min discoverable
} else {
viewModel.setBluetoothDecision(
ReturnShardViewModel.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 onReturnShardStateChanged(ReturnShardState state) {
if (state instanceof ReturnShardState.ContactExchangeFinished) {
ReturnShardState.ContactExchangeResult result =
((ReturnShardState.ContactExchangeFinished) state).result;
onContactExchangeResult(result);
} else if (state instanceof ReturnShardState.Failed) {
Boolean qrCodeTooOld =
((ReturnShardState.Failed) state).qrCodeTooOld;
onAddingContactFailed(qrCodeTooOld);
}
}
private void onContactExchangeResult(
ReturnShardState.ContactExchangeResult result) {
if (result instanceof ReturnShardState.ContactExchangeResult.Success) {
Author remoteAuthor =
((ReturnShardState.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 ReturnShardState.ContactExchangeResult.Error) {
Author duplicateAuthor =
((ReturnShardState.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());
}
}

View File

@@ -1,4 +1,80 @@
package org.briarproject.briar.android.socialbackup.recover;
public class ReturnShardState {
import android.graphics.Bitmap;
import org.briarproject.bramble.api.identity.Author;
import androidx.annotation.Nullable;
abstract class ReturnShardState {
static class KeyAgreementListening extends
ReturnShardState {
final Bitmap qrCode;
KeyAgreementListening(Bitmap qrCode) {
this.qrCode = qrCode;
}
}
static class QrCodeScanned extends
ReturnShardState {
}
static class KeyAgreementWaiting extends ReturnShardState {
}
static class KeyAgreementStarted extends ReturnShardState {
}
static class ContactExchangeStarted extends ReturnShardState {
}
static class ContactExchangeFinished extends ReturnShardState {
final ContactExchangeResult
result;
ContactExchangeFinished(
ContactExchangeResult result) {
this.result = result;
}
}
static class Failed extends ReturnShardState {
/**
* 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
}

View File

@@ -1,4 +1,526 @@
package org.briarproject.briar.android.socialbackup.recover;
public class ReturnShardViewModel {
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.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.QrCodeDecoder;
import org.briarproject.briar.android.contact.add.nearby.QrCodeUtils;
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.Objects.requireNonNull;
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;
@NotNullByDefault
class ReturnShardViewModel extends AndroidViewModel
implements EventListener, QrCodeDecoder.ResultCallback {
private static final Logger LOG =
getLogger(ReturnShardViewModel.class.getName());
// TODO deduplicate
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> checkPermissions =
new MutableLiveEvent<>();
private final MutableLiveEvent<Boolean> requestBluetoothDiscoverable =
new MutableLiveEvent<>();
private final MutableLiveEvent<Boolean> showQrCodeFragment =
new MutableLiveEvent<>();
private final MutableLiveData<ReturnShardState> state =
new MutableLiveData<>();
final QrCodeDecoder qrCodeDecoder;
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;
private boolean isActivityResumed = 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 KeyAgreementTask task;
private volatile boolean gotLocalPayload = false, gotRemotePayload = false;
@Inject
ReturnShardViewModel(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 == BluetoothDecision.REFUSED) {
bluetoothDecision = BluetoothDecision.UNKNOWN; // Ask again
}
wasContinueClicked = true;
checkPermissions.setEvent(true);
showQrCodeFragmentIfAllowed();
}
@UiThread
boolean isBluetoothSupported() {
return bt != null && bluetoothPlugin != null;
}
@UiThread
boolean isWifiReady() {
if (wifiPlugin == null) return true; // Continue without wifi
Plugin.State state = wifiPlugin.getState();
// Wait for plugin to become enabled
return state == ACTIVE || state == INACTIVE;
}
@UiThread
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
void enableWifiIfWeShould() {
if (hasEnabledWifi) return;
if (wifiPlugin == null) return;
Plugin.State state = wifiPlugin.getState();
if (state == STARTING_STOPPING || state == DISABLED) {
LOG.info("Enabling wifi plugin");
hasEnabledWifi = true;
pluginManager.setPluginEnabled(LanTcpConstants.ID, true);
}
}
@UiThread
void enableBluetoothIfWeShould() {
if (bluetoothDecision != BluetoothDecision.ACCEPTED)
return;
if (hasEnabledBluetooth) return;
if (bluetoothPlugin == null || !isBluetoothSupported()) return;
Plugin.State state = bluetoothPlugin.getState();
if (state == STARTING_STOPPING || state == DISABLED) {
LOG.info("Enabling Bluetooth plugin");
hasEnabledBluetooth = true;
pluginManager.setPluginEnabled(BluetoothConstants.ID, true);
}
}
@UiThread
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 = BluetoothDecision.UNKNOWN;
// If we return to the intro fragment, we may need to enable wifi and
// Bluetooth again
hasEnabledWifi = false;
hasEnabledBluetooth = false;
// 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
private void stopListening() {
KeyAgreementTask oldTask = task;
ioExecutor.execute(() -> {
if (oldTask != null) oldTask.stopListening();
});
}
@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 ReturnShardState.KeyAgreementWaiting());
} else if (e instanceof KeyAgreementStartedEvent) {
LOG.info("KeyAgreementStartedEvent received");
state.setValue(new ReturnShardState.KeyAgreementStarted());
} else if (e instanceof KeyAgreementFinishedEvent) {
LOG.info("KeyAgreementFinishedEvent received");
KeyAgreementResult result =
((KeyAgreementFinishedEvent) e).getResult();
startContactExchange(result);
state.setValue(new ReturnShardState.ContactExchangeStarted());
} else if (e instanceof KeyAgreementAbortedEvent) {
LOG.info("KeyAgreementAbortedEvent received");
resetPayloadFlags();
state.setValue(new ReturnShardState.Failed());
} else if (e instanceof KeyAgreementFailedEvent) {
LOG.info("KeyAgreementFailedEvent received");
resetPayloadFlags();
state.setValue(new ReturnShardState.Failed());
}
}
@SuppressWarnings("StatementWithEmptyBody")
@UiThread
void showQrCodeFragmentIfAllowed() {
boolean permissionsGranted = areEssentialPermissionsGranted(
getApplication(), isBluetoothSupported());
if (isActivityResumed && wasContinueClicked && permissionsGranted) {
if (isWifiReady() && isBluetoothReady()) {
LOG.info("Wifi and Bluetooth are ready");
startAddingContact();
} else {
enableWifiIfWeShould();
if (bluetoothDecision == BluetoothDecision.UNKNOWN) {
requestBluetoothDiscoverable.setEvent(true);
} else if (bluetoothDecision == 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 ReturnShardState.KeyAgreementListening(qrCode));
});
}
@Override
@IoExecutor
public void onQrCodeDecoded(Result result) {
LOG.info("Got result from decoder");
// Ignore results until the KeyAgreementTask is ready
if (!gotLocalPayload || gotRemotePayload) 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;
requireNonNull(task).connectAndRunProtocol(remotePayload);
state.postValue(new ReturnShardState.QrCodeScanned());
} catch (UnsupportedVersionException e) {
resetPayloadFlags();
state.postValue(new ReturnShardState.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 ReturnShardState.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);
ReturnShardState.ContactExchangeResult.Success
success =
new ReturnShardState.ContactExchangeResult.Success(
contact.getAuthor());
state.postValue(
new ReturnShardState.ContactExchangeFinished(success));
} catch (ContactExistsException e) {
tryToClose(conn);
ReturnShardState.ContactExchangeResult.Error
error = new ReturnShardState.ContactExchangeResult.Error(
e.getRemoteAuthor());
state.postValue(
new ReturnShardState.ContactExchangeFinished(error));
} catch (DbException | IOException e) {
tryToClose(conn);
logException(LOG, WARNING, e);
ReturnShardState.ContactExchangeResult.Error
error =
new ReturnShardState.ContactExchangeResult.Error(null);
state.postValue(
new ReturnShardState.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);
}
}
/**
* 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.
* TODO check if this is still happening with new permission requesting
*/
@UiThread
void setIsActivityResumed(boolean resumed) {
isActivityResumed = resumed;
// Workaround for
// https://code.google.com/p/android/issues/detail?id=190966
showQrCodeFragmentIfAllowed();
}
@UiThread
void setBluetoothDecision(BluetoothDecision decision) {
bluetoothDecision = decision;
showQrCodeFragmentIfAllowed();
}
LiveEvent<Boolean> getCheckPermissions() {
return checkPermissions;
}
LiveEvent<Boolean> getRequestBluetoothDiscoverable() {
return requestBluetoothDiscoverable;
}
LiveEvent<Boolean> getShowQrCodeFragment() {
return showQrCodeFragment;
}
LiveData<ReturnShardState> getState() {
return state;
}
}