From ddc01b539fcda64d82f60a7a486f42979a4ea6bb Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 7 Feb 2020 20:37:35 -0500 Subject: [PATCH] Use megaphones for PIN reminders. --- .../ConversationListFragment.java | 20 +- .../ConversationListViewModel.java | 4 +- .../securesms/keyvalue/PinValues.java | 57 +++++ .../securesms/keyvalue/SignalStore.java | 8 +- .../lock/RegistrationLockDialog.java | 173 +-------------- .../lock/RegistrationLockReminders.java | 3 - .../lock/SignalPinReminderDialog.java | 205 ++++++++++++++++++ .../securesms/lock/SignalPinReminders.java | 62 ++++++ .../lock/v2/ConfirmKbsPinFragment.java | 1 + .../megaphone/BasicMegaphoneView.java | 8 +- .../securesms/megaphone/Megaphone.java | 4 +- ...er.java => MegaphoneActionController.java} | 12 +- .../megaphone/MegaphoneViewBuilder.java | 6 +- .../securesms/megaphone/Megaphones.java | 49 ++++- .../megaphone/PinReminderSchedule.java | 23 ++ .../reactions/ReactionsMegaphoneView.java | 6 +- .../main/res/layout/basic_megaphone_view.xml | 3 +- .../main/res/layout/kbs_pin_reminder_view.xml | 84 ++++--- app/src/main/res/values/strings.xml | 10 + app/src/main/res/values/themes.xml | 2 +- 20 files changed, 496 insertions(+), 244 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminders.java rename app/src/main/java/org/thoughtcrime/securesms/megaphone/{MegaphoneListener.java => MegaphoneActionController.java} (68%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/megaphone/PinReminderSchedule.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index a02abad745..b5c883e098 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.conversationlist; import android.Manifest; import android.annotation.SuppressLint; +import android.app.Activity; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Context; @@ -107,7 +108,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.megaphone.Megaphone; -import org.thoughtcrime.securesms.megaphone.MegaphoneListener; +import org.thoughtcrime.securesms.megaphone.MegaphoneActionController; import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.mms.GlideApp; @@ -138,7 +139,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana ItemClickListener, ConversationListSearchAdapter.EventListener, MainNavigator.BackHandler, - MegaphoneListener + MegaphoneActionController { private static final String TAG = Log.tag(ConversationListFragment.class); @@ -367,13 +368,18 @@ public class ConversationListFragment extends MainFragment implements LoaderMana } @Override - public void onMegaphoneSnooze(@NonNull Megaphone megaphone) { - viewModel.onMegaphoneSnoozed(megaphone); + public @NonNull Activity getMegaphoneActivity() { + return requireActivity(); } @Override - public void onMegaphoneCompleted(@NonNull Megaphone megaphone) { - viewModel.onMegaphoneCompleted(megaphone.getEvent()); + public void onMegaphoneSnooze(@NonNull Megaphones.Event event) { + viewModel.onMegaphoneSnoozed(event); + } + + @Override + public void onMegaphoneCompleted(@NonNull Megaphones.Event event) { + viewModel.onMegaphoneCompleted(event); } private void initializeProfileIcon(@NonNull Recipient recipient) { @@ -713,7 +719,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana @Override public void onItemLongClick(ConversationListItem item) { - actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(ConversationListFragment.this); + actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this); defaultAdapter.initializeBatchMode(true); defaultAdapter.toggleThreadInBatchSet(item.getThreadId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java index 8b2743c5f9..11c5f00d57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java @@ -69,8 +69,8 @@ class ConversationListViewModel extends ViewModel { megaphoneRepository.markFinished(event); } - void onMegaphoneSnoozed(@NonNull Megaphone snoozed) { - megaphoneRepository.markSeen(snoozed.getEvent()); + void onMegaphoneSnoozed(@NonNull Megaphones.Event event) { + megaphoneRepository.markSeen(event); megaphone.postValue(null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java new file mode 100644 index 0000000000..c2be6e955c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.keyvalue; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.lock.SignalPinReminders; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +public final class PinValues { + + private static final String TAG = Log.tag(PinValues.class); + + private static final String LAST_SUCCESSFUL_ENTRY = "pin.last_successful_entry"; + private static final String NEXT_INTERVAL = "pin.interval_index"; + + private final KeyValueStore store; + + PinValues(KeyValueStore store) { + this.store = store; + } + + public void onEntrySuccess() { + long nextInterval = SignalPinReminders.getNextInterval(getCurrentInterval()); + Log.w(TAG, "onEntrySuccess() nextInterval: " + nextInterval); + + store.beginWrite() + .putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis()) + .putLong(NEXT_INTERVAL, nextInterval) + .apply(); + } + + public void onEntryFailure() { + long nextInterval = SignalPinReminders.getPreviousInterval(getCurrentInterval()); + Log.w(TAG, "onEntryFailure() nextInterval: " + nextInterval); + + store.beginWrite() + .putLong(NEXT_INTERVAL, nextInterval) + .apply(); + } + + public void onPinChange() { + long nextInterval = SignalPinReminders.INITIAL_INTERVAL; + Log.w(TAG, "onPinChange() nextInterval: " + nextInterval); + + store.beginWrite() + .putLong(NEXT_INTERVAL, nextInterval) + .putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis()) + .apply(); + } + + public long getCurrentInterval() { + return store.getLong(NEXT_INTERVAL, TextSecurePreferences.getRegistrationLockNextReminderInterval(ApplicationDependencies.getApplication())); + } + + public long getLastSuccessfulEntryTime() { + return store.getLong(LAST_SUCCESSFUL_ENTRY, TextSecurePreferences.getRegistrationLockLastReminderTime(ApplicationDependencies.getApplication())); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index a052e4e19b..7384d99169 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -17,14 +17,18 @@ public final class SignalStore { private SignalStore() {} - public static KbsValues kbsValues() { + public static @NonNull KbsValues kbsValues() { return new KbsValues(getStore()); } - public static RegistrationValues registrationValues() { + public static @NonNull RegistrationValues registrationValues() { return new RegistrationValues(getStore()); } + public static @NonNull PinValues pinValues() { + return new PinValues(getStore()); + } + public static String getRemoteConfig() { return getStore().getString(REMOTE_CONFIG, null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java index be32357176..eca62cffb8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java @@ -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(); - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java index bb34bad54c..b21c2a1b76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java b/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java new file mode 100644 index 0000000000..2240c72998 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminders.java b/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminders.java new file mode 100644 index 0000000000..686f892dbd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminders.java @@ -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 INTERVALS = new TreeSet() {{ + add(ONE_DAY); + add(THREE_DAYS); + add(ONE_WEEK); + add(TWO_WEEKS); + }}; + + private static final Map STRINGS = new HashMap() {{ + 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; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java index d47a429119..98b57822db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java @@ -108,6 +108,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment { - megaphoneListener.onMegaphoneSnooze(megaphone); + megaphoneListener.onMegaphoneSnooze(megaphone.getEvent()); if (megaphone.getSnoozeListener() != null) { megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java index de20adcdd2..33503042c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java @@ -139,7 +139,7 @@ public class Megaphone { return this; } - public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull EventListener listener) { + public @NonNull Builder setActionButton(@StringRes int buttonTextRes, @NonNull EventListener listener) { this.buttonTextRes = buttonTextRes; this.buttonListener = listener; return this; @@ -160,6 +160,6 @@ public class Megaphone { } public interface EventListener { - void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener); + void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneListener.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java similarity index 68% rename from app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneListener.java rename to app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java index 4a3a44a874..6aea1a9cb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneActionController.java @@ -1,11 +1,12 @@ package org.thoughtcrime.securesms.megaphone; +import android.app.Activity; import android.content.Intent; import androidx.annotation.NonNull; import androidx.annotation.StringRes; -public interface MegaphoneListener { +public interface MegaphoneActionController { /** * When a megaphone wants to navigate to a specific intent. */ @@ -21,13 +22,18 @@ public interface MegaphoneListener { */ void onMegaphoneToastRequested(@NonNull String string); + /** + * When a megaphone needs a raw activity reference. Favor more specific methods when possible. + */ + @NonNull Activity getMegaphoneActivity(); + /** * When a megaphone has been snoozed via "remind me later" or a similar option. */ - void onMegaphoneSnooze(@NonNull Megaphone megaphone); + void onMegaphoneSnooze(@NonNull Megaphones.Event event); /** * Called when a megaphone completed its goal. */ - void onMegaphoneCompleted(@NonNull Megaphone megaphone); + void onMegaphoneCompleted(@NonNull Megaphones.Event event); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java index ca37a8ff98..fa0c65a843 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneViewBuilder.java @@ -12,7 +12,7 @@ public class MegaphoneViewBuilder { public static @Nullable View build(@NonNull Context context, @NonNull Megaphone megaphone, - @NonNull MegaphoneListener listener) + @NonNull MegaphoneActionController listener) { switch (megaphone.getStyle()) { case BASIC: @@ -28,7 +28,7 @@ public class MegaphoneViewBuilder { private static @NonNull View buildBasicMegaphone(@NonNull Context context, @NonNull Megaphone megaphone, - @NonNull MegaphoneListener listener) + @NonNull MegaphoneActionController listener) { BasicMegaphoneView view = new BasicMegaphoneView(context); view.present(megaphone, listener); @@ -37,7 +37,7 @@ public class MegaphoneViewBuilder { private static @NonNull View buildReactionsMegaphone(@NonNull Context context, @NonNull Megaphone megaphone, - @NonNull MegaphoneListener listener) + @NonNull MegaphoneActionController listener) { ReactionsMegaphoneView view = new ReactionsMegaphoneView(context); view.present(megaphone, listener); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index a53e37e0f4..ac9bc3def1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -12,6 +12,11 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.model.MegaphoneRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.RegistrationLockDialog; +import org.thoughtcrime.securesms.lock.RegistrationLockReminders; +import org.thoughtcrime.securesms.lock.SignalPinReminderDialog; +import org.thoughtcrime.securesms.lock.SignalPinReminders; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity; import org.thoughtcrime.securesms.lock.v2.PinUtil; @@ -76,6 +81,7 @@ public final class Megaphones { return new LinkedHashMap() {{ put(Event.REACTIONS, new ForeverSchedule(true)); put(Event.PINS_FOR_ALL, new PinsForAllSchedule()); + put(Event.PIN_REMINDER, new PinReminderSchedule()); }}; } @@ -84,7 +90,9 @@ public final class Megaphones { case REACTIONS: return buildReactionsMegaphone(); case PINS_FOR_ALL: - return buildPinsForAllMegaphone(context, record); + return buildPinsForAllMegaphone(record); + case PIN_REMINDER: + return buildPinReminderMegaphone(context); default: throw new IllegalArgumentException("Event not handled!"); } @@ -96,7 +104,7 @@ public final class Megaphones { .build(); } - private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull Context context, @NonNull MegaphoneRecord record) { + private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull MegaphoneRecord record) { if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN) .setMandatory(true) @@ -125,7 +133,7 @@ public final class Megaphones { private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithPin(@NonNull Megaphone.Builder builder) { return builder.setTitle(R.string.KbsMegaphone__introducing_pins) .setBody(R.string.KbsMegaphone__your_registration_lock_is_now_called_a_pin) - .setButtonText(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> { + .setActionButton(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> { Intent intent = CreateKbsPinActivity.getIntentForPinChangeFromSettings(ApplicationDependencies.getApplication()); listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN); @@ -136,7 +144,7 @@ public final class Megaphones { private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithoutPin(@NonNull Megaphone.Builder builder) { return builder.setTitle(R.string.KbsMegaphone__create_a_pin) .setBody(R.string.KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account) - .setButtonText(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> { + .setActionButton(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> { Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication()); listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN); @@ -144,9 +152,40 @@ public final class Megaphones { .build(); } + private static @NonNull Megaphone buildPinReminderMegaphone(@NonNull Context context) { + return new Megaphone.Builder(Event.PIN_REMINDER, Megaphone.Style.BASIC) + .setTitle(R.string.Megaphones_verify_your_signal_pin) + .setBody(R.string.Megaphones_well_occasionally_ask_you_to_verify_your_pin) + .setImage(R.drawable.kbs_pin_megaphone) + .setActionButton(R.string.Megaphones_verify_pin, (megaphone, controller) -> { + SignalPinReminderDialog.show(controller.getMegaphoneActivity(), controller::onMegaphoneNavigationRequested, new SignalPinReminderDialog.Callback() { + @Override + public void onReminderDismissed(boolean includedFailure) { + if (includedFailure) { + SignalStore.pinValues().onEntryFailure(); + } + } + + @Override + public void onReminderCompleted(boolean includedFailure) { + if (includedFailure) { + SignalStore.pinValues().onEntryFailure(); + } else { + SignalStore.pinValues().onEntrySuccess(); + } + + controller.onMegaphoneSnooze(Event.PIN_REMINDER); + controller.onMegaphoneToastRequested(context.getString(SignalPinReminders.getReminderString(SignalStore.pinValues().getCurrentInterval()))); + } + }); + }) + .build(); + } + public enum Event { REACTIONS("reactions"), - PINS_FOR_ALL("pins_for_all"); + PINS_FOR_ALL("pins_for_all"), + PIN_REMINDER("pin_reminder"); private final String key; diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinReminderSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinReminderSchedule.java new file mode 100644 index 0000000000..0d6e587e7a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinReminderSchedule.java @@ -0,0 +1,23 @@ +package org.thoughtcrime.securesms.megaphone; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.util.FeatureFlags; + +final class PinReminderSchedule implements MegaphoneSchedule { + + @Override + public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) { + if (!SignalStore.kbsValues().isV2RegistrationLockEnabled()) { + return false; + } + + if (!FeatureFlags.pinsForAll()) { + return false; + } + + long lastSuccessTime = SignalStore.pinValues().getLastSuccessfulEntryTime(); + long interval = SignalStore.pinValues().getCurrentInterval(); + + return currentTime - lastSuccessTime >= interval; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsMegaphoneView.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsMegaphoneView.java index fe6e98743f..90e9eaa40b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsMegaphoneView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsMegaphoneView.java @@ -9,7 +9,7 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.megaphone.Megaphone; -import org.thoughtcrime.securesms.megaphone.MegaphoneListener; +import org.thoughtcrime.securesms.megaphone.MegaphoneActionController; public class ReactionsMegaphoneView extends FrameLayout { @@ -31,7 +31,7 @@ public class ReactionsMegaphoneView extends FrameLayout { this.closeButton = findViewById(R.id.reactions_megaphone_x); } - public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener) { - this.closeButton.setOnClickListener(v -> listener.onMegaphoneCompleted(megaphone)); + public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener) { + this.closeButton.setOnClickListener(v -> listener.onMegaphoneCompleted(megaphone.getEvent())); } } diff --git a/app/src/main/res/layout/basic_megaphone_view.xml b/app/src/main/res/layout/basic_megaphone_view.xml index 2cded62036..fe6fa5a930 100644 --- a/app/src/main/res/layout/basic_megaphone_view.xml +++ b/app/src/main/res/layout/basic_megaphone_view.xml @@ -12,7 +12,8 @@ android:paddingStart="8dp" android:paddingEnd="8dp" android:paddingBottom="8dp" - android:background="?megaphone_background"> + android:background="?megaphone_background" + android:clickable="true"> - + android:padding="20dp"> - - - - - + android:text="@string/KbsReminderDialog__enter_your_signal_pin" + style="@style/Signal.Text.Body" + android:fontFamily="sans-serif-medium" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + android:paddingEnd="80dp" + app:layout_constraintTop_toBottomOf="@id/pin_reminder_title" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> - + android:text="@string/KbsReminderDialog__skip" + app:layout_constraintTop_toTopOf="@id/submit" + app:layout_constraintEnd_toStartOf="@id/submit"/> -