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

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
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.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
public class AccountLockedFragment extends Fragment {
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.account_locked_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
AccountLockedFragmentArgs args = AccountLockedFragmentArgs.fromBundle(requireArguments());
TextView description = view.findViewById(R.id.account_locked_description);
description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, args.getTimeRemaining()));
view.findViewById(R.id.account_locked_next).setOnClickListener(this::onNextClicked);
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(this::onLearnMoreClicked);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onNext();
}
});
}
private void onNextClicked(@NonNull View unused) {
onNext();
}
private void onLearnMoreClicked(@NonNull View unused) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
startActivity(intent);
}
private void onNext() {
requireActivity().finish();
}
}

View File

@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
@@ -126,8 +127,13 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
if (FeatureFlags.pinsForAll()) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
} else {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
}
}
});
}

View File

@@ -0,0 +1,221 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
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 android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.util.concurrent.TimeUnit;
public final class KbsLockFragment extends BaseRegistrationFragment {
private EditText pinEntry;
private CircularProgressButton pinButton;
private TextView errorLabel;
private TextView keyboardToggle;
private long timeRemaining;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.kbs_lock_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
View pinForgotButton = view.findViewById(R.id.kbs_lock_forgot_pin);
timeRemaining = KbsLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining();
pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining));
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
handlePinEntry();
return true;
}
return false;
});
pinButton.setOnClickListener((v) -> {
hideKeyboard(requireContext(), pinEntry);
handlePinEntry();
});
keyboardToggle.setOnClickListener((v) -> {
KbsKeyboardType keyboardType = getPinEntryKeyboardType();
updateKeyboard(keyboardType);
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
});
RegistrationViewModel model = getModel();
model.getTokenResponseCredentialsPair().observe(getViewLifecycleOwner(), pair -> {
if (pair.first().getTries() == 0) {
lockAccount();
}
});
model.onRegistrationLockFragmentCreate();
}
private KbsKeyboardType getPinEntryKeyboardType() {
boolean isNumeric = (pinEntry.getImeOptions() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
return isNumeric ? KbsKeyboardType.NUMERIC : KbsKeyboardType.ALPHA_NUMERIC;
}
private void handlePinEntry() {
final String pin = pinEntry.getText().toString();
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
return;
}
RegistrationViewModel model = getModel();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
String storageCredentials = model.getBasicStorageCredentials();
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
setSpinning(pinButton);
registrationService.verifyAccount(requireActivity(),
model.getFcmToken(),
model.getTextCodeEntered(),
pin, storageCredentials, tokenResponse,
new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
cancelSpinning(pinButton);
SignalStore.kbsValues().setKeyboardType(getPinEntryKeyboardType());
Navigation.findNavController(requireView()).navigate(KbsLockFragmentDirections.actionSuccessfulRegistration());
}
@Override
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
model.setStorageCredentials(storageCredentials);
cancelSpinning(pinButton);
pinEntry.setText("");
errorLabel.setText(R.string.KbsLockFragment__incorrect_pin);
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
cancelSpinning(pinButton);
model.setKeyBackupCurrentToken(tokenResponse);
int triesRemaining = tokenResponse.getTries();
if (triesRemaining == 0) {
lockAccount();
return;
}
if (triesRemaining == 3) {
long daysRemaining = getLockoutDays(timeRemaining);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.KbsLockFragment__incorrect_pin)
.setMessage(getString(R.string.KbsLockFragment__you_have_d_attempts_remaining, triesRemaining, daysRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.show();
}
if (triesRemaining > 5) {
errorLabel.setText(R.string.KbsLockFragment__incorrect_pin_try_again);
} else {
errorLabel.setText(getString(R.string.KbsLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining));
}
}
@Override
public void onTooManyAttempts() {
cancelSpinning(pinButton);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
}
@Override
public void onError() {
cancelSpinning(pinButton);
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
});
}
private void handleForgottenPin(long timeRemainingMs) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.KbsLockFragment__forgot_your_pin)
.setMessage(getString(R.string.KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, getLockoutDays(timeRemainingMs)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
private long getLockoutDays(long timeRemainingMs) {
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
}
private void lockAccount() {
KbsLockFragmentDirections.ActionAccountLocked action = KbsLockFragmentDirections.actionAccountLocked(timeRemaining);
Navigation.findNavController(requireView()).navigate(action);
}
private void updateKeyboard(@NonNull KbsKeyboardType keyboard) {
boolean isAlphaNumeric = keyboard == KbsKeyboardType.ALPHA_NUMERIC;
pinEntry.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.KbsLockFragment__enter_alphanumeric_pin;
} else {
return R.string.KbsLockFragment__enter_numeric_pin;
}
}
}

View File

@@ -14,7 +14,11 @@ import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.util.FeatureFlags;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@@ -30,12 +34,19 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
FragmentActivity activity = requireActivity();
if (!isReregister()) {
Intent setProfileNameIntent = getRoutedIntent(activity, EditProfileActivity.class, new Intent(activity, MainActivity.class));
final Intent main = new Intent(activity, MainActivity.class);
final Intent next = getRoutedIntent(activity, EditProfileActivity.class, main);
setProfileNameIntent.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
next.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
activity.startActivity(setProfileNameIntent);
Context context = requireContext();
if (FeatureFlags.pinsForAll() && !PinUtil.userHasPin(context)) {
activity.startActivity(getRoutedIntent(activity, CreateKbsPinActivity.class, next));
} else {
activity.startActivity(next);
}
}
activity.finish();