Redesign FTUX to use Material Design 3.

This commit is contained in:
Nicholas
2023-01-23 16:30:57 -05:00
committed by Greyson Parrelli
parent 0303467c91
commit 150bbf181d
25 changed files with 842 additions and 918 deletions

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.components.registration
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.button.MaterialButton
import org.thoughtcrime.securesms.R
import java.util.concurrent.TimeUnit
class ActionCountDownButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : MaterialButton(context, attrs, defStyle) {
private var countDownToTime: Long = 0
private var listener: Listener? = null
/**
* Starts a count down to the specified {@param time}.
*/
fun startCountDownTo(time: Long) {
if (time > 0) {
countDownToTime = time
updateCountDown()
}
}
fun setCallEnabled() {
setText(R.string.RegistrationActivity_call)
isEnabled = true
alpha = 1.0f
}
private fun updateCountDown() {
val remainingMillis = countDownToTime - System.currentTimeMillis()
if (remainingMillis > 0) {
isEnabled = false
alpha = 0.5f
val totalRemainingSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingMillis).toInt()
val minutesRemaining = totalRemainingSeconds / 60
val secondsRemaining = totalRemainingSeconds % 60
text = resources.getString(R.string.RegistrationActivity_call_me_instead_available_in, minutesRemaining, secondsRemaining)
listener?.onRemaining(this, totalRemainingSeconds)
postDelayed({ updateCountDown() }, 250)
} else {
setCallEnabled()
}
}
fun setListener(listener: Listener?) {
this.listener = listener
}
interface Listener {
fun onRemaining(view: ActionCountDownButton, secondsRemaining: Int)
}
}

View File

@@ -1,79 +0,0 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.button.MaterialButton;
import org.thoughtcrime.securesms.R;
import java.util.concurrent.TimeUnit;
public class CallMeCountDownView extends MaterialButton {
private long countDownToTime;
@Nullable
private Listener listener;
public CallMeCountDownView(Context context) {
super(context);
}
public CallMeCountDownView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CallMeCountDownView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* Starts a count down to the specified {@param time}.
*/
public void startCountDownTo(long time) {
if (time > 0) {
this.countDownToTime = time;
updateCountDown();
}
}
public void setCallEnabled() {
setText(R.string.RegistrationActivity_call);
setEnabled(true);
setAlpha(1.0f);
}
private void updateCountDown() {
final long remainingMillis = countDownToTime - System.currentTimeMillis();
if (remainingMillis > 0) {
setEnabled(false);
setAlpha(0.5f);
int totalRemainingSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(remainingMillis);
int minutesRemaining = totalRemainingSeconds / 60;
int secondsRemaining = totalRemainingSeconds % 60;
setText(getResources().getString(R.string.RegistrationActivity_call_me_instead_available_in, minutesRemaining, secondsRemaining));
if (listener != null) {
listener.onRemaining(this, totalRemainingSeconds);
}
postDelayed(this::updateCountDown, 250);
} else {
setCallEnabled();
}
}
public void setListener(@Nullable Listener listener) {
this.listener = listener;
}
public interface Listener {
void onRemaining(@NonNull CallMeCountDownView view, int secondsRemaining);
}
}

View File

