mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 09:08:33 +00:00
Use megaphones for PIN reminders.
This commit is contained in:
parent
38e4733433
commit
ddc01b539f
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -108,6 +108,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
|
||||
requireActivity().setResult(Activity.RESULT_OK);
|
||||
closeNavGraphBranch();
|
||||
SignalStore.registrationValues().setRegistrationComplete();
|
||||
SignalStore.pinValues().onPinChange();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user