Move backend comms and logic out of KeyAgreementFragment

into ViewModel
This commit is contained in:
Torsten Grote
2021-02-02 15:18:59 -03:00
parent 6d1f1c7852
commit bed87ed439
7 changed files with 318 additions and 260 deletions

View File

@@ -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());

View File

@@ -0,0 +1,55 @@
package org.briarproject.briar.android.contact.add.nearby;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
abstract class ContactAddingState {
static class KeyAgreementListening extends ContactAddingState {
final Bitmap qrCode;
KeyAgreementListening(Bitmap qrCode) {
this.qrCode = qrCode;
}
}
static class QrCodeScanned extends ContactAddingState {
}
static class KeyAgreementWaiting extends ContactAddingState {
}
static class KeyAgreementStarted extends ContactAddingState {
}
static class ContactExchangeStarted extends ContactAddingState {
}
static class ContactExchangeFinished extends ContactAddingState {
final ContactExchangeResult result;
ContactExchangeFinished(ContactExchangeResult result) {
this.result = result;
}
}
static class Failed extends ContactAddingState {
/**
* 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);
}
}
}

View File

@@ -7,14 +7,15 @@ import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; 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.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState; import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeFinished;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.Failed;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.ABORTED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.FAILED;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -25,15 +26,33 @@ public class ContactExchangeActivity extends KeyAgreementActivity {
super.onCreate(state); super.onCreate(state);
requireNonNull(getSupportActionBar()) requireNonNull(getSupportActionBar())
.setTitle(R.string.add_contact_title); .setTitle(R.string.add_contact_title);
viewModel.getKeyAgreementState() viewModel.getState()
.observe(this, this::onKeyAgreementStateChanged); .observe(this, this::onContactAddingStateChanged);
viewModel.getContactExchangeResult()
.observe(this, this::onContactExchangeResult);
} }
private void onKeyAgreementStateChanged(KeyAgreementState state) { @Override
if (state == ABORTED || state == FAILED) { public void onBackPressed() {
showErrorFragment(); if (viewModel.getState().getValue() instanceof Failed) {
// finish this activity when going back in failed state
supportFinishAfterTransition();
} else {
super.onBackPressed();
}
}
private void onContactAddingStateChanged(ContactAddingState state) {
if (state instanceof ContactExchangeFinished) {
ContactExchangeResult result =
((ContactExchangeFinished) state).result;
onContactExchangeResult(result);
} else if (state instanceof Failed) {
// Remove navigation icon, so user can't go back when failed
// ErrorFragment will finish or relaunch this activity
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setNavigationIcon(null);
Boolean qrCodeTooOld = ((Failed) state).qrCodeTooOld;
onAddingContactFailed(qrCodeTooOld);
} }
} }
@@ -60,6 +79,22 @@ public class ContactExchangeActivity extends KeyAgreementActivity {
} else throw new AssertionError(); } 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(ContactExchangeErrorFragment.newInstance(msg));
}
}
private void showErrorFragment() { private void showErrorFragment() {
showNextFragment(new ContactExchangeErrorFragment()); showNextFragment(new ContactExchangeErrorFragment());
} }

View File

@@ -1,7 +1,13 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.contact.add.nearby;
import android.app.Application; import android.app.Application;
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.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactExchangeManager; import org.briarproject.bramble.api.contact.ContactExchangeManager;
@@ -12,95 +18,208 @@ import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementResult; 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.KeyAgreementAbortedEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent; 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.KeyAgreementStartedEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeFinished;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeStarted;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementListening;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementStarted;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementWaiting;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Error; import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Error;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Success; import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Success;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
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.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.ABORTED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.FAILED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.FINISHED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.STARTED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.WAITING;
@NotNullByDefault @NotNullByDefault
class ContactExchangeViewModel extends AndroidViewModel class ContactExchangeViewModel extends AndroidViewModel
implements EventListener { implements EventListener, QrCodeDecoder.ResultCallback {
private static final Logger LOG = private static final Logger LOG =
getLogger(ContactExchangeViewModel.class.getName()); getLogger(ContactExchangeViewModel.class.getName());
enum KeyAgreementState { @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
WAITING, STARTED, FINISHED, ABORTED, FAILED private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
}
private final EventBus eventBus; private final EventBus eventBus;
private final Executor ioExecutor; private final Executor ioExecutor;
private final PayloadEncoder payloadEncoder;
private final PayloadParser payloadParser;
private final Provider<KeyAgreementTask> keyAgreementTaskProvider;
private final ContactExchangeManager contactExchangeManager; private final ContactExchangeManager contactExchangeManager;
private final ConnectionManager connectionManager; private final ConnectionManager connectionManager;
private final MutableLiveData<KeyAgreementState> keyAgreementState =
new MutableLiveData<>(); private final MutableLiveData<ContactAddingState> state =
private final MutableLiveData<ContactExchangeResult> exchangeResult =
new MutableLiveData<>(); new MutableLiveData<>();
final QrCodeDecoder qrCodeDecoder;
@Nullable
private KeyAgreementTask task;
private volatile boolean gotLocalPayload = false, gotRemotePayload = false;
@Inject @Inject
ContactExchangeViewModel(Application app, ContactExchangeViewModel(Application app,
EventBus eventBus, EventBus eventBus,
@IoExecutor Executor ioExecutor, @IoExecutor Executor ioExecutor,
PayloadEncoder payloadEncoder,
PayloadParser payloadParser,
Provider<KeyAgreementTask> keyAgreementTaskProvider,
ContactExchangeManager contactExchangeManager, ContactExchangeManager contactExchangeManager,
ConnectionManager connectionManager) { ConnectionManager connectionManager) {
super(app); super(app);
this.eventBus = eventBus; this.eventBus = eventBus;
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.payloadEncoder = payloadEncoder;
this.payloadParser = payloadParser;
this.keyAgreementTaskProvider = keyAgreementTaskProvider;
this.contactExchangeManager = contactExchangeManager; this.contactExchangeManager = contactExchangeManager;
this.connectionManager = connectionManager; this.connectionManager = connectionManager;
qrCodeDecoder = new QrCodeDecoder(ioExecutor, this);
eventBus.addListener(this);
} }
@Override @Override
protected void onCleared() { protected void onCleared() {
super.onCleared(); super.onCleared();
eventBus.removeListener(this); eventBus.removeListener(this);
stopListening();
}
/**
* 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
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 @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof KeyAgreementWaitingEvent) { if (e instanceof KeyAgreementListeningEvent) {
keyAgreementState.setValue(WAITING); 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) { } else if (e instanceof KeyAgreementStartedEvent) {
keyAgreementState.setValue(STARTED); LOG.info("KeyAgreementStartedEvent received");
} else if (e instanceof KeyAgreementAbortedEvent) { state.setValue(new KeyAgreementStarted());
keyAgreementState.setValue(ABORTED);
} else if (e instanceof KeyAgreementFinishedEvent) { } else if (e instanceof KeyAgreementFinishedEvent) {
keyAgreementState.setValue(FINISHED); LOG.info("KeyAgreementFinishedEvent received");
KeyAgreementResult result = KeyAgreementResult result =
((KeyAgreementFinishedEvent) e).getResult(); ((KeyAgreementFinishedEvent) e).getResult();
startContactExchange(result); startContactExchange(result);
state.setValue(new ContactExchangeStarted());
} else if (e instanceof KeyAgreementAbortedEvent) {
LOG.info("KeyAgreementAbortedEvent received");
resetPayloadFlags();
state.setValue(new ContactAddingState.Failed());
} else if (e instanceof KeyAgreementFailedEvent) { } else if (e instanceof KeyAgreementFailedEvent) {
keyAgreementState.setValue(FAILED); LOG.info("KeyAgreementFailedEvent received");
resetPayloadFlags();
state.setValue(new ContactAddingState.Failed());
} }
} }
/**
* 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");
// 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 ContactAddingState.QrCodeScanned());
} catch (UnsupportedVersionException e) {
resetPayloadFlags();
state.postValue(new ContactAddingState.Failed(e.isTooOld()));
} catch (IOException | IllegalArgumentException e) {
LOG.log(WARNING, "QR Code Invalid", e);
Toast.makeText(getApplication(), R.string.qr_code_invalid,
LENGTH_LONG).show();
resetPayloadFlags();
state.postValue(new ContactAddingState.Failed());
}
}
private void resetPayloadFlags() {
gotRemotePayload = false;
gotLocalPayload = false;
}
@UiThread @UiThread
private void startContactExchange(KeyAgreementResult result) { private void startContactExchange(KeyAgreementResult result) {
TransportId t = result.getTransportId(); TransportId t = result.getTransportId();
@@ -114,14 +233,17 @@ class ContactExchangeViewModel extends AndroidViewModel
// Reuse the connection as a transport connection // Reuse the connection as a transport connection
connectionManager connectionManager
.manageOutgoingConnection(contact.getId(), t, conn); .manageOutgoingConnection(contact.getId(), t, conn);
exchangeResult.postValue(new Success(contact.getAuthor())); Success success = new Success(contact.getAuthor());
state.postValue(new ContactExchangeFinished(success));
} catch (ContactExistsException e) { } catch (ContactExistsException e) {
tryToClose(conn); tryToClose(conn);
exchangeResult.postValue(new Error(e.getRemoteAuthor())); Error error = new Error(e.getRemoteAuthor());
state.postValue(new ContactExchangeFinished(error));
} catch (DbException | IOException e) { } catch (DbException | IOException e) {
tryToClose(conn); tryToClose(conn);
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
exchangeResult.postValue(new Error(null)); Error error = new Error(null);
state.postValue(new ContactExchangeFinished(error));
} }
}); });
} }
@@ -135,11 +257,8 @@ class ContactExchangeViewModel extends AndroidViewModel
} }
} }
LiveData<KeyAgreementState> getKeyAgreementState() { LiveData<ContactAddingState> getState() {
return keyAgreementState; return state;
} }
LiveData<ContactExchangeResult> getContactExchangeResult() {
return exchangeResult;
}
} }

View File

@@ -205,6 +205,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
if (isResumed && continueClicked && areEssentialPermissionsGranted()) { if (isResumed && continueClicked && areEssentialPermissionsGranted()) {
if (isWifiReady() && isBluetoothReady()) { if (isWifiReady() && isBluetoothReady()) {
LOG.info("Wifi and Bluetooth are ready"); LOG.info("Wifi and Bluetooth are ready");
viewModel.startListening();
showQrCodeFragment(); showQrCodeFragment();
} else { } else {
if (shouldEnableWifi()) { if (shouldEnableWifi()) {

View File

@@ -1,9 +1,7 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.contact.add.nearby;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.os.Bundle; import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -12,36 +10,24 @@ import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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.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.KeyAgreementListeningEvent;
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.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.ContactExchangeViewModel.KeyAgreementState; import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeStarted;
import org.briarproject.briar.android.fragment.BaseEventFragment; import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.Failed;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementStarted;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementWaiting;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.QrCodeScanned;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.QrCodeView; 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 java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
@@ -50,39 +36,20 @@ import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.Toast.LENGTH_LONG; 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.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.ABORTED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.FAILED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.FINISHED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.STARTED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState.WAITING;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class KeyAgreementFragment extends BaseEventFragment public class KeyAgreementFragment extends BaseFragment
implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener { implements QrCodeView.FullscreenListener {
static final String TAG = KeyAgreementFragment.class.getName(); static final String TAG = KeyAgreementFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG); 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 @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@Inject
Provider<KeyAgreementTask> keyAgreementTaskProvider;
@Inject
PayloadEncoder payloadEncoder;
@Inject
PayloadParser payloadParser;
@Inject
@IoExecutor
Executor ioExecutor;
@Inject
EventBus eventBus;
private ContactExchangeViewModel viewModel; private ContactExchangeViewModel viewModel;
private CameraView cameraView; private CameraView cameraView;
@@ -91,10 +58,6 @@ public class KeyAgreementFragment extends BaseEventFragment
private QrCodeView qrCodeView; private QrCodeView qrCodeView;
private TextView status; private TextView status;
private boolean gotRemotePayload;
private volatile boolean gotLocalPayload;
private KeyAgreementTask task;
public static KeyAgreementFragment newInstance() { public static KeyAgreementFragment newInstance() {
Bundle args = new Bundle(); Bundle args = new Bundle();
KeyAgreementFragment fragment = new KeyAgreementFragment(); KeyAgreementFragment fragment = new KeyAgreementFragment();
@@ -109,11 +72,6 @@ public class KeyAgreementFragment extends BaseEventFragment
.get(ContactExchangeViewModel.class); .get(ContactExchangeViewModel.class);
} }
@Override
public String getUniqueTag() {
return TAG;
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@@ -133,16 +91,15 @@ public class KeyAgreementFragment extends BaseEventFragment
qrCodeView = view.findViewById(R.id.qr_code_view); qrCodeView = view.findViewById(R.id.qr_code_view);
qrCodeView.setFullscreenListener(this); qrCodeView.setFullscreenListener(this);
LifecycleOwner lifecycleOwner = getViewLifecycleOwner(); viewModel.getState().observe(getViewLifecycleOwner(),
viewModel.getKeyAgreementState() this::onContactAddingStateChanged);
.observe(lifecycleOwner, this::onKeyAgreementStateChanged);
} }
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
cameraView.setPreviewConsumer(new QrCodeDecoder(this)); cameraView.setPreviewConsumer(viewModel.qrCodeDecoder);
} }
@Override @Override
@@ -153,7 +110,16 @@ public class KeyAgreementFragment extends BaseEventFragment
} catch (CameraException e) { } catch (CameraException e) {
logCameraExceptionAndFinish(e); logCameraExceptionAndFinish(e);
} }
startListening(); }
@Override
public void onStop() {
super.onStop();
try {
cameraView.stop();
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
}
} }
@Override @Override
@@ -178,17 +144,42 @@ public class KeyAgreementFragment extends BaseEventFragment
cameraOverlay.invalidate(); cameraOverlay.invalidate();
} }
@Override @UiThread
public void onStop() { private void onContactAddingStateChanged(ContactAddingState state) {
super.onStop(); if (state instanceof ContactAddingState.KeyAgreementListening) {
stopListening(); Bitmap qrCode =
try { ((ContactAddingState.KeyAgreementListening) state).qrCode;
cameraView.stop(); qrCodeView.setQrCode(qrCode);
} catch (CameraException e) { } else if (state instanceof QrCodeScanned) {
logCameraExceptionAndFinish(e); 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);
statusView.setVisibility(VISIBLE);
status.setText(R.string.authenticating_with_device);
} else if (state instanceof ContactExchangeStarted) {
statusView.setVisibility(VISIBLE);
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 @UiThread
private void logCameraExceptionAndFinish(CameraException e) { private void logCameraExceptionAndFinish(CameraException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
@@ -197,130 +188,6 @@ public class KeyAgreementFragment extends BaseEventFragment
finish(); 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());
}
}
@UiThread
private void onKeyAgreementStateChanged(KeyAgreementState state) {
if (state == WAITING) {
status.setText(R.string.waiting_for_contact_to_scan);
} else if (state == STARTED) {
qrCodeView.setVisibility(INVISIBLE);
statusView.setVisibility(VISIBLE);
status.setText(R.string.authenticating_with_device);
} else if (state == FINISHED) {
statusView.setVisibility(VISIBLE);
status.setText(R.string.exchanging_contact_details);
} else if (state == ABORTED || state == FAILED) {
reset();
}
}
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 @Override
protected void finish() { protected void finish() {
requireActivity().getSupportFragmentManager().popBackStack(); requireActivity().getSupportFragmentManager().popBackStack();

View File

@@ -4,7 +4,6 @@ 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,12 @@ 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 java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
@@ -26,22 +27,23 @@ 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 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(@IoExecutor Executor ioExecutor, ResultCallback callback) {
this.ioExecutor = ioExecutor;
this.callback = callback; this.callback = callback;
} }
@@ -74,8 +76,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 +90,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();
} }
callback.handleResult(result); });
return null; askForPreviewFrame();
}
@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 +124,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@NotNullByDefault @NotNullByDefault
interface ResultCallback { interface ResultCallback {
@IoExecutor
void handleResult(Result result); void onQrCodeDecoded(Result result);
} }
} }