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

@@ -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"
}
}