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

@ -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) {

View File

@ -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);
}

View File

@ -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()));
}
}

View File

@ -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);
}

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 {
return;
}
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;
}
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);
});
}
/**
* @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;

View File

@ -21,7 +21,7 @@ public class BasicMegaphoneView extends FrameLayout {
private Button snoozeButton;
private Megaphone megaphone;
private MegaphoneListener megaphoneListener;
private MegaphoneActionController megaphoneListener;
public BasicMegaphoneView(@NonNull Context context) {
super(context);
@ -52,7 +52,7 @@ public class BasicMegaphoneView extends FrameLayout {
}
}
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener megaphoneListener) {
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController megaphoneListener) {
this.megaphone = megaphone;
this.megaphoneListener = megaphoneListener;
@ -92,7 +92,7 @@ public class BasicMegaphoneView extends FrameLayout {
if (megaphone.canSnooze()) {
snoozeButton.setVisibility(VISIBLE);
snoozeButton.setOnClickListener(v -> {
megaphoneListener.onMegaphoneSnooze(megaphone);
megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
if (megaphone.getSnoozeListener() != null) {
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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<Event, MegaphoneSchedule>() {{
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;

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -12,7 +12,8 @@
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:background="?megaphone_background">
android:background="?megaphone_background"
android:clickable="true">
<ImageView
android:id="@+id/basic_megaphone_image"

View File

@ -1,36 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/signal_primary"
android:gravity="center"
android:orientation="horizontal"
android:padding="40dp">
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:id="@+id/pin_reminder_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:gravity="center"
android:text="@string/KbsReminderDialog__enter_your_signal_pin"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
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" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pin_wrapper"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="80dp"
android:paddingTop="40dp"
android:paddingEnd="80dp">
android:paddingEnd="80dp"
app:layout_constraintTop_toBottomOf="@id/pin_reminder_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin"
@ -43,37 +41,33 @@
<TextView
android:id="@+id/reminder"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.3"
android:paddingStart="20dp"
android:paddingTop="40dp"
android:paddingEnd="20dp"
android:paddingBottom="40dp"
android:textSize="15sp"
style="@style/Signal.Text.Preview"
app:layout_constraintTop_toBottomOf="@id/pin_wrapper"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@string/KbsReminderDialog__to_help_you_memorize_your_pin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
<Button
android:id="@+id/skip"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/KbsReminderDialog__skip" />
android:text="@string/KbsReminderDialog__skip"
app:layout_constraintTop_toTopOf="@id/submit"
app:layout_constraintEnd_toStartOf="@id/submit"/>
<Button
android:id="@+id/submit"
style="@style/Button.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/KbsReminderDialog__submit" />
android:text="@string/KbsReminderDialog__submit"
app:layout_constraintTop_toBottomOf="@id/reminder"
app:layout_constraintEnd_toEndOf="parent"/>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -545,6 +545,9 @@
<string name="Megaphones_introducing_reactions">Introducing Reactions</string>
<string name="Megaphones_tap_and_hold_any_message_to_quicky_share_how_you_feel">Tap and hold any message to quickly share how you feel.</string>
<string name="Megaphones_remind_me_later">Remind me later</string>
<string name="Megaphones_verify_your_signal_pin">Verify your Signal PIN</string>
<string name="Megaphones_well_occasionally_ask_you_to_verify_your_pin">We\'ll occasionally ask you to verify your PIN so that you remember it.</string>
<string name="Megaphones_verify_pin">Verify PIN</string>
<!-- NotificationBarManager -->
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
@ -775,6 +778,13 @@
<string name="SharedContactView_invite_to_signal">Invite to Signal</string>
<string name="SharedContactView_message">Signal Message</string>
<!-- SignalPinReminders -->
<string name="SignalPinReminders_well_remind_you_again_later">We\'ll remind you again later.</string>
<string name="SignalPinReminders_well_remind_you_again_tomorrow">We\'ll remind you again tomorrow.</string>
<string name="SignalPinReminders_well_remind_you_again_in_a_few_days">We\'ll remind you again in a few days.</string>
<string name="SignalPinReminders_well_remind_you_again_in_a_week">We\'ll remind you again in a week.</string>
<string name="SignalPinReminders_well_remind_you_again_in_a_couple_weeks">We\'ll remind you again in a couple weeks.</string>
<!-- Slide -->
<string name="Slide_image">Image</string>
<string name="Slide_sticker">Sticker</string>

View File

@ -321,7 +321,7 @@
<item name="media_keyboard_button_color">@color/core_grey_60</item>
<item name="megaphone_background">@color/core_white</item>
<item name="megaphone_background">@color/core_grey_05</item>
<item name="megaphone_background_shadow">@drawable/megaphone_background_shadow</item>
<item name="megaphone_body_text_color">@color/core_grey_65</item>
<item name="megaphone_reactions_shade">@color/core_grey_02</item>