diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
index df093059d..a857ac7ce 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
@@ -11,6 +11,8 @@ public interface FeatureFlags {
boolean shouldEnableDisappearingMessages();
+ boolean shouldEnableMailbox();
+
boolean shouldEnablePrivateGroupsInCore();
boolean shouldEnableForumsInCore();
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java
index 6e867ffbe..c3f92b08a 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java
@@ -25,6 +25,11 @@ public class TestFeatureFlagModule {
return true;
}
+ @Override
+ public boolean shouldEnableMailbox() {
+ return true;
+ }
+
@Override
public boolean shouldEnablePrivateGroupsInCore() {
return true;
diff --git a/briar-android/artwork/ic_mailbox.svg b/briar-android/artwork/ic_mailbox.svg
new file mode 100644
index 000000000..800fb4f76
--- /dev/null
+++ b/briar-android/artwork/ic_mailbox.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/briar-android/artwork/mailbox_onboarding_dark.svg b/briar-android/artwork/mailbox_onboarding_dark.svg
new file mode 100644
index 000000000..ea1a19757
--- /dev/null
+++ b/briar-android/artwork/mailbox_onboarding_dark.svg
@@ -0,0 +1,105 @@
+
diff --git a/briar-android/artwork/mailbox_onboarding_light.svg b/briar-android/artwork/mailbox_onboarding_light.svg
new file mode 100644
index 000000000..988d2438d
--- /dev/null
+++ b/briar-android/artwork/mailbox_onboarding_light.svg
@@ -0,0 +1,105 @@
+
diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml
index edcea26a7..10559cd45 100644
--- a/briar-android/src/main/AndroidManifest.xml
+++ b/briar-android/src/main/AndroidManifest.xml
@@ -477,6 +477,17 @@
android:value="org.briarproject.briar.android.conversation.ConversationActivity" />
+
+
+
+
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
index 42d6570e2..e5b3d55dc 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
@@ -44,6 +44,9 @@ import org.briarproject.briar.android.hotspot.ManualHotspotFragment;
import org.briarproject.briar.android.hotspot.QrHotspotFragment;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver;
+import org.briarproject.briar.android.mailbox.MailboxScanFragment;
+import org.briarproject.briar.android.mailbox.OfflineFragment;
+import org.briarproject.briar.android.mailbox.SetupDownloadFragment;
import org.briarproject.briar.android.removabledrive.ChooserFragment;
import org.briarproject.briar.android.removabledrive.ReceiveFragment;
import org.briarproject.briar.android.removabledrive.SendFragment;
@@ -239,4 +242,10 @@ public interface AndroidComponent
void inject(ReceiveFragment receiveFragment);
void inject(BluetoothIntroFragment bluetoothIntroFragment);
+
+ void inject(SetupDownloadFragment setupDownloadFragment);
+
+ void inject(MailboxScanFragment mailboxScanFragment);
+
+ void inject(OfflineFragment offlineFragment);
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
index 9e570d13a..e6682a751 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
@@ -31,6 +31,7 @@ import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils;
+import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.android.account.DozeHelperModule;
import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule;
@@ -340,6 +341,11 @@ public class AppModule {
return true;
}
+ @Override
+ public boolean shouldEnableMailbox() {
+ return BuildConfig.DEBUG;
+ }
+
@Override
public boolean shouldEnablePrivateGroupsInCore() {
return true;
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 43baf5816..d4fb54b54 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
@@ -46,6 +46,7 @@ import org.briarproject.briar.android.login.ChangePasswordActivity;
import org.briarproject.briar.android.login.OpenDatabaseFragment;
import org.briarproject.briar.android.login.PasswordFragment;
import org.briarproject.briar.android.login.StartupActivity;
+import org.briarproject.briar.android.mailbox.MailboxActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.navdrawer.TransportsActivity;
import org.briarproject.briar.android.panic.PanicPreferencesActivity;
@@ -250,4 +251,6 @@ public interface ActivityComponent {
void inject(RssFeedDeleteFeedDialogFragment fragment);
void inject(ConnectViaBluetoothActivity connectViaBluetoothActivity);
+
+ void inject(MailboxActivity mailboxActivity);
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java
index 0c4761ada..30c518dbc 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java
@@ -181,12 +181,6 @@ public abstract class BaseActivity extends AppCompatActivity
showFragment(getSupportFragmentManager(), f, f.getUniqueTag());
}
- protected boolean isFragmentAdded(String fragmentTag) {
- FragmentManager fm = getSupportFragmentManager();
- Fragment f = fm.findFragmentByTag(fragmentTag);
- return f != null && f.isAdded();
- }
-
private boolean showScreenFilterWarning() {
if (((BriarApplication) getApplication()).isInstrumentationTest()) {
return false;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
index dc8fca71f..f40dbe094 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
@@ -3,11 +3,10 @@ package org.briarproject.briar.android.contact.add.nearby;
import android.content.Context;
import org.briarproject.briar.R;
+import org.briarproject.briar.android.util.Permission;
import java.util.Map;
-import androidx.annotation.StringRes;
-import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentActivity;
@@ -17,16 +16,13 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
import static androidx.core.content.ContextCompat.checkSelfPermission;
-import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
+import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
+import static org.briarproject.briar.android.util.UiUtils.showRationale;
class AddNearbyContactPermissionManager {
- private enum Permission {
- UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
- }
-
private Permission cameraPermission = Permission.UNKNOWN;
private Permission locationPermission = Permission.UNKNOWN;
@@ -68,27 +64,30 @@ class AddNearbyContactPermissionManager {
// If an essential permission has been permanently denied, ask the
// user to change the setting
if (cameraPermission == Permission.PERMANENTLY_DENIED) {
- showDenialDialog(R.string.permission_camera_title,
+ showDenialDialog(ctx, R.string.permission_camera_title,
R.string.permission_camera_denied_body);
return false;
}
if (isBluetoothSupported &&
locationPermission == Permission.PERMANENTLY_DENIED) {
- showDenialDialog(R.string.permission_location_title,
+ showDenialDialog(ctx, R.string.permission_location_title,
R.string.permission_location_denied_body);
return false;
}
// Should we show the rationale for one or both permissions?
if (cameraPermission == Permission.SHOW_RATIONALE &&
locationPermission == Permission.SHOW_RATIONALE) {
- showRationale(R.string.permission_camera_location_title,
- R.string.permission_camera_location_request_body);
+ showRationale(ctx, R.string.permission_camera_location_title,
+ R.string.permission_camera_location_request_body,
+ this::requestPermissions);
} else if (cameraPermission == Permission.SHOW_RATIONALE) {
- showRationale(R.string.permission_camera_title,
- R.string.permission_camera_request_body);
+ showRationale(ctx, R.string.permission_camera_title,
+ R.string.permission_camera_request_body,
+ this::requestPermissions);
} else if (locationPermission == Permission.SHOW_RATIONALE) {
- showRationale(R.string.permission_location_title,
- R.string.permission_location_request_body);
+ showRationale(ctx, R.string.permission_location_title,
+ R.string.permission_location_request_body,
+ this::requestPermissions);
} else if (locationEnabled) {
requestPermissions();
} else {
@@ -97,27 +96,6 @@ class AddNearbyContactPermissionManager {
return false;
}
- private void showDenialDialog(@StringRes int title, @StringRes int body) {
- AlertDialog.Builder builder =
- new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
- builder.setTitle(title);
- builder.setMessage(body);
- builder.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx));
- builder.setNegativeButton(R.string.cancel,
- (dialog, which) -> ctx.supportFinishAfterTransition());
- builder.show();
- }
-
- private void showRationale(@StringRes int title, @StringRes int body) {
- AlertDialog.Builder builder =
- new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
- builder.setTitle(title);
- builder.setMessage(body);
- builder.setNeutralButton(R.string.continue_button,
- (dialog, which) -> requestPermissions());
- builder.show();
- }
-
private void requestPermissions() {
String[] permissions;
if (isBluetoothSupported) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java
index a2b2a4cf7..7a8ac1d9a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java
@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.Context;
import org.briarproject.briar.R;
+import org.briarproject.briar.android.util.Permission;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
@@ -19,10 +20,6 @@ import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
class BluetoothConditionManager {
- private enum Permission {
- UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
- }
-
private Permission locationPermission = Permission.UNKNOWN;
/**
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java
index 199349e66..bd2f31810 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java
@@ -1,5 +1,6 @@
package org.briarproject.briar.android.fragment;
+import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -16,6 +17,7 @@ import org.briarproject.briar.R;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
@@ -93,11 +95,6 @@ public class FinalFragment extends Fragment {
AppCompatActivity a = (AppCompatActivity) requireActivity();
a.setTitle(args.getInt(ARG_TITLE));
- ActionBar actionBar = a.getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(false);
- actionBar.setHomeButtonEnabled(false);
- }
a.getOnBackPressedDispatcher().addCallback(
getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
@@ -108,6 +105,18 @@ public class FinalFragment extends Fragment {
return v;
}
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ // onAttach(Activity) is deprecated, we are told to cast the context
+ AppCompatActivity a = (AppCompatActivity) context;
+ ActionBar actionBar = a.getSupportActionBar();
+ if (shouldHideActionBarBackButton() && actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+ }
+ }
+
@Override
public void onStart() {
super.onStart();
@@ -115,6 +124,17 @@ public class FinalFragment extends Fragment {
scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
}
+ @Override
+ public void onDetach() {
+ AppCompatActivity a = (AppCompatActivity) requireActivity();
+ ActionBar actionBar = a.getSupportActionBar();
+ if (shouldHideActionBarBackButton() && actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ }
+ super.onDetach();
+ }
+
/**
* This is the action that the system back button
* and the button at the bottom will perform.
@@ -123,4 +143,13 @@ public class FinalFragment extends Fragment {
requireActivity().supportFinishAfterTransition();
}
+ /**
+ * If you are overriding {@link #onBackButtonPressed()}
+ * and are no longer finishing the fragment, return false here.
+ * Otherwise the ActionBar back button will be missing in your activity.
+ */
+ protected boolean shouldHideActionBarBackButton() {
+ return true;
+ }
+
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/AbstractConditionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/AbstractConditionManager.java
index 855fd845d..d0e65a93f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/AbstractConditionManager.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/AbstractConditionManager.java
@@ -20,11 +20,7 @@ import static android.content.Context.WIFI_SERVICE;
*/
abstract class AbstractConditionManager {
- enum Permission {
- UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
- }
-
- protected final Consumer permissionUpdateCallback;
+ final Consumer permissionUpdateCallback;
protected FragmentActivity ctx;
WifiManager wifiManager;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29.java
index 036225899..071fbeeb6 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29.java
@@ -4,6 +4,7 @@ import android.content.Intent;
import android.provider.Settings;
import org.briarproject.briar.R;
+import org.briarproject.briar.android.util.Permission;
import java.util.logging.Logger;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java
index 359b86f2d..edbe226b7 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java
@@ -56,13 +56,9 @@ public class HotspotActivity extends BriarActivity
if (hotspotState instanceof HotspotStarted) {
HotspotStarted started = (HotspotStarted) hotspotState;
String tag = HotspotFragment.TAG;
- // check if fragment is already added
- // to not lose state on configuration changes
- if (fm.findFragmentByTag(tag) == null) {
- if (started.wasNotYetConsumed()) {
- started.consume();
- showFragment(fm, new HotspotFragment(), tag);
- }
+ if (started.wasNotYetConsumed()) {
+ started.consume();
+ showFragment(fm, new HotspotFragment(), tag);
}
} else if (hotspotState instanceof HotspotError) {
HotspotError error = (HotspotError) hotspotState;
@@ -116,10 +112,8 @@ public class HotspotActivity extends BriarActivity
private void showErrorFragment(String error) {
FragmentManager fm = getSupportFragmentManager();
String tag = HotspotErrorFragment.TAG;
- if (fm.findFragmentByTag(tag) == null) {
- Fragment f = HotspotErrorFragment.newInstance(error);
- showFragment(fm, f, tag, false);
- }
+ Fragment f = HotspotErrorFragment.newInstance(error);
+ showFragment(fm, f, tag, false);
}
@Override
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/login/StartupActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/login/StartupActivity.java
index c65a06cc7..6c43e0126 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/login/StartupActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/login/StartupActivity.java
@@ -85,16 +85,10 @@ public class StartupActivity extends BaseActivity implements
if (state == SIGNED_OUT) {
// Configuration changes such as screen rotation
// can cause this to get called again.
- // Don't replace the fragment in that case to not lose view state.
- if (!isFragmentAdded(PasswordFragment.TAG)) {
- showInitialFragment(new PasswordFragment());
- }
+ showInitialFragment(new PasswordFragment());
} else if (state == SIGNED_IN || state == STARTING) {
startService(new Intent(this, BriarService.class));
- // Only show OpenDatabaseFragment if not already visible.
- if (!isFragmentAdded(OpenDatabaseFragment.TAG)) {
- showNextFragment(new OpenDatabaseFragment());
- }
+ showNextFragment(new OpenDatabaseFragment());
} else if (state == STARTED) {
setResult(RESULT_OK);
supportFinishAfterTransition();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/CameraPermissionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/CameraPermissionManager.java
new file mode 100644
index 000000000..34e819185
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/CameraPermissionManager.java
@@ -0,0 +1,83 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.content.Context;
+
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.util.Permission;
+
+import java.util.Map;
+
+import androidx.core.util.Consumer;
+import androidx.fragment.app.FragmentActivity;
+
+import static android.Manifest.permission.CAMERA;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
+import static androidx.core.content.ContextCompat.checkSelfPermission;
+import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
+import static org.briarproject.briar.android.util.UiUtils.showRationale;
+
+class CameraPermissionManager {
+
+ private Permission cameraPermission = Permission.UNKNOWN;
+
+ private final FragmentActivity ctx;
+ private final Consumer requestPermissions;
+
+ CameraPermissionManager(FragmentActivity ctx,
+ Consumer requestPermissions) {
+ this.ctx = ctx;
+ this.requestPermissions = requestPermissions;
+ }
+
+ void resetPermissions() {
+ cameraPermission = Permission.UNKNOWN;
+ }
+
+ private static boolean areEssentialPermissionsGranted(Context ctx) {
+ return checkSelfPermission(ctx, CAMERA) == PERMISSION_GRANTED;
+ }
+
+ private boolean areEssentialPermissionsGranted() {
+ return cameraPermission == Permission.GRANTED;
+ }
+
+ boolean checkPermissions() {
+ if (areEssentialPermissionsGranted()) return true;
+ // If an essential permission has been permanently denied, ask the
+ // user to change the setting
+ if (cameraPermission == Permission.PERMANENTLY_DENIED) {
+ showDenialDialog(ctx, R.string.permission_camera_title,
+ R.string.permission_camera_qr_denied_body);
+ } else if (cameraPermission == Permission.SHOW_RATIONALE) {
+ showRationale(ctx, R.string.permission_camera_title,
+ R.string.permission_camera_request_body,
+ this::requestPermissions);
+ } else {
+ requestPermissions();
+ }
+ return false;
+ }
+
+ private void requestPermissions() {
+ String[] permissions = new String[] {CAMERA};
+ requestPermissions.accept(permissions);
+ }
+
+ void onRequestPermissionResult(Map result) {
+ if (gotPermission(result)) {
+ cameraPermission = Permission.GRANTED;
+ } else if (shouldShowRequestPermissionRationale(ctx, CAMERA)) {
+ cameraPermission = Permission.SHOW_RATIONALE;
+ } else {
+ cameraPermission = Permission.PERMANENTLY_DENIED;
+ }
+ }
+
+ private boolean gotPermission(Map result) {
+ Boolean permissionResult = result.get(CAMERA);
+ return permissionResult == null ? areEssentialPermissionsGranted(ctx) :
+ permissionResult;
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/ErrorFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/ErrorFragment.java
new file mode 100644
index 000000000..5d43ba336
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/ErrorFragment.java
@@ -0,0 +1,51 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.fragment.FinalFragment;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class ErrorFragment extends FinalFragment {
+
+ public static ErrorFragment newInstance(@StringRes int title,
+ @StringRes int text) {
+ ErrorFragment f = new ErrorFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_TITLE, title);
+ args.putInt(ARG_ICON, R.drawable.alerts_and_states_error);
+ args.putInt(ARG_ICON_TINT, R.color.briar_red_500);
+ args.putInt(ARG_TEXT, text);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View v = super.onCreateView(inflater, container, savedInstanceState);
+ buttonView.setText(R.string.try_again_button);
+ return v;
+ }
+
+ @Override
+ protected void onBackButtonPressed() {
+ requireActivity().getSupportFragmentManager().popBackStack();
+ }
+
+ @Override
+ protected boolean shouldHideActionBarBackButton() {
+ return false;
+ }
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java
new file mode 100644
index 000000000..9570f4740
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java
@@ -0,0 +1,128 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import org.briarproject.bramble.api.mailbox.MailboxStatus;
+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.BriarActivity;
+
+import javax.inject.Inject;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static org.briarproject.briar.android.util.UiUtils.showFragment;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class MailboxActivity extends BriarActivity {
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private MailboxViewModel viewModel;
+ private ProgressBar progressBar;
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+
+ viewModel = new ViewModelProvider(this, viewModelFactory)
+ .get(MailboxViewModel.class);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_mailbox);
+
+ progressBar = findViewById(R.id.progressBar);
+ if (viewModel.getState().getValue() == null) {
+ progressBar.setVisibility(VISIBLE);
+ }
+
+ viewModel.getState().observeEvent(this, state -> {
+ if (state instanceof MailboxState.NotSetup) {
+ onNotSetup();
+ } else if (state instanceof MailboxState.ScanningQrCode) {
+ onScanningQrCode();
+ } else if (state instanceof MailboxState.SettingUp) {
+ onCodeScanned();
+ } else if (state instanceof MailboxState.QrCodeWrong) {
+ onQrCodeWrong();
+ } else if (state instanceof MailboxState.OfflineInSetup) {
+ onOffline();
+ } else if (state instanceof MailboxState.IsSetup) {
+ onIsSetup(((MailboxState.IsSetup) state).mailboxStatus);
+ } else {
+ throw new AssertionError("Unknown state: " + state);
+ }
+ });
+ }
+
+ @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()
+ .getLastValue() instanceof MailboxState.SettingUp) {
+ // don't go back in flow if we are already setting up mailbox
+ supportFinishAfterTransition();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void onNotSetup() {
+ progressBar.setVisibility(INVISIBLE);
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragmentContainer, new SetupIntroFragment(),
+ SetupIntroFragment.TAG)
+ .commit();
+ }
+
+ private void onScanningQrCode() {
+ showFragment(getSupportFragmentManager(), new MailboxScanFragment(),
+ MailboxScanFragment.TAG);
+ }
+
+ private void onCodeScanned() {
+ showFragment(getSupportFragmentManager(),
+ new MailboxConnectingFragment(),
+ MailboxConnectingFragment.TAG, false);
+ }
+
+ private void onQrCodeWrong() {
+ Fragment f = ErrorFragment.newInstance(
+ R.string.mailbox_setup_qr_code_wrong_title,
+ R.string.mailbox_setup_qr_code_wrong_description);
+ showFragment(getSupportFragmentManager(), f, ErrorFragment.TAG);
+ }
+
+ private void onOffline() {
+ showFragment(getSupportFragmentManager(), new OfflineFragment(),
+ OfflineFragment.TAG);
+ }
+
+ private void onIsSetup(MailboxStatus mailboxStatus) {
+ // TODO
+ Toast.makeText(this, "NOT IMPLEMENTED", Toast.LENGTH_LONG).show();
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxConnectingFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxConnectingFragment.java
new file mode 100644
index 000000000..3361c01cd
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxConnectingFragment.java
@@ -0,0 +1,36 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.briar.R;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class MailboxConnectingFragment extends Fragment {
+
+ static final String TAG = MailboxConnectingFragment.class.getName();
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_mailbox_connecting,
+ container, false);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ requireActivity().setTitle(R.string.mailbox_setup_title);
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxModule.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxModule.java
index 196e4e151..e78b23736 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxModule.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxModule.java
@@ -12,8 +12,7 @@ public interface MailboxModule {
@Binds
@IntoMap
- @ViewModelKey(MailboxPairViewModel.class)
- ViewModel bindMailboxViewModel(
- MailboxPairViewModel mailboxPairViewModel);
+ @ViewModelKey(MailboxViewModel.class)
+ ViewModel bindMailboxViewModel(MailboxViewModel mailboxViewModel);
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxPairViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxPairViewModel.java
deleted file mode 100644
index 1c6023997..000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxPairViewModel.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package org.briarproject.briar.android.mailbox;
-
-import android.app.Application;
-
-import com.google.zxing.Result;
-
-import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.db.DatabaseExecutor;
-import org.briarproject.bramble.api.db.TransactionManager;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
-import org.briarproject.bramble.api.lifecycle.LifecycleManager;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.system.AndroidExecutor;
-import org.briarproject.bramble.util.StringUtils;
-import org.briarproject.briar.android.qrcode.QrCodeDecoder;
-import org.briarproject.briar.android.viewmodel.DbViewModel;
-
-import java.nio.charset.Charset;
-import java.util.Arrays;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Logger.getLogger;
-
-@UiThread
-@NotNullByDefault
-class MailboxPairViewModel extends DbViewModel
- implements QrCodeDecoder.ResultCallback {
- private static final Logger LOG =
- getLogger(MailboxPairViewModel.class.getName());
-
- private static final int VERSION_REQUIRED = 32;
-
- @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
- private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
-
- private final CryptoComponent crypto;
- private final QrCodeDecoder qrCodeDecoder;
-
- @Nullable
- private String onionAddress = null;
- @Nullable
- private String setupToken = null;
-
- @Inject
- MailboxPairViewModel(
- Application app,
- @DatabaseExecutor Executor dbExecutor,
- LifecycleManager lifecycleManager,
- TransactionManager db,
- AndroidExecutor androidExecutor,
- @IoExecutor Executor ioExecutor,
- CryptoComponent crypto) {
- super(app, dbExecutor, lifecycleManager, db, androidExecutor);
- this.crypto = crypto;
- qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
- }
-
- @Override
- public void onQrCodeDecoded(Result result) {
- LOG.info("Got result from decoder");
- byte[] bytes = result.getText().getBytes(ISO_8859_1);
-
- if (LOG.isLoggable(INFO))
- LOG.info("QR code length in bytes: " + bytes.length);
- if (bytes.length != 65) {
- LOG.info("QR code has wrong length");
- return;
- }
-
- if (LOG.isLoggable(INFO))
- LOG.info("QR code version: " + bytes[0]);
- if (bytes[0] != VERSION_REQUIRED) {
- LOG.info("QR code has wrong version");
- return;
- }
-
- byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33);
- onionAddress = crypto.encodeOnionAddress(onionPubKey);
- setupToken = StringUtils.toHexString(Arrays.copyOfRange(bytes, 33, 65))
- .toLowerCase();
- LOG.info("QR code is valid");
- }
-
- QrCodeDecoder getQrCodeDecoder() {
- return qrCodeDecoder;
- }
-
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxScanFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxScanFragment.java
new file mode 100644
index 000000000..335fd90f8
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxScanFragment.java
@@ -0,0 +1,100 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+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.qrcode.CameraException;
+import org.briarproject.briar.android.qrcode.CameraView;
+
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import androidx.annotation.UiThread;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.widget.Toast.LENGTH_LONG;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.briar.android.AppModule.getAndroidComponent;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class MailboxScanFragment extends Fragment {
+
+ static final String TAG = MailboxScanFragment.class.getName();
+
+ private static final Logger LOG = Logger.getLogger(TAG);
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private MailboxViewModel viewModel;
+
+ private CameraView cameraView;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentActivity activity = requireActivity();
+ getAndroidComponent(activity).inject(this);
+ viewModel = new ViewModelProvider(activity, viewModelFactory)
+ .get(MailboxViewModel.class);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_mailbox_scan, container,
+ false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ cameraView = view.findViewById(R.id.camera_view);
+ cameraView.setPreviewConsumer(viewModel.getQrCodeDecoder());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ requireActivity().setTitle(R.string.mailbox_setup_button_scan);
+ try {
+ cameraView.start();
+ } catch (CameraException e) {
+ logCameraExceptionAndFinish(e);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ try {
+ cameraView.stop();
+ } catch (CameraException e) {
+ logCameraExceptionAndFinish(e);
+ }
+ }
+
+ @UiThread
+ private void logCameraExceptionAndFinish(CameraException e) {
+ logException(LOG, WARNING, e);
+ Toast.makeText(requireContext(), R.string.camera_error,
+ LENGTH_LONG).show();
+ requireActivity().getSupportFragmentManager().popBackStack();
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java
new file mode 100644
index 000000000..caa6400e1
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java
@@ -0,0 +1,43 @@
+package org.briarproject.briar.android.mailbox;
+
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxStatus;
+
+import androidx.annotation.Nullable;
+
+class MailboxState {
+
+ static class NotSetup extends MailboxState {
+ }
+
+ static class ScanningQrCode extends MailboxState {
+ }
+
+ static class SettingUp extends MailboxState {
+ }
+
+ static class QrCodeWrong extends MailboxState {
+ }
+
+ static class OfflineInSetup extends MailboxState {
+ @Nullable
+ final MailboxProperties mailboxProperties;
+
+ OfflineInSetup(@Nullable MailboxProperties mailboxProperties) {
+ this.mailboxProperties = mailboxProperties;
+ }
+
+ OfflineInSetup() {
+ this(null);
+ }
+ }
+
+ static class IsSetup extends MailboxState {
+ final MailboxStatus mailboxStatus;
+
+ IsSetup(MailboxStatus mailboxStatus) {
+ this.mailboxStatus = mailboxStatus;
+ }
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java
new file mode 100644
index 000000000..1c199aa59
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java
@@ -0,0 +1,180 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.app.Application;
+
+import com.google.zxing.Result;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.db.DatabaseExecutor;
+import org.briarproject.bramble.api.db.TransactionManager;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
+import org.briarproject.bramble.api.mailbox.MailboxStatus;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.Plugin;
+import org.briarproject.bramble.api.plugin.PluginManager;
+import org.briarproject.bramble.api.plugin.TorConstants;
+import org.briarproject.bramble.api.system.AndroidExecutor;
+import org.briarproject.briar.android.mailbox.MailboxState.NotSetup;
+import org.briarproject.briar.android.qrcode.QrCodeDecoder;
+import org.briarproject.briar.android.viewmodel.DbViewModel;
+import org.briarproject.briar.android.viewmodel.LiveEvent;
+import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import androidx.annotation.UiThread;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
+
+@NotNullByDefault
+class MailboxViewModel extends DbViewModel
+ implements QrCodeDecoder.ResultCallback {
+
+ private static final Logger LOG =
+ getLogger(MailboxViewModel.class.getName());
+
+ @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
+ private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+ private static final int VERSION_REQUIRED = 32;
+
+ private final CryptoComponent crypto;
+ private final QrCodeDecoder qrCodeDecoder;
+ private final PluginManager pluginManager;
+ private final MailboxSettingsManager mailboxSettingsManager;
+
+ private final MutableLiveEvent state =
+ new MutableLiveEvent<>();
+
+ @Inject
+ MailboxViewModel(
+ Application app,
+ @DatabaseExecutor Executor dbExecutor,
+ LifecycleManager lifecycleManager,
+ TransactionManager db,
+ AndroidExecutor androidExecutor,
+ @IoExecutor Executor ioExecutor,
+ CryptoComponent crypto,
+ PluginManager pluginManager,
+ MailboxSettingsManager mailboxSettingsManager) {
+ super(app, dbExecutor, lifecycleManager, db, androidExecutor);
+ this.crypto = crypto;
+ this.pluginManager = pluginManager;
+ this.mailboxSettingsManager = mailboxSettingsManager;
+ qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
+ checkIfSetup();
+ }
+
+ @UiThread
+ private void checkIfSetup() {
+ runOnDbThread(true, txn -> {
+ MailboxProperties props =
+ mailboxSettingsManager.getOwnMailboxProperties(txn);
+ if (props == null) state.postEvent(new NotSetup());
+ else {
+ MailboxStatus mailboxStatus =
+ mailboxSettingsManager.getOwnMailboxStatus(txn);
+ state.postEvent(new MailboxState.IsSetup(mailboxStatus));
+ }
+ }, this::handleException);
+ }
+
+ @UiThread
+ void onScanButtonClicked() {
+ if (isTorActive()) {
+ state.setEvent(new MailboxState.ScanningQrCode());
+ } else {
+ state.setEvent(new MailboxState.OfflineInSetup());
+ }
+ }
+
+ @Override
+ @IoExecutor
+ public void onQrCodeDecoded(Result result) {
+ LOG.info("Got result from decoder");
+ MailboxProperties properties;
+ try {
+ properties = decodeQrCode(result.getText());
+ } catch (FormatException e) {
+ state.postEvent(new MailboxState.QrCodeWrong());
+ return;
+ }
+ onMailboxPropertiesReceived(properties);
+ }
+
+ @IoExecutor
+ // TODO move this into core #2168
+ private MailboxProperties decodeQrCode(String payload)
+ throws FormatException {
+ byte[] bytes = payload.getBytes(ISO_8859_1);
+ if (bytes.length != 65) {
+ if (LOG.isLoggable(WARNING)) {
+ LOG.warning("QR code length is not 65: " + bytes.length);
+ }
+ throw new FormatException();
+ }
+ int version = bytes[0] & 0xFF;
+ if (version != VERSION_REQUIRED) {
+ if (LOG.isLoggable(WARNING)) {
+ LOG.warning("QR code has not version " + VERSION_REQUIRED +
+ ": " + version);
+ }
+ throw new FormatException();
+ }
+ LOG.info("QR code is valid");
+ byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33);
+ String onionAddress = crypto.encodeOnionAddress(onionPubKey);
+ byte[] tokenBytes = Arrays.copyOfRange(bytes, 33, 65);
+ MailboxAuthToken setupToken = new MailboxAuthToken(tokenBytes);
+ return new MailboxProperties(onionAddress, setupToken, true);
+ }
+
+ private void onMailboxPropertiesReceived(MailboxProperties properties) {
+ if (isTorActive()) {
+ // TODO pass props to core #2168
+ state.postEvent(new MailboxState.SettingUp());
+ } else {
+ state.postEvent(new MailboxState.OfflineInSetup(properties));
+ }
+ }
+
+ // TODO ideally also move this into core #2168
+ private boolean isTorActive() {
+ Plugin plugin = pluginManager.getPlugin(TorConstants.ID);
+ return plugin != null && plugin.getState() == ACTIVE;
+ }
+
+ @UiThread
+ void tryAgainWhenOffline() {
+ MailboxState.OfflineInSetup offline =
+ (MailboxState.OfflineInSetup) requireNonNull(
+ state.getLastValue());
+ if (offline.mailboxProperties == null) {
+ onScanButtonClicked();
+ } else {
+ onMailboxPropertiesReceived(offline.mailboxProperties);
+ }
+ }
+
+ @UiThread
+ QrCodeDecoder getQrCodeDecoder() {
+ return qrCodeDecoder;
+ }
+
+ @UiThread
+ LiveEvent getState() {
+ return state;
+ }
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java
new file mode 100644
index 000000000..fba30ccf6
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java
@@ -0,0 +1,80 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.content.Context;
+import android.content.Intent;
+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.navdrawer.TransportsActivity;
+
+import javax.inject.Inject;
+
+import androidx.annotation.Nullable;
+import androidx.core.widget.NestedScrollView;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.view.View.FOCUS_DOWN;
+import static org.briarproject.briar.android.AppModule.getAndroidComponent;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class OfflineFragment extends Fragment {
+
+ public static final String TAG = OfflineFragment.class.getName();
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private MailboxViewModel viewModel;
+
+ private NestedScrollView scrollView;
+ protected Button buttonView;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentActivity activity = requireActivity();
+ getAndroidComponent(activity).inject(this);
+ viewModel = new ViewModelProvider(activity, viewModelFactory)
+ .get(MailboxViewModel.class);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View v = inflater
+ .inflate(R.layout.fragment_offline, container, false);
+
+ scrollView = (NestedScrollView) v;
+ Button checkButton = v.findViewById(R.id.checkButton);
+ checkButton.setOnClickListener(view -> {
+ Intent i = new Intent(requireContext(), TransportsActivity.class);
+ startActivity(i);
+ });
+ buttonView = v.findViewById(R.id.button);
+ buttonView.setOnClickListener(view -> {
+ getParentFragmentManager().popBackStackImmediate();
+ viewModel.tryAgainWhenOffline();
+ });
+
+ return v;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Scroll down in case the screen is small, so the button is visible
+ scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/SetupDownloadFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/SetupDownloadFragment.java
new file mode 100644
index 000000000..1d76a6c2f
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/SetupDownloadFragment.java
@@ -0,0 +1,93 @@
+package org.briarproject.briar.android.mailbox;
+
+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 android.widget.ScrollView;
+
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.briar.R;
+
+import javax.inject.Inject;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import static android.view.View.FOCUS_DOWN;
+import static org.briarproject.briar.android.AppModule.getAndroidComponent;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class SetupDownloadFragment extends Fragment {
+
+ static final String TAG = SetupDownloadFragment.class.getName();
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private MailboxViewModel viewModel;
+
+ private CameraPermissionManager permissionManager;
+ private ScrollView scrollView;
+
+ private final ActivityResultLauncher permissionLauncher =
+ registerForActivityResult(new RequestMultiplePermissions(), r -> {
+ permissionManager.onRequestPermissionResult(r);
+ if (permissionManager.checkPermissions()) {
+ scanCode();
+ }
+ });
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentActivity activity = requireActivity();
+ getAndroidComponent(activity).inject(this);
+ viewModel = new ViewModelProvider(activity, viewModelFactory)
+ .get(MailboxViewModel.class);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_mailbox_setup_download,
+ container, false);
+ scrollView = v.findViewById(R.id.scrollView);
+
+ permissionManager = new CameraPermissionManager(requireActivity(),
+ permissionLauncher::launch);
+
+ Button scanButton = v.findViewById(R.id.scanButton);
+ scanButton.setOnClickListener(view -> {
+ if (permissionManager.checkPermissions()) {
+ scanCode();
+ }
+ });
+ return v;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ requireActivity().setTitle(R.string.mailbox_setup_title);
+ // Scroll down in case the screen is small, so the button is visible
+ scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
+ // Permissions may have been granted manually while we were stopped
+ permissionManager.resetPermissions();
+ }
+
+ private void scanCode() {
+ viewModel.onScanButtonClicked();
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/SetupIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/SetupIntroFragment.java
new file mode 100644
index 000000000..8ba22a04c
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/SetupIntroFragment.java
@@ -0,0 +1,54 @@
+package org.briarproject.briar.android.mailbox;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ScrollView;
+
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.briar.R;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import static android.view.View.FOCUS_DOWN;
+import static org.briarproject.briar.android.util.UiUtils.showFragment;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class SetupIntroFragment extends Fragment {
+
+ static final String TAG = SetupIntroFragment.class.getName();
+
+ private ScrollView scrollView;
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_mailbox_setup_intro,
+ container, false);
+ scrollView = v.findViewById(R.id.scrollView);
+ Button button = v.findViewById(R.id.continueButton);
+ button.setOnClickListener(view -> {
+ FragmentManager fm = getParentFragmentManager();
+ Fragment f = new SetupDownloadFragment();
+ showFragment(fm, f, SetupDownloadFragment.TAG);
+ });
+ return v;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ requireActivity().setTitle(R.string.mailbox_setup_title);
+ // Scroll down in case the screen is small, so the button is visible
+ scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java
index c4d368db5..219c43891 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java
@@ -1,6 +1,7 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -8,6 +9,7 @@ import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
+import org.briarproject.briar.android.mailbox.MailboxActivity;
import org.briarproject.briar.android.util.ActivityLaunchers.GetImageAdvanced;
import javax.inject.Inject;
@@ -36,7 +38,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
private static final String PREF_KEY_FEEDBACK = "pref_key_send_feedback";
private static final String PREF_KEY_DEV = "pref_key_dev";
private static final String PREF_KEY_EXPLODE = "pref_key_explode";
- private static final String PREF_KEY_SHARE_APP = "pref_key_share_app";
+ private static final String PREF_KEY_MAILBOX = "pref_key_mailbox";
@Inject
ViewModelProvider.Factory viewModelFactory;
@@ -70,6 +72,18 @@ public class SettingsFragment extends PreferenceFragmentCompat {
prefAvatar.setVisible(false);
}
+ Preference prefMailbox =
+ requireNonNull(findPreference(PREF_KEY_MAILBOX));
+ if (viewModel.shouldEnableMailbox()) {
+ prefMailbox.setOnPreferenceClickListener(preference -> {
+ Intent i = new Intent(requireContext(), MailboxActivity.class);
+ startActivity(i);
+ return true;
+ });
+ } else {
+ prefMailbox.setVisible(false);
+ }
+
Preference prefFeedback =
requireNonNull(findPreference(PREF_KEY_FEEDBACK));
prefFeedback.setOnPreferenceClickListener(preference -> {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java
index ba92042f0..694460934 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsViewModel.java
@@ -161,6 +161,10 @@ class SettingsViewModel extends DbViewModel implements EventListener {
return featureFlags.shouldEnableProfilePictures();
}
+ boolean shouldEnableMailbox() {
+ return featureFlags.shouldEnableMailbox();
+ }
+
private void loadOwnIdentityInfo() {
runOnDbThread(() -> {
try {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/Permission.java b/briar-android/src/main/java/org/briarproject/briar/android/util/Permission.java
new file mode 100644
index 000000000..5ebd1f224
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/Permission.java
@@ -0,0 +1,5 @@
+package org.briarproject.briar.android.util;
+
+public enum Permission {
+ UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
index 9c6f182b2..ea6272b71 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
@@ -52,6 +52,7 @@ import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
@@ -146,6 +147,10 @@ public class UiUtils {
public static void showFragment(FragmentManager fm, Fragment f,
@Nullable String tag, boolean addToBackStack) {
+ // don't re-add same (already added/visible) fragment again
+ Fragment fragment = fm.findFragmentByTag(tag);
+ if (fragment != null && fragment.isAdded()) return;
+
FragmentTransaction ta = fm.beginTransaction()
.setCustomAnimations(R.anim.step_next_in,
R.anim.step_previous_out, R.anim.step_previous_in,
@@ -574,4 +579,26 @@ public class UiUtils {
SOFT_INPUT_STATE_HIDDEN);
}
+ public static void showDenialDialog(FragmentActivity ctx,
+ @StringRes int title, @StringRes int body) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
+ builder.setTitle(title);
+ builder.setMessage(body);
+ builder.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx));
+ builder.setNegativeButton(R.string.cancel,
+ (dialog, which) -> ctx.supportFinishAfterTransition());
+ builder.show();
+ }
+
+ public static void showRationale(FragmentActivity ctx, @StringRes int title,
+ @StringRes int body, Runnable requestPermissions) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
+ builder.setTitle(title);
+ builder.setMessage(body);
+ builder.setNeutralButton(R.string.continue_button,
+ (dialog, which) -> requestPermissions.run());
+ builder.show();
+ }
}
diff --git a/briar-android/src/main/res/drawable-night/ic_mailbox_onboarding.xml b/briar-android/src/main/res/drawable-night/ic_mailbox_onboarding.xml
new file mode 100644
index 000000000..76a2beda4
--- /dev/null
+++ b/briar-android/src/main/res/drawable-night/ic_mailbox_onboarding.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/drawable/border_qr_scanner.xml b/briar-android/src/main/res/drawable/border_qr_scanner.xml
new file mode 100644
index 000000000..c059d1504
--- /dev/null
+++ b/briar-android/src/main/res/drawable/border_qr_scanner.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/drawable/ic_mailbox.xml b/briar-android/src/main/res/drawable/ic_mailbox.xml
new file mode 100644
index 000000000..592cafb24
--- /dev/null
+++ b/briar-android/src/main/res/drawable/ic_mailbox.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/briar-android/src/main/res/drawable/ic_mailbox_onboarding.xml b/briar-android/src/main/res/drawable/ic_mailbox_onboarding.xml
new file mode 100644
index 000000000..abd0bc87b
--- /dev/null
+++ b/briar-android/src/main/res/drawable/ic_mailbox_onboarding.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/activity_mailbox.xml b/briar-android/src/main/res/layout/activity_mailbox.xml
new file mode 100644
index 000000000..27e3eff1c
--- /dev/null
+++ b/briar-android/src/main/res/layout/activity_mailbox.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/fragment_mailbox_connecting.xml b/briar-android/src/main/res/layout/fragment_mailbox_connecting.xml
new file mode 100644
index 000000000..c7cf2b242
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_mailbox_connecting.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/fragment_mailbox_scan.xml b/briar-android/src/main/res/layout/fragment_mailbox_scan.xml
new file mode 100644
index 000000000..b93440c7a
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_mailbox_scan.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/fragment_mailbox_setup_download.xml b/briar-android/src/main/res/layout/fragment_mailbox_setup_download.xml
new file mode 100644
index 000000000..a0bf77a92
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_mailbox_setup_download.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/fragment_mailbox_setup_intro.xml b/briar-android/src/main/res/layout/fragment_mailbox_setup_intro.xml
new file mode 100644
index 000000000..613287b0a
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_mailbox_setup_intro.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/fragment_offline.xml b/briar-android/src/main/res/layout/fragment_offline.xml
new file mode 100644
index 000000000..f45a23d33
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_offline.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index e2842fc44..89ffffe0b 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -612,6 +612,23 @@
Choose ringtone
Cannot load ringtone
+
+ Mailbox
+ Mailbox Setup
+ A Mailbox enables your contacts to send you messages while you are offline. The Mailbox will receive your messages and store them until you come online.\n
+ \nYou can install the Briar Mailbox app on a spare device. Keep it connected to power and Wi-Fi so it\'s always online.
+ First, install the Mailbox app on another device by searching for \"Briar Mailbox\" on Google Play or wherever you downloaded Briar.\n
+ \nThen link your Mailbox with Briar by scanning the QR code shown by the Mailbox app.
+ Share Download Link
+ Scan Mailbox QR code
+ You have denied access to the camera, but scanning a QR code requires using the camera.\n\nPlease consider granting access.
+ Connecting…
+ Wrong QR code
+ The scanned code is invalid. Please open the Briar Mailbox app on your mailbox device and scan the QR code it presents.
+ Offline
+ Ensure that this device is online and connections to the internet are allowed.\n\nAfterwards, wait for the globe icon in connection settings to turn green.
+ Check connection settings
+
Disappearing messages
Turning on this setting will make new
diff --git a/briar-android/src/main/res/xml/settings.xml b/briar-android/src/main/res/xml/settings.xml
index 3bd775506..f968985ff 100644
--- a/briar-android/src/main/res/xml/settings.xml
+++ b/briar-android/src/main/res/xml/settings.xml
@@ -24,6 +24,11 @@
app:fragment="org.briarproject.briar.android.settings.NotificationsFragment"
app:icon="@drawable/ic_notifications" />
+
+