From 47cd1b568f7b67b968d2a55920dacb2e0780d081 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 25 Jun 2024 12:48:01 -0400 Subject: [PATCH] Add lock screen help dialog. --- app/src/main/AndroidManifest.xml | 2 +- .../securesms/PassphrasePromptActivity.java | 47 ++++++++++++++++++- .../securesms/keyvalue/MiscellaneousValues.kt | 10 ++++ app/src/main/res/values/strings.xml | 2 + 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e9c56a4842..403c12d324 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -661,7 +661,7 @@ diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java index 17c0ff087c..7088ee3821 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java @@ -44,10 +44,13 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; @@ -55,6 +58,7 @@ import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.crypto.InvalidPassphraseException; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.DynamicIntroTheme; @@ -74,7 +78,8 @@ public class PassphrasePromptActivity extends PassphraseActivity { private static final String TAG = Log.tag(PassphrasePromptActivity.class); private static final short AUTHENTICATE_REQUEST_CODE = 1007; private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown"; - public static final String FROM_FOREGROUND = "from_foreground"; + public static final String FROM_FOREGROUND = "from_foreground"; + private static final int HELP_COUNT_THRESHOLD = 3; private DynamicIntroTheme dynamicTheme = new DynamicIntroTheme(); private DynamicLanguage dynamicLanguage = new DynamicLanguage(); @@ -188,6 +193,7 @@ public class PassphrasePromptActivity extends PassphraseActivity { } else { Log.w(TAG, "Authentication failed"); hadFailure = true; + incrementAttemptCountAndShowHelpIfNecessary(); } } @@ -207,6 +213,7 @@ public class PassphrasePromptActivity extends PassphraseActivity { passphraseText.setText(""); passphraseText.setError( getString(R.string.PassphrasePromptActivity_invalid_passphrase_exclamation)); + incrementAttemptCountAndShowHelpIfNecessary(); } } @@ -216,6 +223,7 @@ public class PassphrasePromptActivity extends PassphraseActivity { MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); setMasterSecret(masterSecret); + SignalStore.misc().setLockScreenAttemptCount(0); } catch (InvalidPassphraseException e) { throw new AssertionError(e); } @@ -272,6 +280,10 @@ public class PassphrasePromptActivity extends PassphraseActivity { fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN); lockScreenButton.setOnClickListener(v -> resumeScreenLock(true)); + + if (SignalStore.misc().getLockScreenAttemptCount() > HELP_COUNT_THRESHOLD) { + showHelpDialogAndResetAttemptCount(null); + } } private void setLockTypeVisibility() { @@ -288,6 +300,10 @@ public class PassphrasePromptActivity extends PassphraseActivity { } private void resumeScreenLock(boolean force) { + if (incrementAttemptCountAndShowHelpIfNecessary(() -> resumeScreenLock(force))) { + return; + } + if (!biometricAuth.authenticate(getApplicationContext(), force, this::showConfirmDeviceCredentialIntent)) { handleAuthenticated(); } @@ -312,6 +328,33 @@ public class PassphrasePromptActivity extends PassphraseActivity { return Unit.INSTANCE; } + private boolean incrementAttemptCountAndShowHelpIfNecessary() { + return incrementAttemptCountAndShowHelpIfNecessary(null); + } + + private boolean incrementAttemptCountAndShowHelpIfNecessary(Runnable onDismissed) { + SignalStore.misc().incrementLockScreenAttemptCount(); + + if (SignalStore.misc().getLockScreenAttemptCount() > HELP_COUNT_THRESHOLD) { + showHelpDialogAndResetAttemptCount(onDismissed); + return true; + } + + return false; + } + + private void showHelpDialogAndResetAttemptCount(@Nullable Runnable onDismissed) { + new MaterialAlertDialogBuilder(this) + .setMessage(R.string.PassphrasePromptActivity_help_prompt_body) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + SignalStore.misc().setLockScreenAttemptCount(0); + if (onDismissed != null) { + onDismissed.run(); + } + }) + .show(); + } + private class PassphraseActionListener implements TextView.OnEditorActionListener { @Override public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) { @@ -366,6 +409,8 @@ public class PassphrasePromptActivity extends PassphraseActivity { Log.w(TAG, "Authentication error: " + errorCode); hadFailure = true; + incrementAttemptCountAndShowHelpIfNecessary(); + if (errorCode != BiometricPrompt.ERROR_CANCELED && errorCode != BiometricPrompt.ERROR_USER_CANCELED) { onAuthenticationFailed(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt index 6312c6b951..8c32330d4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt @@ -36,6 +36,7 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto private const val LINKED_DEVICE_LAST_ACTIVE_CHECK_TIME = "misc.linked_device.last_active_check_time" private const val LEAST_ACTIVE_LINKED_DEVICE = "misc.linked_device.least_active" private const val NEXT_DATABASE_ANALYSIS_TIME = "misc.next_database_analysis_time" + private const val LOCK_SCREEN_ATTEMPT_COUNT = "misc.lock_screen_attempt_count" } public override fun onFirstEverAppLaunch() { @@ -248,4 +249,13 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto * When the next scheduled database analysis is. */ var nextDatabaseAnalysisTime: Long by longValue(NEXT_DATABASE_ANALYSIS_TIME, 0) + + /** + * How many times the lock screen has been seen and _not_ unlocked. Used to determine if the user is confused by how to bypass the lock screen. + */ + var lockScreenAttemptCount: Int by integerValue(LOCK_SCREEN_ATTEMPT_COUNT, 0) + + fun incrementLockScreenAttemptCount() { + lockScreenAttemptCount++ + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5488bea7a4..7190bf4090 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7127,6 +7127,8 @@ Monthly Manually back up + + Please enter your device pin, password or pattern.