@@ -1,139 +0,0 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
import java.util.List;
public final class VerificationCodeView extends FrameLayout {
private final List<TextView> codes = new ArrayList<>(6);
private final List<View> containers = new ArrayList<>(6);
private OnCodeEnteredListener listener;
private int index;
public VerificationCodeView(Context context) {
super(context);
initialize(context);
}
public VerificationCodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
public VerificationCodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public VerificationCodeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context);
}
private void initialize(@NonNull Context context) {
inflate(context, R.layout.verification_code_view, this);
codes.add(findViewById(R.id.code_zero));
codes.add(findViewById(R.id.code_one));
codes.add(findViewById(R.id.code_two));
codes.add(findViewById(R.id.code_three));
codes.add(findViewById(R.id.code_four));
codes.add(findViewById(R.id.code_five));
containers.add(findViewById(R.id.container_zero));
containers.add(findViewById(R.id.container_one));
containers.add(findViewById(R.id.container_two));
containers.add(findViewById(R.id.container_three));
containers.add(findViewById(R.id.container_four));
containers.add(findViewById(R.id.container_five));
}
@MainThread
public void setOnCompleteListener(OnCodeEnteredListener listener) {
this.listener = listener;
}
@MainThread
public void append(int value) {
if (index >= codes.size()) return;
setInactive(containers);
setActive(containers.get(index));
TextView codeView = codes.get(index++);
Animation translateIn = new TranslateAnimation(0, 0, codeView.getHeight(), 0);
translateIn.setInterpolator(new OvershootInterpolator());
translateIn.setDuration(500);
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setDuration(200);
AnimationSet animationSet = new AnimationSet(false);
animationSet.addAnimation(fadeIn);
animationSet.addAnimation(translateIn);
animationSet.reset();
animationSet.setStartTime(0);
codeView.setText(String.valueOf(value));
codeView.clearAnimation();
codeView.startAnimation(animationSet);
if (index == codes.size() && listener != null) {
listener.onCodeComplete(Stream.of(codes).map(TextView::getText).collect(Collectors.joining()));
}
}
@MainThread
public void delete() {
if (index <= 0) return;
codes.get(--index).setText("");
setInactive(containers);
setActive(containers.get(index));
}
@MainThread
public void clear() {
if (index != 0) {
Stream.of(codes).forEach(code -> code.setText(""));
index = 0;
}
setInactive(containers);
}
private static void setInactive(List<View> views) {
Stream.of(views).forEach(c -> c.setBackgroundResource(R.drawable.labeled_edit_text_background_inactive));
}
private static void setActive(@NonNull View container) {
container.setBackgroundResource(R.drawable.labeled_edit_text_background_active);
}
public interface OnCodeEnteredListener {
void onCodeComplete(@NonNull String code);
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.components.registration
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.google.android.material.textfield.TextInputLayout
import org.thoughtcrime.securesms.R
class VerificationCodeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) :
FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
private val containers: MutableList<TextInputLayout> = ArrayList(6)
private var listener: OnCodeEnteredListener? = null
private var index = 0
init {
inflate(context, R.layout.verification_code_view, this)
containers.add(findViewById(R.id.container_zero))
containers.add(findViewById(R.id.container_one))
containers.add(findViewById(R.id.container_two))
containers.add(findViewById(R.id.container_three))
containers.add(findViewById(R.id.container_four))
containers.add(findViewById(R.id.container_five))
}
fun setOnCompleteListener(listener: OnCodeEnteredListener?) {
this.listener = listener
}
fun append(digit: Int) {
if (index >= containers.size) return
containers[index++].editText?.setText(digit.toString())
if (index == containers.size) {
listener?.onCodeComplete(containers.joinToString("") { it.editText?.text.toString() })
}
}
fun delete() {
if (index <= 0) return
containers[--index].editText?.setText("")
}
fun clear() {
if (index != 0) {
containers.forEach { it.editText?.setText("") }
index = 0
}
}
interface OnCodeEnteredListener {
fun onCodeComplete(code: String)
}
}

View File

