From 9d01de9868270d2efa49240e495605ddafb7acff Mon Sep 17 00:00:00 2001 From: ameba23 Date: Mon, 12 Apr 2021 12:21:16 +0200 Subject: [PATCH] Secret owner return shard - activity, view model and fragment --- .../android/activity/ActivityComponent.java | 3 + .../recover/OwnerReturnShardActivity.java | 192 ++++++++++++++ .../recover/OwnerReturnShardFragment.java | 158 ++++++++++++ .../recover/OwnerReturnShardViewModel.java | 236 ++++++++++++++++++ 4 files changed, 589 insertions(+) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardActivity.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardFragment.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardViewModel.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 91a288110..eb409419c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -88,6 +88,7 @@ import org.briarproject.briar.android.socialbackup.ExistingBackupFragment; import org.briarproject.briar.android.socialbackup.recover.CustodianReturnShardActivity; import org.briarproject.briar.android.socialbackup.recover.CustodianReturnShardFragment; import org.briarproject.briar.android.socialbackup.recover.OwnerRecoveryModeExplainerFragment; +import org.briarproject.briar.android.socialbackup.recover.OwnerReturnShardFragment; import org.briarproject.briar.android.socialbackup.recover.RecoverActivity; import org.briarproject.briar.android.socialbackup.ShardsSentFragment; import org.briarproject.briar.android.socialbackup.ThresholdSelectorFragment; @@ -287,4 +288,6 @@ public interface ActivityComponent { void inject(CustodianRecoveryModeExplainerFragment custodianRecoveryModeExplainerFragment); void inject(CustodianReturnShardFragment custodianReturnShardFragment); + + void inject(OwnerReturnShardFragment ownerReturnShardFragment); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardActivity.java new file mode 100644 index 000000000..c2982f698 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardActivity.java @@ -0,0 +1,192 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; +import android.widget.Toast; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactErrorFragment; +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.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Logger.getLogger; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class OwnerReturnShardActivity extends BaseActivity + implements BaseFragment.BaseFragmentListener { + + private static final Logger LOG = + getLogger(OwnerReturnShardActivity.class.getName()); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private OwnerReturnShardViewModel viewModel; + +// private final ActivityResultLauncher permissionLauncher = +// registerForActivityResult( +// new ActivityResultContracts.RequestMultiplePermissions(), +// r -> +// permissionManager.onRequestPermissionResult(r, +// viewModel::showQrCodeFragmentIfAllowed)); + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(OwnerReturnShardViewModel.class); + } + + // TODO the following two methods should be injected from messageParser +// private Shard parseShardMessage(BdfList body) throws FormatException { +// // Message type, secret ID, shard +// byte[] secretId = body.getRaw(1); +// byte[] shard = body.getRaw(2); +// return new Shard(secretId, shard); +// } +// +// private ReturnShardPayload parseReturnShardPayload(BdfList body) +// throws FormatException { +// checkSize(body, 2); +// Shard shard = parseShardMessage(body.getList(0)); +// org.briarproject.briar.api.socialbackup.BackupPayload backupPayload = +// new BackupPayload(body.getRaw(1)); +// return new ReturnShardPayload(shard, backupPayload); +// } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + setContentView(R.layout.activity_fragment_container); + if (state == null) { + showInitialFragment(new OwnerRecoveryModeExplainerFragment()); + } +// viewModel.getCheckPermissions().observeEvent(this, check -> +// permissionManager.checkPermissions()); +// viewModel.getRequestBluetoothDiscoverable().observeEvent(this, r -> +// requestBluetoothDiscoverable()); // never false + viewModel.getShowQrCodeFragment().observeEvent(this, show -> { + if (show) showQrCodeFragment(); + }); +// viewModel.getState() +// .observe(this, this::onReturnShardStateChanged); + } + + @Override + public void onStart() { + super.onStart(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); +// viewModel.setIsActivityResumed(true); + } + + @Override + protected void onPause() { + super.onPause(); +// viewModel.setIsActivityResumed(false); + } + + @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 showQrCodeFragment() { + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentByTag(OwnerReturnShardFragment.TAG) == null) { + BaseFragment f = OwnerReturnShardFragment.newInstance(); + fm.beginTransaction() + .replace(R.id.fragmentContainer, f, f.getUniqueTag()) + .addToBackStack(f.getUniqueTag()) + .commit(); + } + } + + private void onReturnShardStateChanged(ReturnShardState state) { + if (state instanceof ReturnShardState.SocialBackupExchangeFinished) { + ReturnShardState.SocialBackupExchangeResult result = + ((ReturnShardState.SocialBackupExchangeFinished) state).result; + onSocialBackupExchangeResult(result); + } else if (state instanceof ReturnShardState.Failed) { + Boolean qrCodeTooOld = + ((ReturnShardState.Failed) state).qrCodeTooOld; + onAddingContactFailed(qrCodeTooOld); + } + } + + private void onSocialBackupExchangeResult( + ReturnShardState.SocialBackupExchangeResult result) { + if (result instanceof ReturnShardState.SocialBackupExchangeResult.Success) { +// String text = getString(R.string.contact_added_toast, contactName); + Toast.makeText(this, "Shard return successful", LENGTH_LONG).show(); + supportFinishAfterTransition(); + } else if (result instanceof ReturnShardState.SocialBackupExchangeResult.Error) { + showErrorFragment(); + } 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()); + } + + @Override + @Deprecated + public void runOnDbThread(Runnable runnable) { + throw new RuntimeException("Don't use this deprecated method here."); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardFragment.java new file mode 100644 index 000000000..8582466d9 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardFragment.java @@ -0,0 +1,158 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.contact.add.nearby.CameraException; +import org.briarproject.briar.android.contact.add.nearby.CameraView; +import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.view.QrCodeView; +import org.briarproject.briar.api.socialbackup.recovery.SecretOwnerTask; + +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.ViewModelProvider; + +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.logException; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class OwnerReturnShardFragment extends BaseFragment + implements QrCodeView.FullscreenListener { + + public static final String TAG = OwnerReturnShardFragment.class.getName(); + + private static final Logger LOG = Logger.getLogger(TAG); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private OwnerReturnShardViewModel viewModel; + private LinearLayout cameraOverlay; + private View statusView; + private QrCodeView qrCodeView; + private TextView status; + + public static OwnerReturnShardFragment newInstance() { + Bundle args = new Bundle(); + OwnerReturnShardFragment fragment = new OwnerReturnShardFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(OwnerReturnShardViewModel.class); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_keyagreement_qr, container, + false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + cameraOverlay = view.findViewById(R.id.camera_overlay); + statusView = view.findViewById(R.id.status_container); + status = view.findViewById(R.id.connect_status); + qrCodeView = view.findViewById(R.id.qr_code_view); + qrCodeView.setFullscreenListener(this); + + viewModel.getState().observe(getViewLifecycleOwner(), + this::onReturnShardStateChanged); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); + } + + @Override + public void onDestroy() { + requireActivity() + .setRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED); + super.onDestroy(); + } + + @Override + public void setFullscreen(boolean fullscreen) { + LinearLayout.LayoutParams statusParams, qrCodeParams; + if (fullscreen) { + // Grow the QR code view to fill its parent + statusParams = new LinearLayout.LayoutParams(0, 0, 0f); + qrCodeParams = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f); + } else { + // Shrink the QR code view to fill half its parent + if (cameraOverlay.getOrientation() == HORIZONTAL) { + statusParams = new LinearLayout.LayoutParams(0, MATCH_PARENT, 1f); + qrCodeParams = new LinearLayout.LayoutParams(0, MATCH_PARENT, 1f); + } else { + statusParams = new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1f); + qrCodeParams = new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1f); + } + } + statusView.setLayoutParams(statusParams); + qrCodeView.setLayoutParams(qrCodeParams); + cameraOverlay.invalidate(); + } + + @UiThread + private void onReturnShardStateChanged(@Nullable SecretOwnerTask.State state) { + if (state instanceof SecretOwnerTask.State.Listening) { + Bitmap qrCode = + ((ReturnShardState.KeyAgreementListening) state).qrCode; + qrCodeView.setQrCode(qrCode); + } else if (state instanceof SecretOwnerTask.State.ReceivingShard) { + statusView.setVisibility(VISIBLE); + status.setText(R.string.connecting_to_device); + } else if (state instanceof SecretOwnerTask.State.SendingAck) { + status.setText(R.string.waiting_for_contact_to_scan); + } else if (state instanceof SecretOwnerTask.State.Success) { + status.setText("Success"); + } else if (state instanceof SecretOwnerTask.State.Failure) { + // the activity will replace this fragment with an error fragment + statusView.setVisibility(INVISIBLE); + } + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + protected void finish() { + requireActivity().getSupportFragmentManager().popBackStack(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardViewModel.java new file mode 100644 index 000000000..a4bb76a43 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardViewModel.java @@ -0,0 +1,236 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.app.Application; +import android.graphics.Bitmap; +import android.util.DisplayMetrics; + +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.keyagreement.KeyAgreementResult; +import org.briarproject.bramble.api.keyagreement.Payload; +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.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.contact.add.nearby.QrCodeUtils; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; +import org.briarproject.briar.api.socialbackup.ReturnShardPayload; +import org.briarproject.briar.api.socialbackup.recovery.SecretOwnerTask; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +class OwnerReturnShardViewModel extends AndroidViewModel { + + private static final Logger LOG = + getLogger(OwnerReturnShardViewModel.class.getName()); + + @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 + private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + private ReturnShardPayload returnShardPayload; + + private final AndroidExecutor androidExecutor; + private final Executor ioExecutor; + + private final MutableLiveEvent showQrCodeFragment = + new MutableLiveEvent<>(); + private final MutableLiveData state = + new MutableLiveData<>(); + + private boolean wasContinueClicked = false; + private boolean isActivityResumed = false; + + @Inject + OwnerReturnShardViewModel(Application app, + AndroidExecutor androidExecutor, + @IoExecutor Executor ioExecutor) { + super(app); + this.androidExecutor = androidExecutor; + this.ioExecutor = ioExecutor; +// IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); + } + + @Override + protected void onCleared() { + super.onCleared(); + stopListening(); + } + + @UiThread + void onContinueClicked() { + wasContinueClicked = true; + startShardReturn(); + } + + @UiThread + void startShardReturn() { + // 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, we may need to enable wifi and +// hasEnabledWifi = false; + startListening(); + showQrCodeFragment.setEvent(true); + } + + @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.SocialBackupExchangeStarted()); +// } 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()); +// } +// } + + /** + * 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 SecretOwnerTask.State.Listening(qrCode)); + }); + } + + @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 { +// if (sending) { +// socialBackupExchangeManager.sendReturnShard(conn, masterKey, alice, returnShardPayload); +// } else { +// returnShardPayload = socialBackupExchangeManager.receiveReturnShard(conn, masterKey, alice); +// } +// ReturnShardState.SocialBackupExchangeResult.Success +// success = +// new ReturnShardState.SocialBackupExchangeResult.Success(); +// state.postValue( +// new ReturnShardState.SocialBackupExchangeFinished(success)); +// } catch (ContactExistsException e) { +// tryToClose(conn); +// ReturnShardState.SocialBackupExchangeResult.Error +// error = new ReturnShardState.SocialBackupExchangeResult.Error( +// e.getRemoteAuthor()); +// state.postValue( +// new ReturnShardState.SocialBackupExchangeFinished(error)); +// } catch (DbException | IOException e) { +// tryToClose(conn); +// logException(LOG, WARNING, e); +// ReturnShardState.SocialBackupExchangeResult.Error +// error = +// new ReturnShardState.SocialBackupExchangeResult.Error(null); +// state.postValue( +// new ReturnShardState.SocialBackupExchangeFinished(error)); +// } +// }); +// } + + + /** + * 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(); + } + + LiveEvent getShowQrCodeFragment() { + return showQrCodeFragment; + } + + LiveData getState() { + return state; + } +}