Implement new PIN UX.

This commit is contained in:
Alex Hart
2020-01-30 16:23:29 -04:00
parent 109d67956f
commit fb82420376
71 changed files with 3000 additions and 203 deletions

View File

@@ -6,6 +6,7 @@ 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;
@@ -26,13 +27,20 @@ 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;
import org.thoughtcrime.securesms.util.FeatureFlags;
@@ -43,7 +51,6 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
@@ -55,10 +62,9 @@ public final class RegistrationLockDialog {
private static final String TAG = Log.tag(RegistrationLockDialog.class);
private static final int MIN_V2_NUMERIC_PIN_LENGTH_ENTRY = 4;
private static final int MIN_V2_NUMERIC_PIN_LENGTH_SETTING = 4;
public static void showReminderIfNecessary(@NonNull Fragment fragment) {
final Context context = fragment.requireContext();
public static void showReminderIfNecessary(@NonNull Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
if (!RegistrationLockReminders.needsReminder(context)) return;
@@ -69,6 +75,86 @@ public final class RegistrationLockDialog {
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.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
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.requestFocus();
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.getIntentForPinUpdate(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)
.setCancelable(true)
@@ -84,8 +170,8 @@ public final class RegistrationLockDialog {
dialog.show();
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
EditText pinEditText = dialog.findViewById(R.id.pin);
TextView reminder = dialog.findViewById(R.id.reminder);
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
if (pinEditText == null) throw new AssertionError();
if (reminder == null) throw new AssertionError();
@@ -136,17 +222,15 @@ public final class RegistrationLockDialog {
private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) {
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getPinBackedMasterKey();
String localPinHash = kbsValues.getLocalPinHash();
if (masterKey == null) throw new AssertionError("No masterKey set at time of reminder");
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
return new AfterTextChanged((Editable s) -> {
if (s == null) return;
String pin = s.toString();
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < MIN_V2_NUMERIC_PIN_LENGTH_ENTRY) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
dialog.dismiss();
@@ -178,9 +262,9 @@ public final class RegistrationLockDialog {
String pinValue = pin.getText().toString().replace(" ", "");
String repeatValue = repeat.getText().toString().replace(" ", "");
if (pinValue.length() < MIN_V2_NUMERIC_PIN_LENGTH_SETTING) {
if (pinValue.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) {
Toast.makeText(context,
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, MIN_V2_NUMERIC_PIN_LENGTH_SETTING),
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH),
Toast.LENGTH_LONG).show();
return;
}
@@ -325,4 +409,78 @@ 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

@@ -35,19 +35,19 @@ public class RegistrationLockReminders {
}
public static void scheduleReminder(@NonNull Context context, boolean success) {
Long nextReminderInterval;
if (success) {
long timeSinceLastReminder = System.currentTimeMillis() - TextSecurePreferences.getRegistrationLockLastReminderTime(context);
nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
if (nextReminderInterval == null) nextReminderInterval = INTERVALS.last();
} else {
long lastReminderInterval = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);
nextReminderInterval = INTERVALS.lower(lastReminderInterval);
if (nextReminderInterval == null) nextReminderInterval = INTERVALS.first();
}
Long nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
if (nextReminderInterval == null) {
nextReminderInterval = INTERVALS.last();
}
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
} else {
long timeSinceLastReminder = TextSecurePreferences.getRegistrationLockLastReminderTime(context) + TimeUnit.MINUTES.toMillis(5);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, timeSinceLastReminder);
}
}
}

View File

@@ -0,0 +1,149 @@
package org.thoughtcrime.securesms.lock.v2;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import com.airbnb.lottie.LottieAnimationView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends Fragment {
private TextView title;
private TextView description;
private EditText input;
private TextView label;
private TextView keyboardToggle;
private TextView confirm;
private LottieAnimationView lottieProgress;
private LottieAnimationView lottieEnd;
private ViewModel viewModel;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.base_kbs_pin_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeViews(view);
viewModel = initializeViewModel();
viewModel.getUserEntry().observe(getViewLifecycleOwner(), kbsPin -> {
boolean isEntryValid = kbsPin.length() >= KbsConstants.MINIMUM_NEW_PIN_LENGTH;
confirm.setEnabled(isEntryValid);
confirm.setAlpha(isEntryValid ? 1f : 0.5f);
});
viewModel.getKeyboard().observe(getViewLifecycleOwner(), keyboardType -> {
updateKeyboard(keyboardType);
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
});
initializeListeners();
}
@Override
public void onResume() {
super.onResume();
input.requestFocus();
}
protected abstract ViewModel initializeViewModel();
protected abstract void initializeViewStates();
protected TextView getTitle() {
return title;
}
protected TextView getDescription() {
return description;
}
protected EditText getInput() {
return input;
}
protected LottieAnimationView getLottieProgress() {
return lottieProgress;
}
protected LottieAnimationView getLottieEnd() {
return lottieEnd;
}
protected TextView getLabel() {
return label;
}
protected TextView getKeyboardToggle() {
return keyboardToggle;
}
protected TextView getConfirm() {
return confirm;
}
private void initializeViews(@NonNull View view) {
title = view.findViewById(R.id.edit_kbs_pin_title);
description = view.findViewById(R.id.edit_kbs_pin_description);
input = view.findViewById(R.id.edit_kbs_pin_input);
label = view.findViewById(R.id.edit_kbs_pin_input_label);
keyboardToggle = view.findViewById(R.id.edit_kbs_pin_keyboard_toggle);
confirm = view.findViewById(R.id.edit_kbs_pin_confirm);
lottieProgress = view.findViewById(R.id.edit_kbs_pin_lottie_progress);
lottieEnd = view.findViewById(R.id.edit_kbs_pin_lottie_end);
initializeViewStates();
}
private void initializeListeners() {
input.addTextChangedListener(new AfterTextChanged(s -> viewModel.setUserEntry(s.toString())));
input.setImeOptions(EditorInfo.IME_ACTION_NEXT);
input.setOnEditorActionListener(this::handleEditorAction);
keyboardToggle.setOnClickListener(v -> viewModel.toggleAlphaNumeric());
confirm.setOnClickListener(v -> viewModel.confirm());
}
private boolean handleEditorAction(@NonNull View view, int actionId, @NonNull KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT && confirm.isEnabled()) {
viewModel.confirm();
}
return true;
}
private void updateKeyboard(@NonNull KbsKeyboardType keyboard) {
boolean isAlphaNumeric = keyboard == KbsKeyboardType.ALPHA_NUMERIC;
input.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
}
private @StringRes int resolveKeyboardToggleText(@NonNull KbsKeyboardType keyboard) {
if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) {
return R.string.BaseKbsPinFragment__create_numeric_pin;
} else {
return R.string.BaseKbsPinFragment__create_alphanumeric_pin;
}
}
}