@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel.ContinueStatus
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController
import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController
import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -54,13 +54,13 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
oldNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_old_number_country_code)
oldNumber = view.findViewById(R.id.change_number_enter_phone_number_old_number_number)
val oldController = RegistrationNumberInputController(
val oldController = ChangeNumberInputController(
requireContext(),
oldNumberCountryCode,
oldNumber,
oldNumberCountrySpinner,
false,
object : RegistrationNumberInputController.Callbacks {
object : ChangeNumberInputController.Callbacks {
override fun onNumberFocused() {
scrollView.postDelayed({ scrollView.smoothScrollTo(0, oldNumber.bottom) }, 250)
}
@@ -91,13 +91,13 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
newNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_new_number_country_code)
newNumber = view.findViewById(R.id.change_number_enter_phone_number_new_number_number)
val newController = RegistrationNumberInputController(
val newController = ChangeNumberInputController(
requireContext(),
newNumberCountryCode,
newNumber,
newNumberCountrySpinner,
true,
object : RegistrationNumberInputController.Callbacks {
object : ChangeNumberInputController.Callbacks {
override fun onNumberFocused() {
scrollView.postDelayed({ scrollView.smoothScrollTo(0, newNumber.bottom) }, 250)
}

View File

@@ -17,10 +17,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.airbnb.lottie.LottieAnimationView;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
@@ -29,19 +25,18 @@ import org.thoughtcrime.securesms.pin.PinOptOutDialog;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends LoggingFragment {
public abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends LoggingFragment {
private TextView title;
private LearnMoreTextView description;
private EditText input;
private TextView label;
private TextView keyboardToggle;
private TextView confirm;
private LottieAnimationView lottieProgress;
private LottieAnimationView lottieEnd;
private ViewModel viewModel;
private TextView title;
private LearnMoreTextView description;
private EditText input;
private TextView label;
private TextView keyboardToggle;
private CircularProgressMaterialButton confirm;
private ViewModel viewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -78,10 +73,6 @@ abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.BaseKbsPinFragment__learn_more_url));
});
Toolbar toolbar = view.findViewById(R.id.kbs_pin_toolbar);
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(null);
initializeListeners();
}
@@ -137,13 +128,6 @@ abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends
return input;
}
protected LottieAnimationView getLottieProgress() {
return lottieProgress;
}
protected LottieAnimationView getLottieEnd() {
return lottieEnd;
}
protected TextView getLabel() {
return label;
@@ -153,7 +137,7 @@ abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends
return keyboardToggle;
}
protected TextView getConfirm() {
protected CircularProgressMaterialButton getConfirm() {
return confirm;
}
@@ -173,8 +157,6 @@ abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends
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();
}

View File

@@ -1,189 +0,0 @@
package org.thoughtcrime.securesms.lock.v2;
import android.animation.Animator;
import android.app.Activity;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.RawRes;
import androidx.appcompat.app.AlertDialog;
import androidx.autofill.HintConstants;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.ViewModelProvider;
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.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.SpanUtil;
import java.util.Objects;
public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewModel> {
private ConfirmKbsPinViewModel viewModel;
@Override
protected void initializeViewStates() {
ConfirmKbsPinFragmentArgs args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments());
if (args.getIsPinChange()) {
initializeViewStatesForPinChange();
} else {
initializeViewStatesForPinCreate();
}
ViewCompat.setAutofillHints(getInput(), HintConstants.AUTOFILL_HINT_NEW_PASSWORD);
}
@Override
protected ConfirmKbsPinViewModel initializeViewModel() {
ConfirmKbsPinFragmentArgs args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments());
KbsPin userEntry = Objects.requireNonNull(args.getUserEntry());
PinKeyboardType keyboard = args.getKeyboard();
ConfirmKbsPinRepository repository = new ConfirmKbsPinRepository();
ConfirmKbsPinViewModel.Factory factory = new ConfirmKbsPinViewModel.Factory(userEntry, keyboard, repository);
viewModel = new ViewModelProvider(this, factory).get(ConfirmKbsPinViewModel.class);
viewModel.getLabel().observe(getViewLifecycleOwner(), this::updateLabel);
viewModel.getSaveAnimation().observe(getViewLifecycleOwner(), this::updateSaveAnimation);
return viewModel;
}
private void initializeViewStatesForPinCreate() {
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
getKeyboardToggle().setVisibility(View.INVISIBLE);
getLabel().setText("");
getDescription().setLearnMoreVisible(false);
}
private void initializeViewStatesForPinChange() {
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
getDescription().setLearnMoreVisible(false);
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);
getInput().setEnabled(false);
break;
case RE_ENTER_PIN:
getLabel().setText(R.string.ConfirmKbsPinFragment__re_enter_your_pin);
break;
case PIN_DOES_NOT_MATCH:
getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red_500),
getString(R.string.ConfirmKbsPinFragment__pins_dont_match)));
getInput().getText().clear();
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();
RegistrationUtil.maybeMarkRegistrationComplete(requireContext());
StorageSyncHelper.scheduleSyncForDataChange();
}
});
break;
case FAILURE:
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_fail, new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
RegistrationUtil.maybeMarkRegistrationComplete(requireContext());
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 markMegaphoneSeenIfNecessary() {
ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.PINS_FOR_ALL);
}
}

View File

