From 9e4ace4ce7d26652ef161e10bc26f6aa1b240ce4 Mon Sep 17 00:00:00 2001 From: ameba23 Date: Mon, 12 Apr 2021 10:50:47 +0200 Subject: [PATCH] Activity and view model for custodian returning shard --- .../android/activity/ActivityComponent.java | 10 +- ...ustodianRecoveryModeExplainerFragment.java | 41 ++-- .../recover/CustodianReturnShardActivity.java | 79 ++++++++ .../recover/CustodianReturnShardFragment.java | 185 ++++++++++++++++++ .../CustodianReturnShardViewModel.java | 110 +++++++++++ .../recover/CustodianSendShardActivity.java | 70 ------- .../OwnerRecoveryModeExplainerFragment.java | 1 - .../recovery/CustodianTaskImpl.java | 4 + 8 files changed, 412 insertions(+), 88 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardActivity.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardFragment.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardViewModel.java delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianSendShardActivity.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/socialbackup/recovery/CustodianTaskImpl.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 92b6c95e2..91a288110 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 @@ -80,11 +80,13 @@ import org.briarproject.briar.android.sharing.ShareBlogFragment; import org.briarproject.briar.android.sharing.ShareForumActivity; import org.briarproject.briar.android.sharing.ShareForumFragment; import org.briarproject.briar.android.sharing.SharingModule; -//import org.briarproject.briar.android.socialbackup.CustodianDisplayFragment; import org.briarproject.briar.android.socialbackup.CustodianHelpRecoverActivity; +import org.briarproject.briar.android.socialbackup.CustodianRecoveryModeExplainerFragment; import org.briarproject.briar.android.socialbackup.CustodianSelectorFragment; import org.briarproject.briar.android.socialbackup.DistributedBackupActivity; import org.briarproject.briar.android.socialbackup.ExistingBackupFragment; +import org.briarproject.briar.android.socialbackup.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.RecoverActivity; import org.briarproject.briar.android.socialbackup.ShardsSentFragment; @@ -202,6 +204,8 @@ public interface ActivityComponent { void inject(ReturnShardActivity returnShardActivity); + void inject(CustodianReturnShardActivity custodianSendShardActivity); + // Fragments void inject(AuthorNameFragment fragment); @@ -279,4 +283,8 @@ public interface ActivityComponent { void inject(NewOrRecoverFragment newOrRecoverFragment); void inject(ReturnShardFragment returnShardFragment); + + void inject(CustodianRecoveryModeExplainerFragment custodianRecoveryModeExplainerFragment); + + void inject(CustodianReturnShardFragment custodianReturnShardFragment); } 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 index 50660fc99..c682c1d96 100644 --- 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 @@ -1,48 +1,57 @@ 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.R; +import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.socialbackup.recover.CustodianReturnShardViewModel; + +import javax.inject.Inject; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.briarproject.briar.R; +import androidx.lifecycle.ViewModelProvider; public class CustodianRecoveryModeExplainerFragment extends BaseFragment { - protected CustodianScanQrButtonListener listener; + @Inject + ViewModelProvider.Factory viewModelFactory; - public static final String TAG = CustodianRecoveryModeExplainerFragment.class.getName(); + private CustodianReturnShardViewModel viewModel; + + 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); + 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); + View view = + inflater.inflate(R.layout.fragment_recovery_custodian_explainer, + container, false); 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 = (CustodianScanQrButtonListener) context; - } - @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..43b7ccb02 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardActivity.java @@ -0,0 +1,79 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.os.Bundle; + +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.android.socialbackup.CustodianRecoveryModeExplainerFragment; + +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; + +public class CustodianReturnShardActivity extends BriarActivity { + 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); + +// byte[] returnShardPayloadBytes = +// getIntent().getByteArrayExtra(RETURN_SHARD_PAYLOAD); +// try { +// ReturnShardPayload returnShardPayload = parseReturnShardPayload( +// clientHelper.toList(returnShardPayloadBytes)); +// 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(new CustodianRecoveryModeExplainerFragment()); + } +// viewModel.getCheckPermissions().observeEvent(this, check -> +// permissionManager.checkPermissions()); +// viewModel.getRequestBluetoothDiscoverable().observeEvent(this, r -> +// requestBluetoothDiscoverable()); // never false + viewModel.getShowCameraFragment().observeEvent(this, show -> { + if (show) showCameraFragment(); + }); +// viewModel.getState() +// .observe(this, this::onReturnShardStateChanged); + } + + 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/CustodianReturnShardFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardFragment.java new file mode 100644 index 000000000..220eeba31 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardFragment.java @@ -0,0 +1,185 @@ +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.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.CustodianTask; + +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 CustodianReturnShardFragment extends BaseFragment + implements QrCodeView.FullscreenListener { + + public static final String TAG = CustodianReturnShardFragment.class.getName(); + + private static final Logger LOG = Logger.getLogger(TAG); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private CustodianReturnShardViewModel viewModel; + private CameraView cameraView; + private LinearLayout cameraOverlay; + private View statusView; + private TextView status; + + public static CustodianReturnShardFragment newInstance() { + Bundle args = new Bundle(); + CustodianReturnShardFragment fragment = new CustodianReturnShardFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(CustodianReturnShardViewModel.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); + cameraView = view.findViewById(R.id.camera_view); + cameraOverlay = view.findViewById(R.id.camera_overlay); + statusView = view.findViewById(R.id.status_container); + status = view.findViewById(R.id.connect_status); + + viewModel.getState().observe(getViewLifecycleOwner(), + this::onReturnShardStateChanged); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + requireActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); + cameraView.setPreviewConsumer(viewModel.getQrCodeDecoder()); + } + + @Override + public void onStart() { + super.onStart(); + try { + cameraView.start(); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + } + } + + @Override + public void onStop() { + super.onStop(); + try { + cameraView.stop(); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + } + } + + @Override + public void onDestroy() { + requireActivity() + .setRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED); + super.onDestroy(); + } + + @Override + public void setFullscreen(boolean fullscreen) { + LinearLayout.LayoutParams statusParams, qrCodeParams; + if (fullscreen) { + statusParams = new LinearLayout.LayoutParams(0, 0, 0f); + } else { + if (cameraOverlay.getOrientation() == HORIZONTAL) { + statusParams = new LinearLayout.LayoutParams(0, MATCH_PARENT, 1f); + } else { + statusParams = new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1f); + } + } + statusView.setLayoutParams(statusParams); + cameraOverlay.invalidate(); + } + + @UiThread + private void onReturnShardStateChanged(@Nullable CustodianTask.State state) { + 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); + } else if (state instanceof CustodianTask.State.SendingShard) { + 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 CustodianTask.State.Failure) { + // the activity will replace this fragment with an error fragment + statusView.setVisibility(INVISIBLE); + cameraView.setVisibility(INVISIBLE); + } + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @UiThread + private void logCameraExceptionAndFinish(CameraException e) { + logException(LOG, WARNING, e); + Toast.makeText(getActivity(), R.string.camera_error, + LENGTH_LONG).show(); + finish(); + } + + @Override + protected void finish() { + requireActivity().getSupportFragmentManager().popBackStack(); + } + +} 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..b454ca859 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianReturnShardViewModel.java @@ -0,0 +1,110 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.app.Application; +import android.widget.Toast; + +import com.google.zxing.Result; + +import org.briarproject.bramble.api.UnsupportedVersionException; +import org.briarproject.bramble.api.keyagreement.Payload; +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.recovery.CustodianTask; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.cert.CertPathValidatorException; +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.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; + +public class CustodianReturnShardViewModel extends AndroidViewModel + implements QrCodeDecoder.ResultCallback { + + private static final Logger LOG = + getLogger(CustodianReturnShardViewModel.class.getName()); + + private final AndroidExecutor androidExecutor; + private final Executor ioExecutor; + private boolean wasContinueClicked = false; + private final MutableLiveEvent showCameraFragment = + new MutableLiveEvent<>(); + private final MutableLiveData state = + new MutableLiveData<>(); + final QrCodeDecoder qrCodeDecoder; + + @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 + private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + @Inject + public CustodianReturnShardViewModel( + @NonNull Application application, + @IoExecutor Executor ioExecutor, + AndroidExecutor androidExecutor) { + super(application); + + this.androidExecutor = androidExecutor; + this.ioExecutor = ioExecutor; + qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this); + } + + @IoExecutor + @Override + public void onQrCodeDecoded(Result result) { + LOG.info("Got result from decoder"); + // Ignore results until the KeyAgreementTask is ready +// if (!gotLocalPayload || gotRemotePayload) return; + try { + byte[] payloadBytes = result.getText().getBytes(ISO_8859_1); + if (LOG.isLoggable(INFO)) + LOG.info("Remote payload is " + payloadBytes.length + " bytes"); +// Payload remotePayload = payloadParser.parse(payloadBytes); +// gotRemotePayload = true; +// requireNonNull(task).connectAndRunProtocol(remotePayload); +// state.postValue(new ReturnShardState.QrCodeScanned()); + } catch (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 CustodianTask.State.Failure( + CustodianTask.State.Failure.Reason.QR_CODE_INVALID)); + } + } + + @UiThread + public void onContinueClicked() { + wasContinueClicked = true; +// checkPermissions.setEvent(true); + showCameraFragment.setEvent(true); + } + + QrCodeDecoder getQrCodeDecoder() { + return qrCodeDecoder; + } + + LiveEvent getShowCameraFragment() { + return showCameraFragment; + } + + LiveData getState() { + return state; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianSendShardActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianSendShardActivity.java deleted file mode 100644 index dd0eb19be..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/CustodianSendShardActivity.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.briarproject.briar.android.socialbackup.recover; - -import android.os.Bundle; -import android.widget.Toast; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.briar.R; -import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.activity.BriarActivity; -import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactPermissionManager; -import org.briarproject.briar.android.socialbackup.OwnerRecoveryModeMainFragment; -import org.briarproject.briar.api.socialbackup.ReturnShardPayload; - -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import androidx.lifecycle.ViewModelProvider; - -import static java.util.logging.Logger.getLogger; -import static org.briarproject.briar.android.socialbackup.CustodianHelpRecoverActivity.RETURN_SHARD_PAYLOAD; - -public class CustodianSendShardActivity extends BriarActivity { - private CustodianSendShardViewModel viewModel; - - private static final Logger LOG = - getLogger(CustodianSendShardActivity.class.getName()); - - @Inject - ViewModelProvider.Factory viewModelFactory; - - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); - viewModel = new ViewModelProvider(this, viewModelFactory) - .get(CustodianSendViewModel.class); - } - - @Override - public void onCreate(@Nullable Bundle state) { - super.onCreate(state); - - byte[] returnShardPayloadBytes = - getIntent().getByteArrayExtra(RETURN_SHARD_PAYLOAD); - try { - ReturnShardPayload returnShardPayload = parseReturnShardPayload( - clientHelper.toList(returnShardPayloadBytes)); -// 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(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); - } -} 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..370cb71c8 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 @@ -47,7 +47,6 @@ 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); diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/recovery/CustodianTaskImpl.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/recovery/CustodianTaskImpl.java new file mode 100644 index 000000000..197b1f015 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/recovery/CustodianTaskImpl.java @@ -0,0 +1,4 @@ +package org.briarproject.briar.socialbackup.recovery; + +public class CustodianTaskImpl { +}