diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java index 5785575ee..f3c19076d 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTransport.java @@ -34,12 +34,12 @@ class KeyAgreementTransport { Logger.getLogger(KeyAgreementTransport.class.getName()); // Accept records with current protocol version, known record type - private static Predicate ACCEPT = r -> + private static final Predicate ACCEPT = r -> r.getProtocolVersion() == PROTOCOL_VERSION && isKnownRecordType(r.getRecordType()); // Ignore records with current protocol version, unknown record type - private static Predicate IGNORE = r -> + private static final Predicate IGNORE = r -> r.getProtocolVersion() == PROTOCOL_VERSION && !isKnownRecordType(r.getRecordType()); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactAddingState.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactAddingState.java new file mode 100644 index 000000000..51278b322 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactAddingState.java @@ -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); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeActivity.java index 7d2a6e1e3..c4a435136 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeActivity.java @@ -7,14 +7,15 @@ 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.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 androidx.appcompat.widget.Toolbar; + import static android.widget.Toast.LENGTH_LONG; 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 @ParametersNotNullByDefault @@ -25,15 +26,33 @@ public class ContactExchangeActivity extends KeyAgreementActivity { super.onCreate(state); requireNonNull(getSupportActionBar()) .setTitle(R.string.add_contact_title); - viewModel.getKeyAgreementState() - .observe(this, this::onKeyAgreementStateChanged); - viewModel.getContactExchangeResult() - .observe(this, this::onContactExchangeResult); + viewModel.getState() + .observe(this, this::onContactAddingStateChanged); } - private void onKeyAgreementStateChanged(KeyAgreementState state) { - if (state == ABORTED || state == FAILED) { - showErrorFragment(); + @Override + public void onBackPressed() { + 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(); } + 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() { showNextFragment(new ContactExchangeErrorFragment()); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeViewModel.java index 19394e5fe..241e665ec 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/ContactExchangeViewModel.java @@ -1,7 +1,13 @@ package org.briarproject.briar.android.contact.add.nearby; 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.contact.Contact; 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.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.TransportId; 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.Success; 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.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.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 class ContactExchangeViewModel extends AndroidViewModel - implements EventListener { + implements EventListener, QrCodeDecoder.ResultCallback { private static final Logger LOG = getLogger(ContactExchangeViewModel.class.getName()); - enum KeyAgreementState { - WAITING, STARTED, FINISHED, ABORTED, FAILED - } + @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 + private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); private final EventBus eventBus; private final Executor ioExecutor; + private final PayloadEncoder payloadEncoder; + private final PayloadParser payloadParser; + private final Provider keyAgreementTaskProvider; private final ContactExchangeManager contactExchangeManager; private final ConnectionManager connectionManager; - private final MutableLiveData keyAgreementState = - new MutableLiveData<>(); - private final MutableLiveData exchangeResult = + + private final MutableLiveData state = new MutableLiveData<>(); + final QrCodeDecoder qrCodeDecoder; + + @Nullable + private KeyAgreementTask task; + private volatile boolean gotLocalPayload = false, gotRemotePayload = false; + @Inject ContactExchangeViewModel(Application app, EventBus eventBus, @IoExecutor Executor ioExecutor, + PayloadEncoder payloadEncoder, + PayloadParser payloadParser, + Provider keyAgreementTaskProvider, ContactExchangeManager contactExchangeManager, ConnectionManager connectionManager) { super(app); this.eventBus = eventBus; this.ioExecutor = ioExecutor; + this.payloadEncoder = payloadEncoder; + this.payloadParser = payloadParser; + this.keyAgreementTaskProvider = keyAgreementTaskProvider; this.contactExchangeManager = contactExchangeManager; this.connectionManager = connectionManager; + qrCodeDecoder = new QrCodeDecoder(ioExecutor, this); + eventBus.addListener(this); } @Override protected void onCleared() { super.onCleared(); 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 public void eventOccurred(Event e) { - if (e instanceof KeyAgreementWaitingEvent) { - keyAgreementState.setValue(WAITING); + 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) { - keyAgreementState.setValue(STARTED); - } else if (e instanceof KeyAgreementAbortedEvent) { - keyAgreementState.setValue(ABORTED); + LOG.info("KeyAgreementStartedEvent received"); + state.setValue(new KeyAgreementStarted()); } else if (e instanceof KeyAgreementFinishedEvent) { - keyAgreementState.setValue(FINISHED); + 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 ContactAddingState.Failed()); } 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 private void startContactExchange(KeyAgreementResult result) { TransportId t = result.getTransportId(); @@ -114,14 +233,17 @@ class ContactExchangeViewModel extends AndroidViewModel // Reuse the connection as a transport connection connectionManager .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) { tryToClose(conn); - exchangeResult.postValue(new Error(e.getRemoteAuthor())); + Error error = new Error(e.getRemoteAuthor()); + state.postValue(new ContactExchangeFinished(error)); } catch (DbException | IOException e) { tryToClose(conn); 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 getKeyAgreementState() { - return keyAgreementState; + LiveData getState() { + return state; } - LiveData getContactExchangeResult() { - return exchangeResult; - } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java index 6a35128e7..39f7d168b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementActivity.java @@ -205,6 +205,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements if (isResumed && continueClicked && areEssentialPermissionsGranted()) { if (isWifiReady() && isBluetoothReady()) { LOG.info("Wifi and Bluetooth are ready"); + viewModel.startListening(); showQrCodeFragment(); } else { if (shouldEnableWifi()) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementFragment.java index 34a29281c..6d179af89 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/KeyAgreementFragment.java @@ -1,9 +1,7 @@ package org.briarproject.briar.android.contact.add.nearby; -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; @@ -12,36 +10,24 @@ 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.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.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.KeyAgreementState; -import org.briarproject.briar.android.fragment.BaseEventFragment; +import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeStarted; +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 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 androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; 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.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; -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 @ParametersNotNullByDefault -public class KeyAgreementFragment extends BaseEventFragment - implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener { +public class KeyAgreementFragment extends BaseFragment + implements 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 ViewModelProvider.Factory viewModelFactory; - @Inject - Provider keyAgreementTaskProvider; - @Inject - PayloadEncoder payloadEncoder; - @Inject - PayloadParser payloadParser; - @Inject - @IoExecutor - Executor ioExecutor; - @Inject - EventBus eventBus; private ContactExchangeViewModel viewModel; private CameraView cameraView; @@ -91,10 +58,6 @@ public class KeyAgreementFragment extends BaseEventFragment private QrCodeView qrCodeView; private TextView status; - private boolean gotRemotePayload; - private volatile boolean gotLocalPayload; - private KeyAgreementTask task; - public static KeyAgreementFragment newInstance() { Bundle args = new Bundle(); KeyAgreementFragment fragment = new KeyAgreementFragment(); @@ -109,11 +72,6 @@ public class KeyAgreementFragment extends BaseEventFragment .get(ContactExchangeViewModel.class); } - @Override - public String getUniqueTag() { - return TAG; - } - @Nullable @Override public View onCreateView(LayoutInflater inflater, @@ -133,16 +91,15 @@ public class KeyAgreementFragment extends BaseEventFragment qrCodeView = view.findViewById(R.id.qr_code_view); qrCodeView.setFullscreenListener(this); - LifecycleOwner lifecycleOwner = getViewLifecycleOwner(); - viewModel.getKeyAgreementState() - .observe(lifecycleOwner, this::onKeyAgreementStateChanged); + viewModel.getState().observe(getViewLifecycleOwner(), + this::onContactAddingStateChanged); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); - cameraView.setPreviewConsumer(new QrCodeDecoder(this)); + cameraView.setPreviewConsumer(viewModel.qrCodeDecoder); } @Override @@ -153,7 +110,16 @@ public class KeyAgreementFragment extends BaseEventFragment } catch (CameraException e) { logCameraExceptionAndFinish(e); } - startListening(); + } + + @Override + public void onStop() { + super.onStop(); + try { + cameraView.stop(); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + } } @Override @@ -178,17 +144,42 @@ public class KeyAgreementFragment extends BaseEventFragment cameraOverlay.invalidate(); } - @Override - public void onStop() { - super.onStop(); - stopListening(); - try { - cameraView.stop(); - } catch (CameraException e) { - logCameraExceptionAndFinish(e); + @UiThread + private void onContactAddingStateChanged(ContactAddingState state) { + if (state instanceof ContactAddingState.KeyAgreementListening) { + Bitmap qrCode = + ((ContactAddingState.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); + 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 private void logCameraExceptionAndFinish(CameraException e) { logException(LOG, WARNING, e); @@ -197,130 +188,6 @@ public class KeyAgreementFragment extends BaseEventFragment 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 protected void finish() { requireActivity().getSupportFragmentManager().popBackStack(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java index f78f62769..ad61baa3c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeDecoder.java @@ -4,7 +4,6 @@ import android.hardware.Camera; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.Size; -import android.os.AsyncTask; import com.google.zxing.BinaryBitmap; import com.google.zxing.LuminanceSource; @@ -15,10 +14,12 @@ import com.google.zxing.Result; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.qrcode.QRCodeReader; +import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import java.util.concurrent.Executor; import java.util.logging.Logger; import androidx.annotation.UiThread; @@ -26,22 +27,23 @@ import androidx.annotation.UiThread; import static com.google.zxing.DecodeHintType.CHARACTER_SET; import static java.util.Collections.singletonMap; import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; -@SuppressWarnings("deprecation") @MethodsNotNullByDefault @ParametersNotNullByDefault class QrCodeDecoder implements PreviewConsumer, PreviewCallback { - private static final Logger LOG = - Logger.getLogger(QrCodeDecoder.class.getName()); + private static final Logger LOG = getLogger(QrCodeDecoder.class.getName()); + private final Executor ioExecutor; private final Reader reader = new QRCodeReader(); private final ResultCallback callback; private Camera camera = null; private int cameraIndex = 0; - QrCodeDecoder(ResultCallback callback) { + QrCodeDecoder(@IoExecutor Executor ioExecutor, ResultCallback callback) { + this.ioExecutor = ioExecutor; this.callback = callback; } @@ -74,8 +76,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback { if (data.length == size.width * size.height * 3 / 2) { CameraInfo info = new CameraInfo(); Camera.getCameraInfo(cameraIndex, info); - new DecoderTask(data, size.width, size.height, - info.orientation).execute(); + decode(data, size.width, size.height, info.orientation); } else { // Camera parameters have changed - ask for a new preview LOG.info("Preview size does not match camera parameters"); @@ -89,43 +90,23 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback { } } - private class DecoderTask extends AsyncTask { - - private final byte[] data; - private final int width, height, orientation; - - private DecoderTask(byte[] data, int width, int height, - int orientation) { - this.data = data; - this.width = width; - this.height = height; - this.orientation = orientation; - } - - @Override - protected Void doInBackground(Void... params) { + private void decode(byte[] data, int width, int height, int orientation) { + ioExecutor.execute(() -> { BinaryBitmap bitmap = binarize(data, width, height, orientation); Result result; try { result = reader.decode(bitmap, singletonMap(CHARACTER_SET, "ISO8859_1")); + callback.onQrCodeDecoded(result); } catch (ReaderException e) { // No barcode found - return null; } catch (RuntimeException e) { LOG.warning("Invalid preview frame"); - return null; } finally { reader.reset(); } - callback.handleResult(result); - return null; - } - - @Override - protected void onPostExecute(Void result) { - askForPreviewFrame(); - } + }); + askForPreviewFrame(); } private static BinaryBitmap binarize(byte[] data, int width, int height, @@ -143,7 +124,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback { @NotNullByDefault interface ResultCallback { - - void handleResult(Result result); + @IoExecutor + void onQrCodeDecoded(Result result); } }