@@ -0,0 +1,123 @@
package org.thoughtcrime.securesms.lock.v2
import android.app.Activity
import android.content.DialogInterface
import android.view.View
import androidx.autofill.HintConstants
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinViewModel.SaveAnimation
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.SpanUtil
internal class ConfirmKbsPinFragment : BaseKbsPinFragment<ConfirmKbsPinViewModel>() {
override fun initializeViewStates() {
val args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments())
if (args.isPinChange) {
initializeViewStatesForPinChange()
} else {
initializeViewStatesForPinCreate()
}
ViewCompat.setAutofillHints(input, HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
override fun initializeViewModel(): ConfirmKbsPinViewModel {
val args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments())
val userEntry = args.userEntry!!
val keyboard = args.keyboard
val repository = ConfirmKbsPinRepository()
val factory = ConfirmKbsPinViewModel.Factory(userEntry, keyboard, repository)
val viewModel = ViewModelProvider(this, factory)[ConfirmKbsPinViewModel::class.java]
viewModel.label.observe(viewLifecycleOwner) { label: ConfirmKbsPinViewModel.LabelState -> updateLabel(label) }
viewModel.saveAnimation.observe(viewLifecycleOwner) { animation: SaveAnimation -> updateSaveAnimation(animation) }
return viewModel
}
private fun initializeViewStatesForPinCreate() {
title.setText(R.string.ConfirmKbsPinFragment__confirm_your_pin)
description.setText(R.string.ConfirmKbsPinFragment__re_enter_the_pin_you_just_created)
keyboardToggle.visibility = View.INVISIBLE
description.setLearnMoreVisible(false)
label.text = ""
confirm.isEnabled = true
}
private fun initializeViewStatesForPinChange() {
title.setText(R.string.ConfirmKbsPinFragment__confirm_your_pin)
description.setText(R.string.ConfirmKbsPinFragment__re_enter_the_pin_you_just_created)
description.setLearnMoreVisible(false)
keyboardToggle.visibility = View.INVISIBLE
label.text = ""
confirm.isEnabled = true
}
private fun updateLabel(labelState: ConfirmKbsPinViewModel.LabelState) {
when (labelState) {
ConfirmKbsPinViewModel.LabelState.EMPTY -> label.text = ""
ConfirmKbsPinViewModel.LabelState.CREATING_PIN -> {
label.setText(R.string.ConfirmKbsPinFragment__creating_pin)
input.isEnabled = false
}
ConfirmKbsPinViewModel.LabelState.RE_ENTER_PIN -> label.setText(R.string.ConfirmKbsPinFragment__re_enter_your_pin)
ConfirmKbsPinViewModel.LabelState.PIN_DOES_NOT_MATCH -> {
label.text = SpanUtil.color(
ContextCompat.getColor(requireContext(), R.color.red_500),
getString(R.string.ConfirmKbsPinFragment__pins_dont_match)
)
input.text.clear()
}
}
}
private fun updateSaveAnimation(animation: SaveAnimation) {
updateInputVisibility(animation)
when (animation) {
SaveAnimation.NONE -> confirm.cancelSpinning()
SaveAnimation.LOADING -> confirm.setSpinning()
SaveAnimation.SUCCESS -> {
confirm.cancelSpinning()
requireActivity().setResult(Activity.RESULT_OK)
closeNavGraphBranch()
RegistrationUtil.maybeMarkRegistrationComplete(requireContext())
StorageSyncHelper.scheduleSyncForDataChange()
}
SaveAnimation.FAILURE -> {
confirm.cancelSpinning()
RegistrationUtil.maybeMarkRegistrationComplete(requireContext())
displayFailedDialog()
}
}
}
private fun updateInputVisibility(saveAnimation: SaveAnimation) {
if (saveAnimation == SaveAnimation.NONE) {
input.visibility = View.VISIBLE
} else {
input.visibility = View.GONE
}
}
private fun displayFailedDialog() {
MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.ConfirmKbsPinFragment__pin_creation_failed)
.setMessage(R.string.ConfirmKbsPinFragment__your_pin_was_not_saved)
.setCancelable(false)
.setPositiveButton(R.string.ok) { d: DialogInterface, w: Int ->
d.dismiss()
markMegaphoneSeenIfNecessary()
requireActivity().setResult(Activity.RESULT_CANCELED)
closeNavGraphBranch()
}
.show()
}
private fun markMegaphoneSeenIfNecessary() {
ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.PINS_FOR_ALL)
}
}

View File

