Use megaphones for PIN reminders.

This commit is contained in:
Greyson Parrelli
2020-02-07 20:37:35 -05:00
parent 38e4733433
commit ddc01b539f
20 changed files with 496 additions and 244 deletions

View File

@@ -6,7 +6,6 @@ import android.graphics.Typeface;
import android.os.AsyncTask;
import android.os.Build;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@@ -27,19 +26,15 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.DialogCompat;
import androidx.fragment.app.Fragment;
import com.google.android.material.textfield.TextInputLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
@@ -66,98 +61,25 @@ public final class RegistrationLockDialog {
public static void showReminderIfNecessary(@NonNull Fragment fragment) {
final Context context = fragment.requireContext();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
if (!RegistrationLockReminders.needsReminder(context)) return;
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) && !SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
return;
}
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) &&
!SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
// Neither v1 or v2 to check against
Log.w(TAG, "Reg lock enabled, but no pin stored to verify against");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
if (!RegistrationLockReminders.needsReminder(context)) {
return;
}
if (FeatureFlags.pinsForAll()) {
showReminder(context, fragment);
} else {
showLegacyPinReminder(context);
}
}
private static void showReminder(@NonNull Context context, @NonNull Fragment fragment) {
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent)
.setView(R.layout.kbs_pin_reminder_view)
.setCancelable(false)
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
.create();
WindowManager windowManager = ServiceUtil.getWindowManager(context);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
dialog.show();
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper);
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
View skip = DialogCompat.requireViewById(dialog, R.id.skip);
View submit = DialogCompat.requireViewById(dialog, R.id.submit);
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
pinEditText.post(() -> {
if (pinEditText.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEditText.getContext()).showSoftInput(pinEditText, 0);
}
});
switch (SignalStore.kbsValues().getKeyboardType()) {
case NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
break;
case ALPHA_NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
break;
return;
}
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, true);
fragment.startActivityForResult(CreateKbsPinActivity.getIntentForPinChangeFromForgotPin(context), CreateKbsPinActivity.REQUEST_NEW_PIN);
}
};
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
reminder.setMovementMethod(LinkMovementMethod.getInstance());
skip.setOnClickListener(v -> {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, false);
});
PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper);
PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled()
? new V2PinVerifier()
: new V1PinVerifier(context);
submit.setOnClickListener(v -> {
Editable pinEditable = pinEditText.getText();
verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback);
});
showLegacyPinReminder(context);
}
/**
* @deprecated TODO [alex]: Remove after pins for all live.
*/
@Deprecated
private static void showLegacyPinReminder(@NonNull Context context) {
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight)
.setView(R.layout.registration_lock_reminder_view)
@@ -403,79 +325,4 @@ public final class RegistrationLockDialog {
dialog.show();
}
private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context,
@NonNull AlertDialog dialog,
@NonNull TextInputLayout inputWrapper)
{
return new PinVerifier.Callback() {
@Override
public void onPinCorrect() {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, true);
}
@Override
public void onPinWrong() {
inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again));
}
};
}
private static final class V1PinVerifier implements PinVerifier {
private final String pinInPreferences;
private V1PinVerifier(@NonNull Context context) {
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) {
callback.onPinCorrect();
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
} else {
callback.onPinWrong();
}
}
}
private static final class V2PinVerifier implements PinVerifier {
private final String localPinHash;
V2PinVerifier() {
localPinHash = SignalStore.kbsValues().getLocalPinHash();
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin == null) return;
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
callback.onPinCorrect();
} else {
callback.onPinWrong();
}
}
}
private interface PinVerifier {
void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback);
interface Callback {
void onPinCorrect();
void onPinWrong();
}
}
}

View File

@@ -25,9 +25,6 @@ public class RegistrationLockReminders {
public static final long INITIAL_INTERVAL = INTERVALS.first();
public static boolean needsReminder(@NonNull Context context) {
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) &&
!SignalStore.kbsValues().isV2RegistrationLockEnabled()) return false;
long lastReminderTime = TextSecurePreferences.getRegistrationLockLastReminderTime(context);
long nextIntervalTime = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);

View File