View File

@@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
interface BaseKbsPinViewModel {
LiveData<KbsPin> getUserEntry();
LiveData<KbsKeyboardType> getKeyboard();
@MainThread
void setUserEntry(String userEntry);
@MainThread
void toggleAlphaNumeric();
@MainThread
void confirm();
}

View File

@@ -0,0 +1,186 @@
package org.thoughtcrime.securesms.lock.v2;
import android.animation.Animator;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.RawRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.util.Preconditions;
import androidx.lifecycle.ViewModelProviders;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieDrawable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.animation.AnimationRepeatListener;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.util.SpanUtil;
public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewModel> {
private ConfirmKbsPinFragmentArgs args;
private ConfirmKbsPinViewModel viewModel;
@Override
protected void initializeViewStates() {
args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments());
if (args.getIsNewPin()) {
initializeViewStatesForNewPin();
} else {
initializeViewStatesForPin();
}
}
@Override
protected ConfirmKbsPinViewModel initializeViewModel() {
KbsPin userEntry = Preconditions.checkNotNull(args.getUserEntry());
KbsKeyboardType keyboard = args.getKeyboard();
ConfirmKbsPinRepository repository = new ConfirmKbsPinRepository();
ConfirmKbsPinViewModel.Factory factory = new ConfirmKbsPinViewModel.Factory(userEntry, keyboard, repository);
viewModel = ViewModelProviders.of(this, factory).get(ConfirmKbsPinViewModel.class);
viewModel.getLabel().observe(getViewLifecycleOwner(), this::updateLabel);
viewModel.getSaveAnimation().observe(getViewLifecycleOwner(), this::updateSaveAnimation);
return viewModel;
}
private void initializeViewStatesForNewPin() {
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
getKeyboardToggle().setVisibility(View.INVISIBLE);
getLabel().setText("");
}
private void initializeViewStatesForPin() {
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
getKeyboardToggle().setVisibility(View.INVISIBLE);
getLabel().setText("");
}
private void updateLabel(@NonNull ConfirmKbsPinViewModel.Label label) {
switch (label) {
case EMPTY:
getLabel().setText("");
break;
case CREATING_PIN:
getLabel().setText(R.string.ConfirmKbsPinFragment__creating_pin);
break;
case RE_ENTER_PIN:
getLabel().setText(R.string.ConfirmKbsPinFragment__re_enter_pin);
break;
case PIN_DOES_NOT_MATCH:
getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red),
getString(R.string.ConfirmKbsPinFragment__pins_dont_match)));
break;
}
}
private void updateSaveAnimation(@NonNull ConfirmKbsPinViewModel.SaveAnimation animation) {
updateAnimationAndInputVisibility(animation);
LottieAnimationView lottieProgress = getLottieProgress();
switch (animation) {
case NONE:
lottieProgress.cancelAnimation();
break;
case LOADING:
lottieProgress.setAnimation(R.raw.lottie_kbs_loading);
lottieProgress.setRepeatMode(LottieDrawable.RESTART);
lottieProgress.setRepeatCount(LottieDrawable.INFINITE);
lottieProgress.playAnimation();
break;
case SUCCESS:
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_success, new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
requireActivity().setResult(Activity.RESULT_OK);
closeNavGraphBranch();
}
});
break;
case FAILURE:
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
displayFailedDialog();
}
});
break;
}
}
private void startEndAnimationOnNextProgressRepetition(@RawRes int lottieAnimationId,
@NonNull AnimationCompleteListener listener)
{
LottieAnimationView lottieProgress = getLottieProgress();
LottieAnimationView lottieEnd = getLottieEnd();
lottieEnd.setAnimation(lottieAnimationId);
lottieEnd.removeAllAnimatorListeners();
lottieEnd.setRepeatCount(0);
lottieEnd.addAnimatorListener(listener);
if (lottieProgress.isAnimating()) {
lottieProgress.addAnimatorListener(new AnimationRepeatListener(animator ->
hideProgressAndStartEndAnimation(lottieProgress, lottieEnd)
));
} else {
hideProgressAndStartEndAnimation(lottieProgress, lottieEnd);
}
}
private void hideProgressAndStartEndAnimation(@NonNull LottieAnimationView lottieProgress,
@NonNull LottieAnimationView lottieEnd)
{
viewModel.onLoadingAnimationComplete();
lottieProgress.setVisibility(View.GONE);
lottieEnd.setVisibility(View.VISIBLE);
lottieEnd.playAnimation();
}
private void updateAnimationAndInputVisibility(ConfirmKbsPinViewModel.SaveAnimation saveAnimation) {
if (saveAnimation == ConfirmKbsPinViewModel.SaveAnimation.NONE) {
getInput().setVisibility(View.VISIBLE);
getLottieProgress().setVisibility(View.GONE);
} else {
getInput().setVisibility(View.GONE);
getLottieProgress().setVisibility(View.VISIBLE);
}
}
private void displayFailedDialog() {
new AlertDialog.Builder(requireContext()).setTitle(R.string.ConfirmKbsPinFragment__pin_creation_failed)
.setMessage(R.string.ConfirmKbsPinFragment__your_pin_was_not_saved)
.setCancelable(false)
.setPositiveButton(R.string.ok, (d, w) -> {
d.dismiss();
markMegaphoneSeenIfNecessary();
requireActivity().setResult(Activity.RESULT_CANCELED);
closeNavGraphBranch();
})
.show();
}
private void closeNavGraphBranch() {
Intent activityIntent = requireActivity().getIntent();
if (activityIntent != null && activityIntent.hasExtra("next_intent")) {
startActivity(activityIntent.getParcelableExtra("next_intent"));
}
requireActivity().finish();
}
private void markMegaphoneSeenIfNecessary() {
ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.PINS_FOR_ALL);
}
}

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
final class ConfirmKbsPinRepository {
private static final String TAG = Log.tag(ConfirmKbsPinRepository.class);
void setPin(@NonNull KbsPin kbsPin, @NonNull KbsKeyboardType keyboard, @NonNull Consumer<PinSetResult> resultConsumer) {
Context context = ApplicationDependencies.getApplication();
String pinValue = kbsPin.toString();
SimpleTask.run(() -> {
try {
Log.i(TAG, "Setting pin on KBS");
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
.restorePin(hashedPin);
if (!restoredData.getMasterKey().equals(masterKey)) {
throw new AssertionError("Failed to set the pin correctly");
} else {
Log.i(TAG, "Set and retrieved pin on KBS successfully");
}
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
TextSecurePreferences.clearOldRegistrationLockPin(context);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
SignalStore.kbsValues().setKeyboardType(keyboard);
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL);
return PinSetResult.SUCCESS;
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException e) {
Log.w(TAG, e);
return PinSetResult.FAILURE;
}
}, resultConsumer::accept);
}
enum PinSetResult {
SUCCESS,
FAILURE
}
}