@@ -16,8 +16,8 @@ final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewMo
private final DefaultValueLiveData<KbsPin> userEntry = new DefaultValueLiveData<>(KbsPin.EMPTY);
private final DefaultValueLiveData<PinKeyboardType> keyboard = new DefaultValueLiveData<>(PinKeyboardType.NUMERIC);
private final DefaultValueLiveData<SaveAnimation> saveAnimation = new DefaultValueLiveData<>(SaveAnimation.NONE);
private final DefaultValueLiveData<Label> label = new DefaultValueLiveData<>(Label.RE_ENTER_PIN);
private final DefaultValueLiveData<SaveAnimation> saveAnimation = new DefaultValueLiveData<>(SaveAnimation.NONE);
private final DefaultValueLiveData<LabelState> label = new DefaultValueLiveData<>(LabelState.EMPTY);
private final KbsPin pinToConfirm;
@@ -35,29 +35,25 @@ final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewMo
return Transformations.distinctUntilChanged(saveAnimation);
}
LiveData<Label> getLabel() {
LiveData<LabelState> getLabel() {
return Transformations.distinctUntilChanged(label);
}
@Override
public void confirm() {
KbsPin userEntry = this.userEntry.getValue();
this.userEntry.setValue(KbsPin.EMPTY);
if (pinToConfirm.toString().equals(userEntry.toString())) {
this.label.setValue(Label.CREATING_PIN);
this.label.setValue(LabelState.CREATING_PIN);
this.saveAnimation.setValue(SaveAnimation.LOADING);
repository.setPin(pinToConfirm, this.keyboard.getValue(), this::handleResult);
} else {
this.label.setValue(Label.PIN_DOES_NOT_MATCH);
this.userEntry.setValue(KbsPin.EMPTY);
this.label.setValue(LabelState.PIN_DOES_NOT_MATCH);
}
}
void onLoadingAnimationComplete() {
this.label.setValue(Label.EMPTY);
}
@Override
public LiveData<KbsPin> getUserEntry() {
return userEntry;
@@ -91,7 +87,7 @@ final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewMo
}
}
enum Label {
enum LabelState {
RE_ENTER_PIN,
PIN_DOES_NOT_MATCH,
CREATING_PIN,

View File

@@ -1,112 +0,0 @@
package org.thoughtcrime.securesms.lock.v2;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.autofill.HintConstants;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
@Override
protected void initializeViewStates() {
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
if (args.getIsPinChange()) {
initializeViewStatesForPinChange(args.getIsForgotPin());
} else {
initializeViewStatesForPinCreate();
}
getLabel().setText(getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits));
getConfirm().setEnabled(false);
ViewCompat.setAutofillHints(getInput(), HintConstants.AUTOFILL_HINT_NEW_PASSWORD);
}
private void initializeViewStatesForPinChange(boolean isForgotPin) {
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
getDescription().setText(R.string.CreateKbsPinFragment__you_can_choose_a_new_pin_as_long_as_this_device_is_registered);
getDescription().setLearnMoreVisible(true);
}
private void initializeViewStatesForPinCreate() {
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
getDescription().setText(R.string.CreateKbsPinFragment__pins_keep_information_stored_with_signal_encrypted);
getDescription().setLearnMoreVisible(true);
}
@Override
protected CreateKbsPinViewModel initializeViewModel() {
CreateKbsPinViewModel viewModel = new ViewModelProvider(this).get(CreateKbsPinViewModel.class);
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard(), args.getIsPinChange()));
viewModel.getErrorEvents().observe(getViewLifecycleOwner(), e -> {
if (e == CreateKbsPinViewModel.PinErrorEvent.WEAK_PIN) {
getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red_500),
getString(R.string.CreateKbsPinFragment__choose_a_stronger_pin)));
shake(getInput(), () -> getInput().getText().clear());
} else {
throw new AssertionError("Unexpected PIN error!");
}
});
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> {
getLabel().setText(getLabelText(k));
getInput().getText().clear();
});
return viewModel;
}
private void onConfirmPin(@NonNull KbsPin userEntry, @NonNull PinKeyboardType keyboard, boolean isPinChange) {
CreateKbsPinFragmentDirections.ActionConfirmPin action = CreateKbsPinFragmentDirections.actionConfirmPin();
action.setUserEntry(userEntry);
action.setKeyboard(keyboard);
action.setIsPinChange(isPinChange);
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), action);
}
private String getLabelText(@NonNull PinKeyboardType keyboard) {
if (keyboard == PinKeyboardType.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_PIN_LENGTH, KbsConstants.MINIMUM_PIN_LENGTH);
}
private static void shake(@NonNull EditText view, @NonNull Runnable afterwards) {
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);
shake.setDuration(50);
shake.setRepeatCount(7);
shake.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
afterwards.run();
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
view.startAnimation(shake);
}
}

View File

