diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index d8353ebe9..7596c8532 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -157,25 +157,7 @@ - - - - - - - - + + + + + { - if (!socialBackupManager.amCustodian(txn, contactId)) { - throw new DbException(); - } - CustodianRecoveryModeExplainerFragment fragment = - new CustodianRecoveryModeExplainerFragment(); - showInitialFragment(fragment); - }); - } catch (DbException e) { - // TODO improve this - Toast.makeText(this, - "You do not hold a backup shard from this contact", - Toast.LENGTH_SHORT).show(); - finish(); - } - } - - @Override - public void scanQrButtonClicked() { - try { - db.transaction(false, txn -> { - byte[] returnShardPayloadBytes = socialBackupManager - .getReturnShardPayloadBytes(txn, contactId); - - Intent i = new Intent(this, ReturnShardActivity.class); - i.putExtra(RETURN_SHARD_PAYLOAD, returnShardPayloadBytes); - startActivity(i); - }); - } catch (DbException e) { - Toast.makeText(this, - "Error reading social backup from storage", - Toast.LENGTH_SHORT).show(); - finish(); - } - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/CustodianRecoveryModeExplainerFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/CustodianRecoveryModeExplainerFragment.java deleted file mode 100644 index 50660fc99..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/CustodianRecoveryModeExplainerFragment.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.briarproject.briar.android.socialbackup; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import org.briarproject.briar.android.fragment.BaseFragment; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import org.briarproject.briar.R; - -public class CustodianRecoveryModeExplainerFragment extends BaseFragment { - - protected CustodianScanQrButtonListener listener; - - public static final String TAG = CustodianRecoveryModeExplainerFragment.class.getName(); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requireActivity().setTitle(R.string.title_help_recover); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable - ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_recovery_custodian_explainer, - container, false); - - Button button = view.findViewById(R.id.button); - button.setOnClickListener(e -> listener.scanQrButtonClicked()); - return view; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - listener = (CustodianScanQrButtonListener) context; - } - - @Override - public String getUniqueTag() { - return TAG; - } -} - diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DarkCrystalImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DarkCrystalImpl.java index b7cee74a8..d8b97b007 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DarkCrystalImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DarkCrystalImpl.java @@ -1,8 +1,9 @@ package org.briarproject.briar.android.socialbackup; + import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.api.socialbackup.Shard; import org.briarproject.briar.api.socialbackup.DarkCrystal; +import org.briarproject.briar.api.socialbackup.Shard; import org.magmacollective.darkcrystal.secretsharingwrapper.SecretSharingWrapper; import java.security.GeneralSecurityException; @@ -13,14 +14,12 @@ import java.util.Random; import javax.inject.Inject; -import dagger.Provides; - import static org.briarproject.briar.socialbackup.SocialBackupConstants.SECRET_ID_BYTES; @NotNullByDefault public class DarkCrystalImpl implements DarkCrystal { - @Inject + @Inject DarkCrystalImpl() { } @@ -30,7 +29,8 @@ public class DarkCrystalImpl implements DarkCrystal { Random random = new Random(); byte[] secretId = new byte[SECRET_ID_BYTES]; random.nextBytes(secretId); - List shardsBytes = SecretSharingWrapper.share(secret.getBytes(), numShards, threshold); + List shardsBytes = SecretSharingWrapper + .share(secret.getBytes(), numShards, threshold); List shards = new ArrayList<>(numShards); for (byte[] shardBytes : shardsBytes) { shards.add(new Shard(secretId, shardBytes)); @@ -44,9 +44,10 @@ public class DarkCrystalImpl implements DarkCrystal { // Check each shard has the same secret Id byte[] secretId = shards.get(0).getSecretId(); for (Shard shard : shards) { - if (!Arrays.equals(shard.getSecretId(), secretId)) throw new GeneralSecurityException(); + if (!Arrays.equals(shard.getSecretId(), secretId)) + throw new GeneralSecurityException(); } - List shardsBytes = new ArrayList<>(shards.size()); + List shardsBytes = new ArrayList<>(shards.size()); for (Shard shard : shards) { shardsBytes.add(shard.getShard()); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DistributedBackupActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DistributedBackupActivity.java index 4f8b29ea8..35679e565 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DistributedBackupActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/DistributedBackupActivity.java @@ -61,7 +61,7 @@ public class DistributedBackupActivity extends BriarActivity implements @Override public void contactsSelected(Collection contacts) { Toast.makeText(this, - String.format("selected %d contacts", contacts.size()), + String.format("Selected %d contacts", contacts.size()), Toast.LENGTH_SHORT).show(); custodians = contacts; ThresholdSelectorFragment fragment = diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianRecoveryModeExplainerFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianRecoveryModeExplainerFragment.java new file mode 100644 index 000000000..49df13ebf --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianRecoveryModeExplainerFragment.java @@ -0,0 +1,59 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +public class CustodianRecoveryModeExplainerFragment extends BaseFragment { + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private CustodianReturnShardViewModel viewModel; + + public static final String TAG = + CustodianRecoveryModeExplainerFragment.class.getName(); + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(CustodianReturnShardViewModel.class); + } +// @Override +// public void onCreate(@Nullable Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// requireActivity().setTitle(R.string.title_help_recover); +// } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable + ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = + inflater.inflate(R.layout.fragment_recovery_custodian_explainer, + container, false); + + Button button = view.findViewById(R.id.button); + button.setOnClickListener(e -> viewModel.onContinueClicked()); + return view; + } + + @Override + public String getUniqueTag() { + return TAG; + } +} + diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardActivity.java new file mode 100644 index 000000000..3346e1ce0 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardActivity.java @@ -0,0 +1,105 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.api.socialbackup.recovery.CustodianTask; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import static java.util.logging.Logger.getLogger; +import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID; + +public class CustodianReturnShardActivity extends BriarActivity + implements BaseFragment.BaseFragmentListener { + + private CustodianReturnShardViewModel viewModel; + private static final Logger LOG = + getLogger(CustodianReturnShardActivity.class.getName()); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(CustodianReturnShardViewModel.class); + } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + setContentView(R.layout.activity_fragment_container); + if (state == null) { + Intent intent = getIntent(); + int id = intent.getIntExtra(CONTACT_ID, -1); + if (id == -1) throw new IllegalStateException("No ContactId"); + ContactId contactId = new ContactId(id); + + try { + viewModel.start(contactId); + } catch (IOException e) { + // TODO improve this + Toast.makeText(this, + "It looks like you are not connected to a Wifi network", + Toast.LENGTH_SHORT).show(); + } catch (DbException e) { + Toast.makeText(this, + "You do not hold a backup piece for this contact", + Toast.LENGTH_SHORT).show(); + finish(); + } + showInitialFragment(new CustodianRecoveryModeExplainerFragment()); + } + viewModel.getShowCameraFragment().observeEvent(this, show -> { + if (show) showCameraFragment(); + }); + viewModel.getSuccessDismissed().observeEvent(this, dismissed -> { + if (dismissed) finish(); + }); + viewModel.getState() + .observe(this, this::onReturnShardStateChanged); + } + + private void onReturnShardStateChanged(CustodianTask.State state) { + if (state instanceof CustodianTask.State.Success) { + CustodianReturnShardSuccessFragment fragment = new CustodianReturnShardSuccessFragment(); + showNextFragment(fragment); + } else if (state instanceof CustodianTask.State.Failure) { + // TODO error fragment here + // TODO handle reason + Toast.makeText(this, + "Backup piece transfer failed", + Toast.LENGTH_SHORT).show(); + finish(); + } + } + + private void showCameraFragment() { + // FIXME #824 + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentByTag(CustodianReturnShardFragment.TAG) == null) { + BaseFragment f = CustodianReturnShardFragment.newInstance(); + fm.beginTransaction() + .replace(R.id.fragmentContainer, f, f.getUniqueTag()) + .addToBackStack(f.getUniqueTag()) + .commit(); + } + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardFragment.java similarity index 73% rename from briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardFragment.java rename to briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardFragment.java index c92de05ea..d893b5b0b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardFragment.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.socialbackup.recover; -import android.graphics.Bitmap; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -17,6 +16,7 @@ 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.CustodianTask; import java.util.logging.Logger; @@ -38,26 +38,25 @@ import static org.briarproject.bramble.util.LogUtils.logException; @MethodsNotNullByDefault @ParametersNotNullByDefault -public class ReturnShardFragment extends BaseFragment +public class CustodianReturnShardFragment extends BaseFragment implements QrCodeView.FullscreenListener { - public static final String TAG = org.briarproject.briar.android.contact.add.nearby.AddNearbyContactFragment.class.getName(); + public static final String TAG = CustodianReturnShardFragment.class.getName(); private static final Logger LOG = Logger.getLogger(TAG); @Inject ViewModelProvider.Factory viewModelFactory; - private ReturnShardViewModel viewModel; + private CustodianReturnShardViewModel viewModel; private CameraView cameraView; private LinearLayout cameraOverlay; private View statusView; - private QrCodeView qrCodeView; private TextView status; - public static ReturnShardFragment newInstance() { + public static CustodianReturnShardFragment newInstance() { Bundle args = new Bundle(); - ReturnShardFragment fragment = new ReturnShardFragment(); + CustodianReturnShardFragment fragment = new CustodianReturnShardFragment(); fragment.setArguments(args); return fragment; } @@ -66,7 +65,7 @@ public class ReturnShardFragment extends BaseFragment public void injectFragment(ActivityComponent component) { component.inject(this); viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) - .get(ReturnShardViewModel.class); + .get(CustodianReturnShardViewModel.class); } @Nullable @@ -85,8 +84,6 @@ public class ReturnShardFragment extends BaseFragment 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); @@ -130,31 +127,31 @@ public class ReturnShardFragment extends BaseFragment 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 ReturnShardState state) { - if (state instanceof ReturnShardState.KeyAgreementListening) { - Bitmap qrCode = - ((ReturnShardState.KeyAgreementListening) state).qrCode; - qrCodeView.setQrCode(qrCode); - } else if (state instanceof ReturnShardState.QrCodeScanned) { + private void onReturnShardStateChanged(@Nullable CustodianTask.State state) { + LOG.info("State changed"); +// if (state instanceof CustodianTask.State.Connecting) { +// try { +// cameraView.stop(); +// } catch (CameraException e) { +// logCameraExceptionAndFinish(e); +// } +// cameraView.setVisibility(INVISIBLE); +// statusView.setVisibility(VISIBLE); +// status.setText(R.string.connecting_to_device); + if (state instanceof CustodianTask.State.SendingShard) { try { cameraView.stop(); } catch (CameraException e) { @@ -162,15 +159,13 @@ public class ReturnShardFragment extends BaseFragment } cameraView.setVisibility(INVISIBLE); statusView.setVisibility(VISIBLE); - status.setText(R.string.connecting_to_device); - } else if (state instanceof ReturnShardState.KeyAgreementWaiting) { - status.setText(R.string.waiting_for_contact_to_scan); - } else if (state instanceof ReturnShardState.KeyAgreementStarted) { - qrCodeView.setVisibility(INVISIBLE); - status.setText(R.string.authenticating_with_device); - } else if (state instanceof ReturnShardState.SocialBackupExchangeStarted) { + status.setText("Sending shard"); + } else if (state instanceof CustodianTask.State.ReceivingAck) { + status.setText("Receiving Ack"); + } else if (state instanceof CustodianTask.State.Success) { + // TODO status.setText(R.string.exchanging_contact_details); - } else if (state instanceof ReturnShardState.Failed) { + } else if (state instanceof CustodianTask.State.Failure) { // the activity will replace this fragment with an error fragment statusView.setVisibility(INVISIBLE); cameraView.setVisibility(INVISIBLE); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardModule.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardModule.java new file mode 100644 index 000000000..d54eb18b0 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardModule.java @@ -0,0 +1,19 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import org.briarproject.briar.android.viewmodel.ViewModelKey; + +import androidx.lifecycle.ViewModel; +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public abstract class CustodianReturnShardModule { + + @Binds + @IntoMap + @ViewModelKey(CustodianReturnShardViewModel.class) + abstract ViewModel bindCustodianReturnShardViewModel( + CustodianReturnShardViewModel custodianReturnShardViewModel); + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardSuccessFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardSuccessFragment.java new file mode 100644 index 000000000..b058d1bb7 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardSuccessFragment.java @@ -0,0 +1,59 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class CustodianReturnShardSuccessFragment extends + BaseFragment { + + public static final String TAG = + CustodianReturnShardFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private CustodianReturnShardViewModel viewModel; + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(CustodianReturnShardViewModel.class); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_recovery_custodian_done, + container, false); + + Button button = view.findViewById(R.id.button); + button.setOnClickListener(e -> viewModel.onSuccessDismissed()); + + return view; + } + + @Override + public String getUniqueTag() { + return TAG; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardViewModel.java new file mode 100644 index 000000000..d3970c2ed --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardViewModel.java @@ -0,0 +1,189 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.app.Application; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.widget.Toast; + +import com.google.zxing.Result; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.R; +import org.briarproject.briar.android.contact.add.nearby.QrCodeDecoder; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; +import org.briarproject.briar.api.socialbackup.SocialBackupManager; +import org.briarproject.briar.api.socialbackup.recovery.CustodianTask; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static android.content.Context.WIFI_SERVICE; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; + +public class CustodianReturnShardViewModel extends AndroidViewModel + implements QrCodeDecoder.ResultCallback, CustodianTask.Observer { + + private static final Logger LOG = + getLogger(CustodianReturnShardViewModel.class.getName()); + + private final AndroidExecutor androidExecutor; + private final Executor ioExecutor; + private final SocialBackupManager socialBackupManager; + private final DatabaseComponent db; + final QrCodeDecoder qrCodeDecoder; + private boolean wasContinueClicked = false; + private boolean qrCodeRead = false; + private WifiManager wifiManager; + private final MutableLiveEvent showCameraFragment = + new MutableLiveEvent<>(); + private final MutableLiveEvent successDismissed = + new MutableLiveEvent<>(); + private final MutableLiveData state = + new MutableLiveData<>(); + private final CustodianTask task; + private byte[] returnShardPayloadBytes; + + @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 + private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + @Inject + public CustodianReturnShardViewModel( + @NonNull Application app, + @IoExecutor Executor ioExecutor, + SocialBackupManager socialBackupManager, + DatabaseComponent db, + CustodianTask task, + AndroidExecutor androidExecutor) { + super(app); + + this.androidExecutor = androidExecutor; + this.ioExecutor = ioExecutor; + this.socialBackupManager = socialBackupManager; + this.db = db; + this.task = task; + qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this); + wifiManager = (WifiManager) app.getSystemService(WIFI_SERVICE); + } + + private InetAddress getWifiIpv4Address() { + if (wifiManager == null) return null; + // If we're connected to a wifi network, return its address + WifiInfo info = wifiManager.getConnectionInfo(); + if (info != null && info.getIpAddress() != 0) { + return intToInetAddress(info.getIpAddress()); + } + return null; + } + + // TODO this is not the right place for this + private InetAddress intToInetAddress(int ip) { + byte[] ipBytes = new byte[4]; + ipBytes[0] = (byte) (ip & 0xFF); + ipBytes[1] = (byte) ((ip >> 8) & 0xFF); + ipBytes[2] = (byte) ((ip >> 16) & 0xFF); + ipBytes[3] = (byte) ((ip >> 24) & 0xFF); + try { + return InetAddress.getByAddress(ipBytes); + } catch (UnknownHostException e) { + // Should only be thrown if address has illegal length + throw new AssertionError(e); + } + } + + public void start(ContactId contactId) throws DbException, IOException { + InetAddress inetAddress = getWifiIpv4Address(); + LOG.info("Client InetAddress: " + inetAddress); + if (inetAddress == null) + throw new IOException("Cannot get IP on local wifi"); + + db.transaction(false, txn -> { + if (!socialBackupManager.amCustodian(txn, contactId)) { + throw new DbException(); + } + returnShardPayloadBytes = socialBackupManager + .getReturnShardPayloadBytes(txn, contactId); + }); + task.cancel(); + task.start(this, returnShardPayloadBytes); + } + + @IoExecutor + @Override + public void onQrCodeDecoded(Result result) { + LOG.info("Got result from decoder"); + if (qrCodeRead) return; + try { + byte[] payloadBytes = result.getText().getBytes(ISO_8859_1); + if (LOG.isLoggable(INFO)) + LOG.info("Remote payload is " + payloadBytes.length + " bytes"); + ioExecutor.execute(() -> { + task.qrCodeDecoded(payloadBytes); + }); + } catch (IllegalArgumentException e) { + LOG.log(WARNING, "QR Code Invalid", e); + androidExecutor.runOnUiThread(() -> Toast.makeText(getApplication(), + R.string.qr_code_invalid, LENGTH_LONG).show()); + ioExecutor.execute(() -> { + task.qrCodeDecoded(null); + }); + } + } + + @UiThread + public void onContinueClicked() { + wasContinueClicked = true; +// checkPermissions.setEvent(true); + showCameraFragment.setEvent(true); + } + + @UiThread + public void onSuccessDismissed() { + successDismissed.setEvent(true); + } + + + QrCodeDecoder getQrCodeDecoder() { + return qrCodeDecoder; + } + + LiveEvent getShowCameraFragment() { + return showCameraFragment; + } + + LiveEvent getSuccessDismissed() { + return successDismissed; + } + + LiveData getState() { + return state; + } + + @Override + public void onStateChanged(CustodianTask.State state) { + this.state.postValue(state); + // Connecting, SendingShard, ReceivingAck, Success, Failure + if (state instanceof CustodianTask.State.SendingShard) { + qrCodeRead = true; + } + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeExplainerFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeExplainerFragment.java index 92e38aaff..00da94a01 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeExplainerFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeExplainerFragment.java @@ -24,7 +24,7 @@ public class OwnerRecoveryModeExplainerFragment extends BaseFragment { @Inject ViewModelProvider.Factory viewModelFactory; - private ReturnShardViewModel viewModel; + private OwnerReturnShardViewModel viewModel; // @Override // public void onCreate(@Nullable Bundle savedInstanceState) { @@ -36,7 +36,7 @@ public class OwnerRecoveryModeExplainerFragment extends BaseFragment { public void injectFragment(ActivityComponent component) { component.inject(this); viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) - .get(ReturnShardViewModel.class); + .get(OwnerReturnShardViewModel.class); } @Override @@ -47,11 +47,10 @@ public class OwnerRecoveryModeExplainerFragment extends BaseFragment { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - System.out.println("GOt here *************************************"); View view = inflater.inflate(R.layout.fragment_recovery_owner_explainer, container, false); Button button = view.findViewById(R.id.beginButton); - button.setOnClickListener(e -> viewModel.onContinueClicked()); + button.setOnClickListener(e -> viewModel.onStartClicked()); return view; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/OwnerRecoveryModeMainFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeMainFragment.java similarity index 53% rename from briar-android/src/main/java/org/briarproject/briar/android/socialbackup/OwnerRecoveryModeMainFragment.java rename to briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeMainFragment.java index 3191126ee..aeeb4eac0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/OwnerRecoveryModeMainFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerRecoveryModeMainFragment.java @@ -1,4 +1,4 @@ -package org.briarproject.briar.android.socialbackup; +package org.briarproject.briar.android.socialbackup.recover; import android.content.Context; import android.os.Bundle; @@ -9,44 +9,47 @@ import android.widget.Button; import android.widget.TextView; import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.socialbackup.ScanQrButtonListener; + +import javax.inject.Inject; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; public class OwnerRecoveryModeMainFragment extends BaseFragment { - protected ScanQrButtonListener listener; - public static final String NUM_RECOVERED = "num_recovered"; public static final String TAG = OwnerRecoveryModeMainFragment.class.getName(); - public static OwnerRecoveryModeMainFragment newInstance(int numRecovered) { - Bundle args = new Bundle(); - args.putInt(NUM_RECOVERED, numRecovered); - OwnerRecoveryModeMainFragment fragment = - new OwnerRecoveryModeMainFragment(); - fragment.setArguments(args); - return fragment; - } + @Inject + ViewModelProvider.Factory viewModelFactory; - private int numShards; + private OwnerReturnShardViewModel viewModel; + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(OwnerReturnShardViewModel.class); + } @Override public String getUniqueTag() { return TAG; } - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requireActivity().setTitle(R.string.title_recovery_mode); - - Bundle args = requireArguments(); - numShards = args.getInt(NUM_RECOVERED); - } +// @Override +// public void onCreate(@Nullable Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// requireActivity().setTitle(R.string.title_recovery_mode); +// +// Bundle args = requireArguments(); +// } @Nullable @Override @@ -57,17 +60,10 @@ public class OwnerRecoveryModeMainFragment extends BaseFragment { container, false); TextView textViewCount = view.findViewById(R.id.textViewShardCount); - textViewCount.setText(String.format("%d", numShards)); + textViewCount.setText(String.format("%d", viewModel.getNumberOfShards())); Button button = view.findViewById(R.id.button); - button.setOnClickListener(e -> listener.scanQrButtonClicked()); + button.setOnClickListener(e -> viewModel.onContinueClicked()); return view; } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - listener = (ScanQrButtonListener) context; - } - } 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..205967c4f --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardActivity.java @@ -0,0 +1,177 @@ +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.FormatException; +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.fragment.BaseFragment; +import org.briarproject.briar.api.socialbackup.ReturnShardPayload; +import org.briarproject.briar.api.socialbackup.recovery.SecretOwnerTask; + +import java.security.GeneralSecurityException; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +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); + } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + setContentView(R.layout.activity_fragment_container); + if (state == null) { + showInitialFragment(new OwnerRecoveryModeExplainerFragment()); + } + viewModel.getShowQrCodeFragment().observeEvent(this, show -> { + if (show) { + viewModel.startListening(); + showQrCodeFragment(); + } + }); + viewModel.getStartClicked().observeEvent(this, start -> { + if (start) { + showNextFragment(new OwnerRecoveryModeMainFragment()); + } + }); + 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() { + // TODO should we cancel the return shard task here? + super.onBackPressed(); + } + + private void showQrCodeFragment() { + LOG.info("showQrCodeFragment called"); + 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(SecretOwnerTask.State state) { + if (state instanceof SecretOwnerTask.State.Success) { + ReturnShardPayload shardPayload = ((SecretOwnerTask.State.Success) state).getRemotePayload(); + boolean added = viewModel.addToShardSet(shardPayload); + Toast.makeText(this, + "Success - got shard" + (added ? "" : " duplicate"), + Toast.LENGTH_SHORT).show(); + if (added && viewModel.canRecover()) { + LOG.info("Secret key recovered"); + int version = 0; + try { + version = viewModel.recover(); + } catch (GeneralSecurityException e) { + LOG.warning("Unable to decrypt backup" + e.toString()); + Toast.makeText(this, + "Unable to decrypt backup", + Toast.LENGTH_LONG).show(); + return; + } catch (FormatException e) { + LOG.warning("Unable to parse backup" + e.getMessage() + e.getStackTrace().toString()); + Toast.makeText(this, + "Unable to parse backup", + Toast.LENGTH_LONG).show(); + return; + } + Toast.makeText(this, + "Account recovered! " + version, + Toast.LENGTH_LONG).show(); + finish(); + return; + } + onBackPressed(); + } else if (state instanceof SecretOwnerTask.State.Failure) { + // TODO error screen, handle reason + Toast.makeText(this, + "Shard return failed!", + Toast.LENGTH_SHORT).show(); + onBackPressed(); +// showNextFragment(new OwnerRecoveryModeExplainerFragment()); + } + } + +// private void showErrorFragment() { +// // TODO change this for an appropriate error message fragment +// 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..686f14cf7 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardFragment.java @@ -0,0 +1,162 @@ +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 org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; +import 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; + +@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); + Bitmap qrCodeBitmap = viewModel.getQrCodeBitmap(); + if (qrCodeBitmap != null) { + qrCodeView.setQrCode(qrCodeBitmap); + } + } + + @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 = viewModel.getQrCodeBitmap(); + 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/ReturnShardModule.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java similarity index 57% rename from briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardModule.java rename to briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java index 749e37a90..b2f2da6e4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java @@ -9,12 +9,12 @@ import dagger.multibindings.IntoMap; @Module -public abstract class ReturnShardModule { +public abstract class OwnerReturnShardModule { @Binds @IntoMap - @ViewModelKey(ReturnShardViewModel.class) - abstract ViewModel bindContactExchangeViewModel( - ReturnShardViewModel returnShardViewModel); + @ViewModelKey(OwnerReturnShardViewModel.class) + abstract ViewModel bindOwnerReturnShardViewModel( + OwnerReturnShardViewModel ownerReturnShardViewModel); } 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..fa30f467e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardViewModel.java @@ -0,0 +1,264 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.app.Application; +import android.graphics.Bitmap; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.util.DisplayMetrics; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +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.BackupPayload; +import org.briarproject.briar.api.socialbackup.DarkCrystal; +import org.briarproject.briar.api.socialbackup.ReturnShardPayload; +import org.briarproject.briar.api.socialbackup.Shard; +import org.briarproject.briar.api.socialbackup.recovery.SecretOwnerTask; +import org.briarproject.briar.socialbackup.BackupPayloadDecoder; +import org.briarproject.briar.socialbackup.SocialBackup; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +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 android.content.Context.WIFI_SERVICE; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; + +@NotNullByDefault +class OwnerReturnShardViewModel extends AndroidViewModel + implements SecretOwnerTask.Observer { + + 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 SecretOwnerTask task; + private final DarkCrystal darkCrystal; + private final BackupPayloadDecoder backupPayloadDecoder; + + private final MutableLiveEvent showQrCodeFragment = + new MutableLiveEvent<>(); + private final MutableLiveData state = + new MutableLiveData<>(); + private final MutableLiveEvent startClicked = + new MutableLiveEvent<>(); + private boolean wasContinueClicked = false; + private boolean isActivityResumed = false; + private ArrayList recoveredShards = new ArrayList<>(); + private Bitmap qrCodeBitmap; + private WifiManager wifiManager; + private SecretKey secretKey; + + @Inject + OwnerReturnShardViewModel(Application app, + AndroidExecutor androidExecutor, + SecretOwnerTask task, + DarkCrystal darkCrystal, + BackupPayloadDecoder backupPayloadDecoder, + @IoExecutor Executor ioExecutor) { + super(app); + this.androidExecutor = androidExecutor; + this.ioExecutor = ioExecutor; + this.backupPayloadDecoder = backupPayloadDecoder; + this.darkCrystal = darkCrystal; + this.task = task; + wifiManager = (WifiManager) app.getSystemService(WIFI_SERVICE); + +// IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); + } + + private InetAddress getWifiIpv4Address() { + if (wifiManager == null) return null; + // If we're connected to a wifi network, return its address + WifiInfo info = wifiManager.getConnectionInfo(); + if (info != null && info.getIpAddress() != 0) { + return intToInetAddress(info.getIpAddress()); + } + return null; + } + + // TODO this is not the right place for this + private InetAddress intToInetAddress(int ip) { + byte[] ipBytes = new byte[4]; + ipBytes[0] = (byte) (ip & 0xFF); + ipBytes[1] = (byte) ((ip >> 8) & 0xFF); + ipBytes[2] = (byte) ((ip >> 16) & 0xFF); + ipBytes[3] = (byte) ((ip >> 24) & 0xFF); + try { + return InetAddress.getByAddress(ipBytes); + } catch (UnknownHostException e) { + // Should only be thrown if address has illegal length + throw new AssertionError(e); + } + } + + @Override + protected void onCleared() { + super.onCleared(); + stopListening(); + } + + @UiThread + void onStartClicked() { + startClicked.setEvent(true); + } + + @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; + showQrCodeFragment.setEvent(true); + } + + @UiThread + public void startListening() { + ioExecutor.execute(() -> { + task.start(this, getWifiIpv4Address()); + }); +// KeyAgreementTask oldTask = task; +// KeyAgreementTask newTask = keyAgreementTaskProvider.get(); +// task = newTask; +// ioExecutor.execute(() -> { +// if (oldTask != null) oldTask.stopListening(); +// newTask.listen(); +// }); + } + + @UiThread + private void stopListening() { + ioExecutor.execute(() -> { + task.cancel(); + }); + } + + + /** + * 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 + */ + void setIsActivityResumed(boolean resumed) { + isActivityResumed = resumed; + // Workaround for + // https://code.google.com/p/android/issues/detail?id=190966 +// showQrCodeFragmentIfAllowed(); + } + + LiveEvent getShowQrCodeFragment() { + return showQrCodeFragment; + } + + LiveEvent getStartClicked() { + return startClicked; + } + + LiveData getState() { + return state; + } + + public Bitmap getQrCodeBitmap() { + LOG.info("getting qrCodeBitmap"); + return qrCodeBitmap; + } + + public int getNumberOfShards() { + return recoveredShards.size(); + } + + @Override + public void onStateChanged(SecretOwnerTask.State state) { + if (state instanceof SecretOwnerTask.State.Listening) { + DisplayMetrics dm = + getApplication().getResources().getDisplayMetrics(); + ioExecutor.execute(() -> { + byte[] payloadBytes = ((SecretOwnerTask.State.Listening) state) + .getLocalPayload(); + if (LOG.isLoggable(INFO)) { + LOG.info("Local QR code payload is " + payloadBytes.length + + " bytes"); + } + // Use ISO 8859-1 to encode bytes directly as a string + String content = new String(payloadBytes, ISO_8859_1); + qrCodeBitmap = QrCodeUtils.createQrCode(dm, content); + this.state.postValue(state); + }); + } else if (state instanceof SecretOwnerTask.State.Success) { +// startClicked.setEvent(true); + this.state.postValue(state); + // TODO do same for failure + } else { + this.state.postValue(state); + } + } + + // TODO figure out how to actually use a set for these objects + public boolean addToShardSet(ReturnShardPayload toAdd) { + boolean found = false; + for (ReturnShardPayload returnShardPayload : recoveredShards) { + if (toAdd.equals(returnShardPayload)) { + found = true; + break; + } + } + if (!found) recoveredShards.add(toAdd); + return !found; + } + + public boolean canRecover() { + ArrayList shards = new ArrayList(); + for (ReturnShardPayload returnShardPayload : recoveredShards) { + // TODO check shards all have same secret id + shards.add(returnShardPayload.getShard()); + } + try { + secretKey = darkCrystal.combineShards(shards); + } catch (GeneralSecurityException e) { + // TODO handle error message + return false; + } + return true; + } + + public int recover() throws FormatException, GeneralSecurityException { + if (secretKey == null) throw new GeneralSecurityException(); + // TODO find backup with highest version number + BackupPayload backupPayload = recoveredShards.get(0).getBackupPayload(); + SocialBackup decodedBackup = backupPayloadDecoder.decodeBackupPayload(secretKey, backupPayload); + int version = decodedBackup.getVersion(); + return version; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RecoverActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RecoverActivity.java deleted file mode 100644 index e76d38efb..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RecoverActivity.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.briarproject.briar.android.socialbackup.recover; - -import android.os.Bundle; -import android.widget.Toast; - -import org.briarproject.briar.R; -import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.activity.BaseActivity; -import org.briarproject.briar.android.fragment.BaseFragment; -import org.briarproject.briar.android.socialbackup.ExplainerDismissedListener; -import org.briarproject.briar.android.socialbackup.OwnerRecoveryModeMainFragment; -import org.briarproject.briar.android.socialbackup.ScanQrButtonListener; - -public class RecoverActivity extends BaseActivity implements - BaseFragment.BaseFragmentListener, ExplainerDismissedListener, - ScanQrButtonListener { - - private int numRecovered; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_recover); - - numRecovered = 0; // TODO - retrieve this from somewhere - - // only show the explainer if we have no shards - if (numRecovered == 0) { - OwnerRecoveryModeExplainerFragment fragment = - new OwnerRecoveryModeExplainerFragment(); - showInitialFragment(fragment); - } else { - OwnerRecoveryModeMainFragment fragment = - OwnerRecoveryModeMainFragment.newInstance(numRecovered); - showInitialFragment(fragment); - } - } - - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); - } - - @Override - public void explainerDismissed() { - OwnerRecoveryModeMainFragment fragment = - OwnerRecoveryModeMainFragment.newInstance(numRecovered); - showNextFragment(fragment); - } - - @Override - public void scanQrButtonClicked() { - // TODO - Toast.makeText(this, - "coming soon...", - Toast.LENGTH_SHORT).show(); - finish(); - } - - @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/ReturnShardActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardActivity.java deleted file mode 100644 index 975c4bf83..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardActivity.java +++ /dev/null @@ -1,266 +0,0 @@ -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.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfList; -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.contact.add.nearby.AddNearbyContactPermissionManager; -import org.briarproject.briar.android.fragment.BaseFragment; -import org.briarproject.briar.android.util.RequestBluetoothDiscoverable; -import org.briarproject.briar.api.socialbackup.BackupPayload; -import org.briarproject.briar.api.socialbackup.ReturnShardPayload; -import org.briarproject.briar.api.socialbackup.Shard; - -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.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static android.widget.Toast.LENGTH_LONG; -import static java.util.logging.Logger.getLogger; -import static org.briarproject.bramble.util.ValidationUtils.checkSize; -import static org.briarproject.briar.android.socialbackup.CustodianHelpRecoverActivity.RETURN_SHARD_PAYLOAD; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -public class ReturnShardActivity extends BaseActivity - implements BaseFragment.BaseFragmentListener { - - private static final Logger LOG = - getLogger(ReturnShardActivity.class.getName()); - - @Inject - ViewModelProvider.Factory viewModelFactory; - -// @Inject -// MessageParser messageParser; - - @Inject - public ClientHelper clientHelper; - - private ReturnShardViewModel viewModel; - private AddNearbyContactPermissionManager permissionManager; - - private final ActivityResultLauncher permissionLauncher = - registerForActivityResult( - new ActivityResultContracts.RequestMultiplePermissions(), - r -> - permissionManager.onRequestPermissionResult(r, - viewModel::showQrCodeFragmentIfAllowed)); - private final ActivityResultLauncher bluetoothLauncher = - registerForActivityResult(new RequestBluetoothDiscoverable(), - this::onBluetoothDiscoverableResult); - - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); - viewModel = new ViewModelProvider(this, viewModelFactory) - .get(ReturnShardViewModel.class); - permissionManager = new AddNearbyContactPermissionManager(this, - permissionLauncher::launch, viewModel.isBluetoothSupported()); - } - - // 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); - - byte[] returnShardPayloadBytes = - getIntent().getByteArrayExtra(RETURN_SHARD_PAYLOAD); - if (returnShardPayloadBytes != null) { - try { - ReturnShardPayload returnShardPayload = parseReturnShardPayload( - clientHelper.toList(returnShardPayloadBytes)); - viewModel.setSending(true); - viewModel.setReturnShardPayload(returnShardPayload); - } catch (FormatException e) { - Toast.makeText(this, - "Error reading social backup", - Toast.LENGTH_SHORT).show(); - finish(); - } - } - setContentView(R.layout.activity_fragment_container); - if (state == null) { - showInitialFragment(getExplainerFragment()); - } - viewModel.getCheckPermissions().observeEvent(this, check -> - permissionManager.checkPermissions()); - viewModel.getRequestBluetoothDiscoverable().observeEvent(this, r -> - requestBluetoothDiscoverable()); // never false - viewModel.getShowQrCodeFragment().observeEvent(this, show -> { - if (show) showQrCodeFragment(); - }); - viewModel.getState() - .observe(this, this::onReturnShardStateChanged); - } - - public BaseFragment getExplainerFragment() { - return new OwnerRecoveryModeExplainerFragment(); - } - - @Override - public void onStart() { - super.onStart(); - // Permissions may have been granted manually while we were stopped - permissionManager.resetPermissions(); - } - - @Override - protected void onPostResume() { - super.onPostResume(); - viewModel.setIsActivityResumed(true); - } - - @Override - protected void onPause() { - super.onPause(); - viewModel.setIsActivityResumed(false); - } - - private void onBluetoothDiscoverableResult(boolean discoverable) { - if (discoverable) { - LOG.info("Bluetooth discoverability was accepted"); - viewModel.setBluetoothDecision( - ReturnShardViewModel.BluetoothDecision.ACCEPTED); - } else { - LOG.info("Bluetooth discoverability was refused"); - viewModel.setBluetoothDecision( - ReturnShardViewModel.BluetoothDecision.REFUSED); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onBackPressed() { - if (viewModel.getState() - .getValue() instanceof ReturnShardState.Failed) { - // re-create this activity when going back in failed state - Intent i = new Intent(this, ReturnShardActivity.class); - i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - startActivity(i); - } else { - super.onBackPressed(); - } - } - - private void requestBluetoothDiscoverable() { - if (!viewModel.isBluetoothSupported()) { - viewModel.setBluetoothDecision( - ReturnShardViewModel.BluetoothDecision.NO_ADAPTER); - } else { - Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); - if (i.resolveActivity(getPackageManager()) != null) { - LOG.info("Asking for Bluetooth discoverability"); - viewModel.setBluetoothDecision( - ReturnShardViewModel.BluetoothDecision.WAITING); - bluetoothLauncher.launch(120); // 2min discoverable - } else { - viewModel.setBluetoothDecision( - ReturnShardViewModel.BluetoothDecision.NO_ADAPTER); - } - } - } - - private void showQrCodeFragment() { - // FIXME #824 - FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentByTag(ReturnShardFragment.TAG) == null) { - BaseFragment f = ReturnShardFragment.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/ReturnShardState.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardState.java deleted file mode 100644 index ea9b1fa9f..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardState.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.briarproject.briar.android.socialbackup.recover; - -import android.graphics.Bitmap; - -import org.briarproject.bramble.api.identity.Author; - -import androidx.annotation.Nullable; - -abstract class ReturnShardState { - - static class KeyAgreementListening extends - ReturnShardState { - final Bitmap qrCode; - - KeyAgreementListening(Bitmap qrCode) { - this.qrCode = qrCode; - } - } - - static class QrCodeScanned extends - ReturnShardState { - } - - static class KeyAgreementWaiting extends ReturnShardState { - } - - static class KeyAgreementStarted extends ReturnShardState { - } - - static class SocialBackupExchangeStarted extends ReturnShardState { - } - - static class SocialBackupExchangeFinished extends ReturnShardState { - final SocialBackupExchangeResult - result; - - SocialBackupExchangeFinished( - SocialBackupExchangeResult result) { - this.result = result; - } - } - - static class Failed extends ReturnShardState { - /** - * Non-null if failed due to the scanned QR code version. - * True if the app producing the code is too old. - * False if the scanning app is too old. - */ - @Nullable - final Boolean qrCodeTooOld; - - Failed(@Nullable Boolean qrCodeTooOld) { - this.qrCodeTooOld = qrCodeTooOld; - } - - Failed() { - this(null); - } - } - - abstract static class SocialBackupExchangeResult { - static class Success extends SocialBackupExchangeResult { - Success() {} - } - - static class Error extends SocialBackupExchangeResult { - @Nullable - final Author duplicateAuthor; - - Error(@Nullable Author duplicateAuthor) { - this.duplicateAuthor = duplicateAuthor; - } - } - } // end ContactExchangeResult - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardViewModel.java deleted file mode 100644 index 3e4576d8e..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/ReturnShardViewModel.java +++ /dev/null @@ -1,545 +0,0 @@ -package org.briarproject.briar.android.socialbackup.recover; - -import android.app.Application; -import android.bluetooth.BluetoothAdapter; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.util.DisplayMetrics; -import android.widget.Toast; - -import com.google.zxing.Result; - -import org.briarproject.bramble.api.UnsupportedVersionException; -import org.briarproject.bramble.api.connection.ConnectionManager; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactExchangeManager; -import org.briarproject.bramble.api.crypto.SecretKey; -import org.briarproject.bramble.api.db.ContactExistsException; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.event.EventListener; -import org.briarproject.bramble.api.keyagreement.KeyAgreementResult; -import org.briarproject.bramble.api.keyagreement.KeyAgreementTask; -import org.briarproject.bramble.api.keyagreement.Payload; -import org.briarproject.bramble.api.keyagreement.PayloadEncoder; -import org.briarproject.bramble.api.keyagreement.PayloadParser; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent; -import org.briarproject.bramble.api.lifecycle.IoExecutor; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.BluetoothConstants; -import org.briarproject.bramble.api.plugin.LanTcpConstants; -import org.briarproject.bramble.api.plugin.Plugin; -import org.briarproject.bramble.api.plugin.PluginManager; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; -import org.briarproject.bramble.api.plugin.event.TransportStateEvent; -import org.briarproject.bramble.api.system.AndroidExecutor; -import org.briarproject.briar.R; -import org.briarproject.briar.android.contact.add.nearby.QrCodeDecoder; -import org.briarproject.briar.android.contact.add.nearby.QrCodeUtils; -import org.briarproject.briar.android.viewmodel.LiveEvent; -import org.briarproject.briar.android.viewmodel.MutableLiveEvent; -import org.briarproject.briar.api.socialbackup.ReturnShardPayload; -import org.briarproject.briar.api.socialbackup.SocialBackupExchangeManager; -import org.briarproject.briar.api.socialbackup.SocialBackupManager; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.inject.Inject; -import javax.inject.Provider; - -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; -import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE; -import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; -import static android.widget.Toast.LENGTH_LONG; -import static java.util.Objects.requireNonNull; -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static java.util.logging.Logger.getLogger; -import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; -import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED; -import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE; -import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; -import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactPermissionManager.areEssentialPermissionsGranted; - -@NotNullByDefault -class ReturnShardViewModel extends AndroidViewModel - implements EventListener, QrCodeDecoder.ResultCallback { - - private static final Logger LOG = - getLogger(ReturnShardViewModel.class.getName()); - - // TODO deduplicate - enum BluetoothDecision { - /** - * We haven't asked the user about Bluetooth discoverability. - */ - UNKNOWN, - - /** - * The device doesn't have a Bluetooth adapter. - */ - NO_ADAPTER, - - /** - * We're waiting for the user to accept or refuse discoverability. - */ - WAITING, - - /** - * The user has accepted discoverability. - */ - ACCEPTED, - - /** - * The user has refused discoverability. - */ - REFUSED - } - - @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 - private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); - - private boolean sending; - private ReturnShardPayload returnShardPayload; - - private final EventBus eventBus; - private final AndroidExecutor androidExecutor; - private final Executor ioExecutor; - private final PluginManager pluginManager; - private final PayloadEncoder payloadEncoder; - private final PayloadParser payloadParser; - private final Provider keyAgreementTaskProvider; - private final SocialBackupExchangeManager socialBackupExchangeManager; - private final SocialBackupManager socialBackupManager; - private final ConnectionManager connectionManager; - - private final MutableLiveEvent checkPermissions = - new MutableLiveEvent<>(); - private final MutableLiveEvent requestBluetoothDiscoverable = - new MutableLiveEvent<>(); - private final MutableLiveEvent showQrCodeFragment = - new MutableLiveEvent<>(); - private final MutableLiveData state = - new MutableLiveData<>(); - - final QrCodeDecoder qrCodeDecoder; - final BroadcastReceiver - bluetoothReceiver = new BluetoothStateReceiver(); - - @Nullable - private final BluetoothAdapter bt; - @Nullable - private final Plugin wifiPlugin, bluetoothPlugin; - - // UiThread - private BluetoothDecision - bluetoothDecision = BluetoothDecision.UNKNOWN; - - private boolean wasContinueClicked = false; - private boolean isActivityResumed = false; - - /** - * Records whether we've enabled the wifi plugin so we don't enable it more - * than once. - */ - private boolean hasEnabledWifi = false; - - /** - * Records whether we've enabled the Bluetooth plugin so we don't enable it - * more than once. - */ - private boolean hasEnabledBluetooth = false; - - @Nullable - private KeyAgreementTask task; - private volatile boolean gotLocalPayload = false, gotRemotePayload = false; - - @Inject - ReturnShardViewModel(Application app, - EventBus eventBus, - AndroidExecutor androidExecutor, - @IoExecutor Executor ioExecutor, - PluginManager pluginManager, - PayloadEncoder payloadEncoder, - PayloadParser payloadParser, - Provider keyAgreementTaskProvider, - SocialBackupExchangeManager socialBackupExchangeManager, - SocialBackupManager socialBackupManager, - ConnectionManager connectionManager) { - super(app); - this.eventBus = eventBus; - this.androidExecutor = androidExecutor; - this.ioExecutor = ioExecutor; - this.pluginManager = pluginManager; - this.payloadEncoder = payloadEncoder; - this.payloadParser = payloadParser; - this.keyAgreementTaskProvider = keyAgreementTaskProvider; - this.socialBackupExchangeManager = socialBackupExchangeManager; - this.socialBackupManager = socialBackupManager; - this.connectionManager = connectionManager; - bt = BluetoothAdapter.getDefaultAdapter(); - wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID); - bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); - qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this); - eventBus.addListener(this); - IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); - getApplication().registerReceiver(bluetoothReceiver, filter); - } - - @Override - protected void onCleared() { - super.onCleared(); - getApplication().unregisterReceiver(bluetoothReceiver); - eventBus.removeListener(this); - stopListening(); - } - - @UiThread - void onContinueClicked() { - if (bluetoothDecision == BluetoothDecision.REFUSED) { - bluetoothDecision = BluetoothDecision.UNKNOWN; // Ask again - } - wasContinueClicked = true; - checkPermissions.setEvent(true); - showQrCodeFragmentIfAllowed(); - } - - @UiThread - boolean isBluetoothSupported() { - return bt != null && bluetoothPlugin != null; - } - - @UiThread - boolean isWifiReady() { - if (wifiPlugin == null) return true; // Continue without wifi - Plugin.State state = wifiPlugin.getState(); - // Wait for plugin to become enabled - return state == ACTIVE || state == INACTIVE; - } - - @UiThread - boolean isBluetoothReady() { - if (bt == null || bluetoothPlugin == null) { - // Continue without Bluetooth - return true; - } - if (bluetoothDecision == BluetoothDecision.UNKNOWN || - bluetoothDecision == BluetoothDecision.WAITING || - bluetoothDecision == BluetoothDecision.REFUSED) { - // Wait for user to accept - return false; - } - if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) { - // Wait for adapter to become discoverable - return false; - } - // Wait for plugin to become active - return bluetoothPlugin.getState() == ACTIVE; - } - - @UiThread - void enableWifiIfWeShould() { - if (hasEnabledWifi) return; - if (wifiPlugin == null) return; - Plugin.State state = wifiPlugin.getState(); - if (state == STARTING_STOPPING || state == DISABLED) { - LOG.info("Enabling wifi plugin"); - hasEnabledWifi = true; - pluginManager.setPluginEnabled(LanTcpConstants.ID, true); - } - } - - @UiThread - void enableBluetoothIfWeShould() { - if (bluetoothDecision != BluetoothDecision.ACCEPTED) - return; - if (hasEnabledBluetooth) return; - if (bluetoothPlugin == null || !isBluetoothSupported()) return; - Plugin.State state = bluetoothPlugin.getState(); - if (state == STARTING_STOPPING || state == DISABLED) { - LOG.info("Enabling Bluetooth plugin"); - hasEnabledBluetooth = true; - pluginManager.setPluginEnabled(BluetoothConstants.ID, true); - } - } - - @UiThread - void startAddingContact() { - // If we return to the intro fragment, the continue button needs to be - // clicked again before showing the QR code fragment - wasContinueClicked = false; - // If we return to the intro fragment, ask for Bluetooth - // discoverability again before showing the QR code fragment - bluetoothDecision = BluetoothDecision.UNKNOWN; - // If we return to the intro fragment, we may need to enable wifi and - // Bluetooth again - hasEnabledWifi = false; - hasEnabledBluetooth = false; - // start to listen with a KeyAgreementTask - startListening(); - showQrCodeFragment.setEvent(true); - } - - /** - * Call this once Bluetooth and Wi-Fi are ready to be used. - * It is possible to call this more than once over the ViewModel's lifetime. - */ - @UiThread - private void startListening() { - KeyAgreementTask oldTask = task; - KeyAgreementTask newTask = keyAgreementTaskProvider.get(); - task = newTask; - ioExecutor.execute(() -> { - if (oldTask != null) oldTask.stopListening(); - newTask.listen(); - }); - } - - @UiThread - private void stopListening() { - KeyAgreementTask oldTask = task; - ioExecutor.execute(() -> { - if (oldTask != null) oldTask.stopListening(); - }); - } - - @Override - public void eventOccurred(Event e) { - if (e instanceof TransportStateEvent) { - TransportStateEvent t = (TransportStateEvent) e; - if (t.getTransportId().equals(BluetoothConstants.ID)) { - if (LOG.isLoggable(INFO)) { - LOG.info("Bluetooth state changed to " + t.getState()); - } - showQrCodeFragmentIfAllowed(); - } else if (t.getTransportId().equals(LanTcpConstants.ID)) { - if (LOG.isLoggable(INFO)) { - LOG.info("Wifi state changed to " + t.getState()); - } - showQrCodeFragmentIfAllowed(); - } - } else if (e instanceof KeyAgreementListeningEvent) { - LOG.info("KeyAgreementListeningEvent received"); - KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e; - onLocalPayloadReceived(event.getLocalPayload()); - } else if (e instanceof KeyAgreementWaitingEvent) { - LOG.info("KeyAgreementWaitingEvent received"); - state.setValue(new ReturnShardState.KeyAgreementWaiting()); - } else if (e instanceof KeyAgreementStartedEvent) { - LOG.info("KeyAgreementStartedEvent received"); - state.setValue(new ReturnShardState.KeyAgreementStarted()); - } else if (e instanceof KeyAgreementFinishedEvent) { - LOG.info("KeyAgreementFinishedEvent received"); - KeyAgreementResult result = - ((KeyAgreementFinishedEvent) e).getResult(); - startContactExchange(result); - state.setValue(new ReturnShardState.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()); - } - } - - @SuppressWarnings("StatementWithEmptyBody") - @UiThread - void showQrCodeFragmentIfAllowed() { - boolean permissionsGranted = areEssentialPermissionsGranted( - getApplication(), isBluetoothSupported()); - if (isActivityResumed && wasContinueClicked && permissionsGranted) { - if (isWifiReady() && isBluetoothReady()) { - LOG.info("Wifi and Bluetooth are ready"); - startAddingContact(); - } else { - enableWifiIfWeShould(); - if (bluetoothDecision == BluetoothDecision.UNKNOWN) { - requestBluetoothDiscoverable.setEvent(true); - } else if (bluetoothDecision == BluetoothDecision.REFUSED) { - // Ask again when the user clicks "continue" - } else { - enableBluetoothIfWeShould(); - } - } - } - } - - /** - * This sets the QR code by setting the state to KeyAgreementListening. - */ - private void onLocalPayloadReceived(Payload localPayload) { - if (gotLocalPayload) return; - DisplayMetrics dm = getApplication().getResources().getDisplayMetrics(); - ioExecutor.execute(() -> { - byte[] payloadBytes = payloadEncoder.encode(localPayload); - if (LOG.isLoggable(INFO)) { - LOG.info("Local payload is " + payloadBytes.length - + " bytes"); - } - // Use ISO 8859-1 to encode bytes directly as a string - String content = new String(payloadBytes, ISO_8859_1); - Bitmap qrCode = QrCodeUtils.createQrCode(dm, content); - gotLocalPayload = true; - state.postValue(new ReturnShardState.KeyAgreementListening(qrCode)); - }); - } - - @Override - @IoExecutor - public void onQrCodeDecoded(Result result) { - LOG.info("Got result from decoder"+gotLocalPayload+gotRemotePayload); - // Ignore results until the KeyAgreementTask is ready - if (!gotLocalPayload || gotRemotePayload) return; - try { - byte[] payloadBytes = result.getText().getBytes(ISO_8859_1); - if (LOG.isLoggable(INFO)) - LOG.info("Remote payload is " + payloadBytes.length + " bytes"); - Payload remotePayload = payloadParser.parse(payloadBytes); - gotRemotePayload = true; - requireNonNull(task).connectAndRunProtocol(remotePayload); - state.postValue(new ReturnShardState.QrCodeScanned()); - } catch (UnsupportedVersionException e) { - resetPayloadFlags(); - state.postValue(new ReturnShardState.Failed(e.isTooOld())); - } catch (IOException | IllegalArgumentException e) { - LOG.log(WARNING, "QR Code Invalid", e); - androidExecutor.runOnUiThread(() -> Toast.makeText(getApplication(), - R.string.qr_code_invalid, LENGTH_LONG).show()); - resetPayloadFlags(); - state.postValue(new ReturnShardState.Failed()); - } - } - - private void resetPayloadFlags() { - gotRemotePayload = false; - gotLocalPayload = false; - } - - @UiThread - private void startContactExchange(KeyAgreementResult result) { - TransportId t = result.getTransportId(); - DuplexTransportConnection conn = result.getConnection(); - SecretKey masterKey = result.getMasterKey(); - boolean alice = result.wasAlice(); - ioExecutor.execute(() -> { - try { - 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)); - } - }); - } - - private class BluetoothStateReceiver extends BroadcastReceiver { - @UiThread - @Override - public void onReceive(Context context, Intent intent) { - int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, -1); - LOG.info("Bluetooth scan mode changed: " + scanMode); - showQrCodeFragmentIfAllowed(); - } - } - - private void tryToClose(DuplexTransportConnection conn) { - try { - conn.getReader().dispose(true, true); - conn.getWriter().dispose(true); - } catch (IOException e) { - logException(LOG, WARNING, e); - } - } - - /** - * Set to true in onPostResume() and false in onPause(). This prevents the - * QR code fragment from being shown if onRequestPermissionsResult() is - * called while the activity is paused, which could cause a crash due to - * https://issuetracker.google.com/issues/37067655. - * TODO check if this is still happening with new permission requesting - */ - @UiThread - void setIsActivityResumed(boolean resumed) { - isActivityResumed = resumed; - // Workaround for - // https://code.google.com/p/android/issues/detail?id=190966 - showQrCodeFragmentIfAllowed(); - } - - @UiThread - void setBluetoothDecision(BluetoothDecision decision) { - bluetoothDecision = decision; - showQrCodeFragmentIfAllowed(); - } - - LiveEvent getCheckPermissions() { - return checkPermissions; - } - - LiveEvent getRequestBluetoothDiscoverable() { - return requestBluetoothDiscoverable; - } - - LiveEvent getShowQrCodeFragment() { - return showQrCodeFragment; - } - - LiveData getState() { - return state; - } - - public void setSending(boolean sending) { - this.sending = sending; - } - - public void setReturnShardPayload(ReturnShardPayload returnShardPayload) { - this.returnShardPayload = returnShardPayload; - } - - QrCodeDecoder getQrCodeDecoder() { - return qrCodeDecoder; - } -} diff --git a/briar-android/src/main/res/layout/fragment_recovery_custodian_done.xml b/briar-android/src/main/res/layout/fragment_recovery_custodian_done.xml index eb33d3c90..25b9e72c6 100644 --- a/briar-android/src/main/res/layout/fragment_recovery_custodian_done.xml +++ b/briar-android/src/main/res/layout/fragment_recovery_custodian_done.xml @@ -10,6 +10,17 @@ android:paddingRight="@dimen/margin_large" android:paddingBottom="@dimen/margin_medium"> +