View File

@@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinRepository.PinSetResult;
final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
private final ConfirmKbsPinRepository repository;
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
private final MutableLiveData<KbsKeyboardType> keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC);
private final MutableLiveData<SaveAnimation> saveAnimation = new MutableLiveData<>(SaveAnimation.NONE);
private final MutableLiveData<Label> label = new MutableLiveData<>(Label.RE_ENTER_PIN);
private final KbsPin pinToConfirm;
private ConfirmKbsPinViewModel(@NonNull KbsPin pinToConfirm,
@NonNull KbsKeyboardType keyboard,
@NonNull ConfirmKbsPinRepository repository)
{
this.keyboard.setValue(keyboard);
this.pinToConfirm = pinToConfirm;
this.repository = repository;
}
LiveData<SaveAnimation> getSaveAnimation() {
return Transformations.distinctUntilChanged(saveAnimation);
}
LiveData<Label> getLabel() {
return Transformations.distinctUntilChanged(label);
}
@Override
public void confirm() {
KbsPin userEntry = this.userEntry.getValue();
if (pinToConfirm.toString().equals(userEntry.toString())) {
this.label.setValue(Label.CREATING_PIN);
this.userEntry.setValue(KbsPin.EMPTY);
this.saveAnimation.setValue(SaveAnimation.LOADING);
repository.setPin(pinToConfirm, Preconditions.checkNotNull(this.keyboard.getValue()), this::handleResult);
} else {
this.label.setValue(Label.PIN_DOES_NOT_MATCH);
}
}
void onLoadingAnimationComplete() {
this.label.setValue(Label.EMPTY);
}
@Override
public LiveData<KbsPin> getUserEntry() {
return userEntry;
}
@Override
public LiveData<KbsKeyboardType> getKeyboard() {
return keyboard;
}
@MainThread
public void setUserEntry(String userEntry) {
this.userEntry.setValue(KbsPin.from(userEntry));
}
@MainThread
public void toggleAlphaNumeric() {
this.keyboard.setValue(this.keyboard.getValue().getOther());
}
private void handleResult(PinSetResult result) {
switch (result) {
case SUCCESS:
this.saveAnimation.setValue(SaveAnimation.SUCCESS);
break;
case FAILURE:
this.saveAnimation.setValue(SaveAnimation.FAILURE);
break;
default:
throw new IllegalStateException("Unknown state: " + result.name());
}
}
enum Label {
RE_ENTER_PIN,
PIN_DOES_NOT_MATCH,
CREATING_PIN,
EMPTY
}
enum SaveAnimation {
NONE,
LOADING,
SUCCESS,
FAILURE
}
static final class Factory implements ViewModelProvider.Factory {
private final KbsPin pinToConfirm;
private final KbsKeyboardType keyboard;
private final ConfirmKbsPinRepository repository;
Factory(@NonNull KbsPin pinToConfirm,
@NonNull KbsKeyboardType keyboard,
@NonNull ConfirmKbsPinRepository repository)
{
this.pinToConfirm = pinToConfirm;
this.keyboard = keyboard;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new ConfirmKbsPinViewModel(pinToConfirm, keyboard, repository);
}
}
}

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.PassphrasePromptActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class CreateKbsPinActivity extends BaseActionBarActivity {
public static final int REQUEST_NEW_PIN = 27698;
private static final String IS_NEW_PIN = "is_new_pin";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
public static Intent getIntentForPinCreate(@NonNull Context context) {
return new Intent(context, CreateKbsPinActivity.class);
}
public static Intent getIntentForPinUpdate(@NonNull Context context) {
Intent intent = getIntentForPinCreate(context);
intent.putExtra(IS_NEW_PIN, true);
return intent;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
if (KeyCachingService.isLocked(this)) {
startActivity(getPromptPassphraseIntent());
finish();
return;
}
dynamicTheme.onCreate(this);
setContentView(R.layout.create_kbs_pin_activity);
CreateKbsPinFragmentArgs arguments = CreateKbsPinFragmentArgs.fromBundle(getIntent().getExtras());
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle());
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private Intent getPromptPassphraseIntent() {
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
return intent;
}
}

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
private CreateKbsPinFragmentArgs args;
@Override
protected void initializeViewStates() {
args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
if (args.getIsNewPin()) {
initializeViewStatesForNewPin();
} else {
initializeViewStatesForPin();
}
getLabel().setText(getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits));
getConfirm().setEnabled(false);
}
private void initializeViewStatesForPin() {
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
getDescription().setText(R.string.CreateKbsPinFragment__pins_add_an_extra_layer_of_security);
}
private void initializeViewStatesForNewPin() {
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
getDescription().setText(R.string.CreateKbsPinFragment__because_youre_still_logged_in);
}
@Override
protected CreateKbsPinViewModel initializeViewModel() {
CreateKbsPinViewModel viewModel = ViewModelProviders.of(this).get(CreateKbsPinViewModel.class);
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> getLabel().setText(getLabelText(k)));
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard()));
return viewModel;
}
private void onConfirmPin(@NonNull KbsPin userEntry, @NonNull KbsKeyboardType keyboard) {
CreateKbsPinFragmentDirections.ActionConfirmPin action = CreateKbsPinFragmentDirections.actionConfirmPin();
action.setUserEntry(userEntry);
action.setKeyboard(keyboard);
action.setIsNewPin(args.getIsNewPin());
Navigation.findNavController(requireView()).navigate(action);
}
private String getLabelText(@NonNull KbsKeyboardType keyboard) {
if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) {
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_characters);
} else {
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits);
}
}
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_NEW_PIN_LENGTH, KbsConstants.MINIMUM_NEW_PIN_LENGTH);
}
}