@@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.lock.v2
import android.view.animation.Animation
import android.view.animation.TranslateAnimation
import android.widget.EditText
import androidx.annotation.PluralsRes
import androidx.autofill.HintConstants
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinViewModel.NavigationEvent
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinViewModel.PinErrorEvent
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class CreateKbsPinFragment : BaseKbsPinFragment<CreateKbsPinViewModel?>() {
override fun initializeViewStates() {
val args = CreateKbsPinFragmentArgs.fromBundle(requireArguments())
if (args.isPinChange) {
initializeViewStatesForPinChange(args.isForgotPin)
} else {
initializeViewStatesForPinCreate()
}
label.text = getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits)
confirm.isEnabled = false
ViewCompat.setAutofillHints(input, HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
private fun initializeViewStatesForPinChange(isForgotPin: Boolean) {
title.setText(R.string.CreateKbsPinFragment__create_a_new_pin)
description.setText(R.string.CreateKbsPinFragment__you_can_choose_a_new_pin_as_long_as_this_device_is_registered)
description.setLearnMoreVisible(true)
}
private fun initializeViewStatesForPinCreate() {
title.setText(R.string.CreateKbsPinFragment__create_your_pin)
description.setText(R.string.CreateKbsPinFragment__pins_can_help_you_restore_your_account)
description.setLearnMoreVisible(true)
}
override fun initializeViewModel(): CreateKbsPinViewModel {
val viewModel = ViewModelProvider(this)[CreateKbsPinViewModel::class.java]
val args = CreateKbsPinFragmentArgs.fromBundle(requireArguments())
viewModel.navigationEvents.observe(viewLifecycleOwner) { e: NavigationEvent -> onConfirmPin(e.userEntry, e.keyboard, args.isPinChange) }
viewModel.errorEvents.observe(viewLifecycleOwner) { e: PinErrorEvent ->
if (e == PinErrorEvent.WEAK_PIN) {
label.text = SpanUtil.color(
ContextCompat.getColor(requireContext(), R.color.red_500),
getString(R.string.CreateKbsPinFragment__choose_a_stronger_pin)
)
shake(input) { input.text.clear() }
} else {
throw AssertionError("Unexpected PIN error!")
}
}
viewModel.keyboard.observe(viewLifecycleOwner) { k: PinKeyboardType ->
label.text = getLabelText(k)
input.text.clear()
}
return viewModel
}
private fun onConfirmPin(userEntry: KbsPin, keyboard: PinKeyboardType, isPinChange: Boolean) {
val action = CreateKbsPinFragmentDirections.actionConfirmPin()
action.userEntry = userEntry
action.keyboard = keyboard
action.isPinChange = isPinChange
findNavController(requireView()).safeNavigate(action)
}
private fun getLabelText(keyboard: PinKeyboardType): String {
return if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_characters)
} else {
getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits)
}
}
private fun getPinLengthRestrictionText(@PluralsRes plurals: Int): String {
return resources.getQuantityString(plurals, KbsConstants.MINIMUM_PIN_LENGTH, KbsConstants.MINIMUM_PIN_LENGTH)
}
companion object {
private fun shake(view: EditText, afterwards: Runnable) {
val shake = TranslateAnimation(0F, 30F, 0F, 0F)
shake.duration = 50
shake.repeatCount = 7
shake.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
afterwards.run()
}
override fun onAnimationRepeat(animation: Animation) {}
})
view.startAnimation(shake)
}
}
}

View File

@@ -21,7 +21,7 @@ import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.registration.CallMeCountDownView;
import org.thoughtcrime.securesms.components.registration.ActionCountDownButton;
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
@@ -55,13 +55,11 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
private static final String TAG = Log.tag(BaseEnterSmsCodeFragment.class);
private ScrollView scrollView;
private TextView header;
private TextView subheader;
private VerificationCodeView verificationCodeView;
private VerificationPinKeyboard keyboard;
private CallMeCountDownView callMeCountDown;
private ActionCountDownButton callMeCountDown;
private View wrongNumber;
private View noCodeReceivedHelp;
private View serviceWarning;
private boolean autoCompleting;
private ViewModel viewModel;
@@ -80,13 +78,11 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
scrollView = view.findViewById(R.id.scroll_view);
header = view.findViewById(R.id.verify_header);
subheader = view.findViewById(R.id.verification_subheader);
verificationCodeView = view.findViewById(R.id.code);
keyboard = view.findViewById(R.id.keyboard);
callMeCountDown = view.findViewById(R.id.call_me_count_down);
wrongNumber = view.findViewById(R.id.wrong_number);
noCodeReceivedHelp = view.findViewById(R.id.no_code);
serviceWarning = view.findViewById(R.id.cell_service_warning);
new SignalStrengthPhoneStateListener(this, this);
@@ -106,14 +102,12 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
}
});
noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport());
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = getViewModel();
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
if (attempts >= 3) {
noCodeReceivedHelp.setVisibility(View.VISIBLE);
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000);
// TODO Add bottom sheet for help
}
});
@@ -330,7 +324,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
public void onResume() {
super.onResume();
header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
subheader.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
}
@@ -348,40 +342,11 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
@Override
public void onNoCellSignalPresent() {
if (serviceWarning.getVisibility() == View.VISIBLE) {
return;
}
serviceWarning.setVisibility(View.VISIBLE);
serviceWarning.animate()
.alpha(1)
.setListener(null)
.start();
scrollView.postDelayed(() -> {
if (serviceWarning.getVisibility() == View.VISIBLE) {
scrollView.smoothScrollTo(0, serviceWarning.getBottom());
}
}, 1000);
// TODO animate in bottom sheet
}
@Override
public void onCellSignalPresent() {
if (serviceWarning.getVisibility() != View.VISIBLE) {
return;
}
serviceWarning.animate()
.alpha(0)
.setListener(new Animator.AnimatorListener() {
@Override public void onAnimationEnd(Animator animation) {
serviceWarning.setVisibility(View.GONE);
}
@Override public void onAnimationStart(Animator animation) {}
@Override public void onAnimationCancel(Animator animation) {}
@Override public void onAnimationRepeat(Animator animation) {}
})
.start();
// TODO animate away bottom sheet
}
}