@@ -0,0 +1,205 @@
package org.thoughtcrime.securesms.lock;
import android.content.Context;
import android.content.Intent;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.DialogCompat;
import com.google.android.material.textfield.TextInputLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
public final class SignalPinReminderDialog {
private static final String TAG = Log.tag(SignalPinReminderDialog.class);
public static void show(@NonNull Context context, @NonNull Launcher launcher, @NonNull Callback mainCallback) {
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent)
.setView(R.layout.kbs_pin_reminder_view)
.setCancelable(false)
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
.create();
WindowManager windowManager = ServiceUtil.getWindowManager(context);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
dialog.show();
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper);
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
View skip = DialogCompat.requireViewById(dialog, R.id.skip);
View submit = DialogCompat.requireViewById(dialog, R.id.submit);
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
pinEditText.post(() -> {
if (pinEditText.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEditText.getContext()).showSoftInput(pinEditText, 0);
}
});
switch (SignalStore.kbsValues().getKeyboardType()) {
case NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
break;
case ALPHA_NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
break;
}
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
dialog.dismiss();
launcher.launch(CreateKbsPinActivity.getIntentForPinChangeFromForgotPin(context), CreateKbsPinActivity.REQUEST_NEW_PIN);
}
};
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
reminder.setMovementMethod(LinkMovementMethod.getInstance());
PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper, mainCallback);
PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled()
? new V2PinVerifier()
: new V1PinVerifier(context);
skip.setOnClickListener(v -> {
dialog.dismiss();
mainCallback.onReminderDismissed(callback.hadWrongGuess());
});
submit.setOnClickListener(v -> {
Editable pinEditable = pinEditText.getText();
verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback);
});
}
private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context,
@NonNull AlertDialog dialog,
@NonNull TextInputLayout inputWrapper,
@NonNull Callback mainCallback)
{
return new PinVerifier.Callback() {
boolean hadWrongGuess = false;
@Override
public void onPinCorrect() {
dialog.dismiss();
mainCallback.onReminderCompleted(hadWrongGuess);
}
@Override
public void onPinWrong() {
hadWrongGuess = true;
inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again));
}
@Override
public boolean hadWrongGuess() {
return hadWrongGuess;
}
};
}
private static final class V1PinVerifier implements PinVerifier {
private final String pinInPreferences;
private V1PinVerifier(@NonNull Context context) {
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) {
callback.onPinCorrect();
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
} else {
callback.onPinWrong();
}
}
}
private static final class V2PinVerifier implements PinVerifier {
private final String localPinHash;
V2PinVerifier() {
localPinHash = SignalStore.kbsValues().getLocalPinHash();
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin == null) return;
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
callback.onPinCorrect();
} else {
callback.onPinWrong();
}
}
}
private interface PinVerifier {
void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback);
interface Callback {
void onPinCorrect();
void onPinWrong();
boolean hadWrongGuess();
}
}
public interface Launcher {
void launch(@NonNull Intent intent, int requestCode);
}
public interface Callback {
void onReminderDismissed(boolean includedFailure);
void onReminderCompleted(boolean includedFailure);
}
}

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.lock;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
/**
* Reminder intervals for Signal PINs.
*/
public class SignalPinReminders {
private static final String TAG = Log.tag(SignalPinReminders.class);
private static final long ONE_DAY = TimeUnit.DAYS.toMillis(1);
private static final long THREE_DAYS = TimeUnit.DAYS.toMillis(3);
private static final long ONE_WEEK = TimeUnit.DAYS.toMillis(7);
private static final long TWO_WEEKS = TimeUnit.DAYS.toMillis(14);
private static final NavigableSet<Long> INTERVALS = new TreeSet<Long>() {{
add(ONE_DAY);
add(THREE_DAYS);
add(ONE_WEEK);
add(TWO_WEEKS);
}};
private static final Map<Long, Integer> STRINGS = new HashMap<Long, Integer>() {{
put(ONE_DAY, R.string.SignalPinReminders_well_remind_you_again_tomorrow);
put(THREE_DAYS, R.string.SignalPinReminders_well_remind_you_again_in_a_few_days);
put(ONE_WEEK, R.string.SignalPinReminders_well_remind_you_again_in_a_week);
put(TWO_WEEKS, R.string.SignalPinReminders_well_remind_you_again_in_a_couple_weeks);
}};
public static final long INITIAL_INTERVAL = INTERVALS.first();
public static long getNextInterval(long currentInterval) {
Long next = INTERVALS.higher(currentInterval);
return next != null ? next : INTERVALS.last();
}
public static long getPreviousInterval(long currentInterval) {
Long previous = INTERVALS.lower(currentInterval);
return previous != null ? previous : INTERVALS.first();
}
public static @StringRes int getReminderString(long interval) {
Integer stringRes = STRINGS.get(interval);
if (stringRes != null) {
return stringRes;
} else {
Log.w(TAG, "Couldn't find a string for interval " + interval);
return R.string.SignalPinReminders_well_remind_you_again_later;
}
}
}

View File

@@ -108,6 +108,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
requireActivity().setResult(Activity.RESULT_OK);
closeNavGraphBranch();
SignalStore.registrationValues().setRegistrationComplete();
SignalStore.pinValues().onPinChange();
}
});
break;