View File

@@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
private final MutableLiveData<KbsKeyboardType> keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC);
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
@Override
public LiveData<KbsPin> getUserEntry() {
return userEntry;
}
@Override
public LiveData<KbsKeyboardType> getKeyboard() {
return keyboard;
}
LiveData<NavigationEvent> getNavigationEvents() { return events; }
@Override
@MainThread
public void setUserEntry(String userEntry) {
this.userEntry.setValue(KbsPin.from(userEntry));
}
@Override
@MainThread
public void toggleAlphaNumeric() {
this.keyboard.setValue(Preconditions.checkNotNull(this.keyboard.getValue()).getOther());
}
@Override
@MainThread
public void confirm() {
events.setValue(new NavigationEvent(Preconditions.checkNotNull(this.getUserEntry().getValue()),
Preconditions.checkNotNull(this.getKeyboard().getValue())));
}
static final class NavigationEvent {
private final KbsPin userEntry;
private final KbsKeyboardType keyboard;
NavigationEvent(@NonNull KbsPin userEntry, @NonNull KbsKeyboardType keyboard) {
this.userEntry = userEntry;
this.keyboard = keyboard;
}
KbsPin getUserEntry() {
return userEntry;
}
KbsKeyboardType getKeyboard() {
return keyboard;
}
}
}

