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.