mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-24 03:35:58 +00:00
Redesign FTUX to use Material Design 3.
This commit is contained in:
committed by
Greyson Parrelli
parent
0303467c91
commit
150bbf181d
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user