View File

@@ -11,9 +11,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
@@ -27,6 +28,7 @@ import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.tasks.Task;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
@@ -35,7 +37,6 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController;
@@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -64,10 +66,9 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
private LabeledEditText countryCode;
private LabeledEditText number;
private TextInputLayout countryCode;
private TextInputLayout number;
private CircularProgressMaterialButton register;
private Spinner countrySpinner;
private View cancel;
private ScrollView scrollView;
private RegistrationViewModel viewModel;
@@ -91,20 +92,16 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
countryCode = view.findViewById(R.id.country_code);
number = view.findViewById(R.id.number);
countrySpinner = view.findViewById(R.id.country_spinner);
cancel = view.findViewById(R.id.cancel_button);
scrollView = view.findViewById(R.id.scroll_view);
register = view.findViewById(R.id.registerButton);
countryCode = view.findViewById(R.id.country_code);
number = view.findViewById(R.id.number);
cancel = view.findViewById(R.id.cancel_button);
scrollView = view.findViewById(R.id.scroll_view);
register = view.findViewById(R.id.registerButton);
RegistrationNumberInputController controller = new RegistrationNumberInputController(requireContext(),
countryCode,
number,
countrySpinner,
true,
this);
this,
Objects.requireNonNull(number.getEditText()),
countryCode);
register.setOnClickListener(v -> handleRegister(requireContext()));
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
@@ -125,7 +122,10 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
Toolbar toolbar = view.findViewById(R.id.toolbar);
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(null);
final ActionBar supportActionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setTitle(null);
}
}
@Override
@@ -144,13 +144,13 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
}
private void handleRegister(@NonNull Context context) {
if (TextUtils.isEmpty(countryCode.getText())) {
if (TextUtils.isEmpty(countryCode.getEditText().getText())) {
showErrorDialog(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code));
return;
}
if (TextUtils.isEmpty(this.number.getText())) {
showErrorDialog(context, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number));
if (TextUtils.isEmpty(this.number.getEditText().getText())) {
showErrorDialog(context, getString(R.string.RegistrationActivity_please_enter_a_valid_phone_number_to_register));
return;
}
@@ -184,8 +184,8 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
disableAllEntries();
if (fcmSupported) {
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
AtomicBoolean handled = new AtomicBoolean(false);
Debouncer debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(5));
@@ -224,14 +224,12 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
private void disableAllEntries() {
countryCode.setEnabled(false);
number.setEnabled(false);
countrySpinner.setEnabled(false);
cancel.setVisibility(View.GONE);
}
private void enableAllEntries() {
countryCode.setEnabled(true);
number.setEnabled(true);
countrySpinner.setEnabled(true);
if (viewModel.isReregister()) {
cancel.setVisibility(View.VISIBLE);
}
@@ -283,22 +281,12 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250);
}
@Override
public void onNumberInputNext(@NonNull View view) {
// Intentionally left blank
}
@Override
public void onNumberInputDone(@NonNull View view) {
ViewUtil.hideKeyboard(requireContext(), view);
handleRegister(requireContext());
}
@Override
public void onPickCountry(@NonNull View view) {
SafeNavigation.safeNavigate(Navigation.findNavController(view), R.id.action_pickCountry);
}
@Override
public void setNationalNumber(@NonNull String number) {
viewModel.setNationalNumber(number);
@@ -325,8 +313,8 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
d.dismiss();
})
.setPositiveButton(R.string.yes, (d, i) -> {
countryCode.setText(String.valueOf(phoneNumber.getCountryCode()));
number.setText(String.valueOf(phoneNumber.getNationalNumber()));
countryCode.getEditText().setText(String.valueOf(phoneNumber.getCountryCode()));
number.getEditText().setText(String.valueOf(phoneNumber.getNationalNumber()));
requestVerificationCode(mode);
d.dismiss();
})
@@ -357,9 +345,9 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
R.string.RegistrationActivity_a_verification_code_will_be_sent_to,
e164number,
() -> {
ViewUtil.hideKeyboard(context, number.getInput());
ViewUtil.hideKeyboard(context, number.getEditText());
onConfirmed.run();
},
() -> number.focusAndMoveCursorToEndAndOpenKeyboard());
() -> ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(this.number.getEditText()));
}
}

View File