View File

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.lock.v2;
public final class KbsConstants {
static final int MINIMUM_NEW_PIN_LENGTH = 6;
/** Migrated pins from V1 might be 4 */
public static final int MINIMUM_POSSIBLE_PIN_LENGTH = 4;
private KbsConstants() {
}
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.Nullable;
public enum KbsKeyboardType {
NUMERIC("numeric"),
ALPHA_NUMERIC("alphaNumeric");
private final String code;
KbsKeyboardType(String code) {
this.code = code;
}
KbsKeyboardType getOther() {
if (this == NUMERIC) return ALPHA_NUMERIC;
else return NUMERIC;
}
public String getCode() {
return code;
}
public static KbsKeyboardType fromCode(@Nullable String code) {
for (KbsKeyboardType type : KbsKeyboardType.values()) {
if (type.code.equals(code)) {
return type;
}
}
return NUMERIC;
}
}

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.PassphrasePromptActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class KbsMigrationActivity extends BaseActionBarActivity {
public static final int REQUEST_NEW_PIN = CreateKbsPinActivity.REQUEST_NEW_PIN;
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
public static Intent createIntent() {
return new Intent(ApplicationDependencies.getApplication(), KbsMigrationActivity.class);
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
if (KeyCachingService.isLocked(this)) {
startActivity(getPromptPassphraseIntent());
finish();
return;
}
dynamicTheme.onCreate(this);
setContentView(R.layout.kbs_migration_activity);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private Intent getPromptPassphraseIntent() {
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
return intent;
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.lock.v2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class KbsPin implements Parcelable {
public static KbsPin EMPTY = new KbsPin("");
private final String pin;
private KbsPin(String pin) {
this.pin = pin;
}
private KbsPin(Parcel in) {
pin = in.readString();
}
@Override
public @NonNull String toString() {
return pin;
}
public static KbsPin from(@Nullable String pin) {
if (pin == null) return EMPTY;
pin = pin.trim();
if (pin.length() == 0) return EMPTY;
return new KbsPin(pin);
}
public int length() {
return pin.length();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(pin);
}
public static final Creator<KbsPin> CREATOR = new Creator<KbsPin>() {
@Override
public KbsPin createFromParcel(Parcel in) {
return new KbsPin(in);
}
@Override
public KbsPin[] newArray(int size) {
return new KbsPin[size];
}
};
}

View File

@@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public final class KbsSplashFragment extends Fragment {
private TextView title;
private TextView description;
private TextView primaryAction;
private TextView secondaryAction;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.kbs_splash_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
title = view.findViewById(R.id.kbs_splash_title);
description = view.findViewById(R.id.kbs_splash_description);
primaryAction = view.findViewById(R.id.kbs_splash_primary_action);
secondaryAction = view.findViewById(R.id.kbs_splash_secondary_action);
primaryAction.setOnClickListener(v -> onCreatePin());
secondaryAction.setOnClickListener(v -> onLearnMore());
if (TextSecurePreferences.isV1RegistrationLockEnabled(requireContext())) {
setUpRegLockEnabled();
} else {
setUpRegLockDisabled();
}
description.setMovementMethod(LinkMovementMethod.getInstance());
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() { }
});
}
private void setUpRegLockEnabled() {
title.setText(R.string.KbsSplashFragment__registration_lock_equals_pin);
description.setText(R.string.KbsSplashFragment__your_registration_lock_is_now_called_a_pin);
primaryAction.setText(R.string.KbsSplashFragment__update_pin);
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
}
private void setUpRegLockDisabled() {
title.setText(R.string.KbsSplashFragment__introducing_pins);
description.setText(R.string.KbsSplashFragment__pins_add_another_level_of_security_to_your_account);
primaryAction.setText(R.string.KbsSplashFragment__create_your_pin);
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
}
private void onCreatePin() {
Navigation.findNavController(requireView()).navigate(KbsSplashFragmentDirections.actionCreateKbsPin());
}
private void onLearnMore() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.KbsSplashFragment__learn_more_link)));
startActivity(intent);
}
}

View File

@@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public final class PinUtil {
private PinUtil() {}
public static boolean userHasPin(@NonNull Context context) {
return TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
}
}