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 @@ + + + + + + + + + +