@@ -24,9 +24,9 @@ import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
/**
* Handle the logic and formatting of phone number input for registration/change number flows.
* Handle the logic and formatting of phone number input specifically for change number flows.
*/
public final class RegistrationNumberInputController {
public final class ChangeNumberInputController {
private final Context context;
private final LabeledEditText countryCode;
@@ -38,12 +38,12 @@ public final class RegistrationNumberInputController {
private AsYouTypeFormatter countryFormatter;
private boolean isUpdating = true;
public RegistrationNumberInputController(@NonNull Context context,
@NonNull LabeledEditText countryCode,
@NonNull LabeledEditText number,
@NonNull Spinner countrySpinner,
boolean lastInput,
@NonNull Callbacks callbacks)
public ChangeNumberInputController(@NonNull Context context,
@NonNull LabeledEditText countryCode,
@NonNull LabeledEditText number,
@NonNull Spinner countrySpinner,
boolean lastInput,
@NonNull Callbacks callbacks)
{
this.context = context;
this.countryCode = countryCode;

View File

@@ -0,0 +1,155 @@
package org.thoughtcrime.securesms.registration.util
import android.content.Context
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.View
import android.view.View.OnFocusChangeListener
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.TextView
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputLayout
import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
/**
* Handle the logic and formatting of phone number input specifically for registration number the flow.
*/
class RegistrationNumberInputController(
val context: Context,
val callbacks: Callbacks,
private val phoneNumberInputLayout: EditText,
countryCodeInputLayout: TextInputLayout
) {
private val spinnerView: MaterialAutoCompleteTextView = countryCodeInputLayout.editText as MaterialAutoCompleteTextView
private val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
private val spinnerAdapter: ArrayAdapter<CountryPrefix> = ArrayAdapter<CountryPrefix>(context, R.layout.registration_country_code_dropdown_item, supportedCountryPrefixes)
private var countryFormatter: AsYouTypeFormatter? = null
private var isUpdating = true
init {
setUpNumberInput()
spinnerView.setAdapter(spinnerAdapter)
spinnerView.addTextChangedListener(CountryCodeEntryListener())
}
private fun advanceToPhoneNumberInput() {
if (!isUpdating) {
phoneNumberInputLayout.requestFocus()
}
val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0
phoneNumberInputLayout.setSelection(numberLength, numberLength)
}
private fun setUpNumberInput() {
phoneNumberInputLayout.addTextChangedListener(NumberChangedListener())
phoneNumberInputLayout.onFocusChangeListener = OnFocusChangeListener { v: View?, hasFocus: Boolean ->
if (hasFocus) {
callbacks.onNumberFocused()
}
}
phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE
phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
callbacks.onNumberInputDone(v!!)
return@setOnEditorActionListener true
}
false
}
}
fun updateNumber(numberViewState: NumberViewState) {
val countryCode = numberViewState.countryCode
isUpdating = true
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode)
setCountryFormatter(regionCode)
isUpdating = false
}
private fun setCountryFormatter(regionCode: String?) {
val util = PhoneNumberUtil.getInstance()
countryFormatter = if (regionCode != null) util.getAsYouTypeFormatter(regionCode) else null
reformatText(phoneNumberInputLayout.text)
}
private fun reformatText(editable: Editable): String? {
if (TextUtils.isEmpty(editable)) {
return null
}
val countryFormatter: AsYouTypeFormatter = countryFormatter ?: return null
countryFormatter.clear()
var formattedNumber: String? = null
val justDigits = StringBuilder()
for (character in editable) {
if (Character.isDigit(character)) {
formattedNumber = countryFormatter.inputDigit(character)
justDigits.append(character)
}
}
if (formattedNumber != null && editable.toString() != formattedNumber) {
editable.replace(0, editable.length, formattedNumber)
}
return if (justDigits.isEmpty()) {
null
} else justDigits.toString()
}
inner class NumberChangedListener : TextWatcher {
override fun afterTextChanged(s: Editable) {
val number: String = reformatText(s) ?: return
if (!isUpdating) {
callbacks.setNationalNumber(number)
}
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
}
inner class CountryCodeEntryListener : TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (s.isNullOrEmpty()) {
return
}
if (s[0] != '+') {
s.insert(0, "+")
}
supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let {
setCountryFormatter(it.regionCode)
callbacks.setCountry(it.digits)
advanceToPhoneNumberInput()
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}
interface Callbacks {
fun onNumberFocused()
fun onNumberInputDone(view: View)
fun setNationalNumber(number: String)
fun setCountry(countryCode: Int)
}
}
data class CountryPrefix(val digits: Int, val regionCode: String) {
override fun toString(): String {
return "+$digits"
}
}