package org.thoughtcrime.securesms.registration.fragments; import android.content.Context; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.navigation.NavController; import androidx.navigation.Navigation; import com.dd.CircularProgressButton; import com.google.android.gms.auth.api.phone.SmsRetriever; import com.google.android.gms.auth.api.phone.SmsRetrieverClient; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.tasks.Task; import com.google.i18n.phonenumbers.AsYouTypeFormatter; import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.LabeledEditText; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; import org.thoughtcrime.securesms.registration.service.RegistrationService; import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.PlayServicesUtil; public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { private static final String TAG = Log.tag(EnterPhoneNumberFragment.class); private LabeledEditText countryCode; private LabeledEditText number; private ArrayAdapter countrySpinnerAdapter; private AsYouTypeFormatter countryFormatter; private CircularProgressButton register; private Spinner countrySpinner; private View cancel; private ScrollView scrollView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); 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); initializeSpinner(countrySpinner); setUpNumberInput(); register.setOnClickListener(v -> handleRegister(requireContext())); if (isReregister()) { cancel.setVisibility(View.VISIBLE); cancel.setOnClickListener(v -> Navigation.findNavController(v).navigateUp()); } else { cancel.setVisibility(View.GONE); } RegistrationViewModel model = getModel(); NumberViewState number = model.getNumber(); initNumber(number); countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener()); if (model.hasCaptchaToken()) { handleRegister(requireContext()); } countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT); } private void setUpNumberInput() { EditText numberInput = number.getInput(); numberInput.addTextChangedListener(new NumberChangedListener()); number.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) { scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250); } }); numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE); numberInput.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_DONE) { hideKeyboard(requireContext(), v); handleRegister(requireContext()); return true; } return false; }); } private void handleRegister(@NonNull Context context) { if (TextUtils.isEmpty(countryCode.getText())) { Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show(); return; } if (TextUtils.isEmpty(this.number.getText())) { Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number), Toast.LENGTH_LONG).show(); return; } final NumberViewState number = getModel().getNumber(); final String e164number = number.getE164Number(); if (!number.isValid()) { Dialogs.showAlertDialog(context, getString(R.string.RegistrationActivity_invalid_number), String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number)); return; } PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context); if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) { handleRequestVerification(context, e164number, true); } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) { handlePromptForNoPlayServices(context, e164number); } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) { GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show(); } else { Dialogs.showAlertDialog(context, getString(R.string.RegistrationActivity_play_services_error), getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)); } } private void handleRequestVerification(@NonNull Context context, @NonNull String e164number, boolean fcmSupported) { setSpinning(register); disableAllEntries(); if (fcmSupported) { SmsRetrieverClient client = SmsRetriever.getClient(context); Task task = client.startSmsRetriever(); task.addOnSuccessListener(none -> { Log.i(TAG, "Successfully registered SMS listener."); requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_FCM_WITH_LISTENER); }); task.addOnFailureListener(e -> { Log.w(TAG, "Failed to register SMS listener.", e); requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_FCM_NO_LISTENER); }); } else { requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_NO_FCM); } } 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 (isReregister()) { cancel.setVisibility(View.VISIBLE); } } private void requestVerificationCode(String e164number, @NonNull RegistrationCodeRequest.Mode mode) { RegistrationViewModel model = getModel(); String captcha = model.getCaptchaToken(); model.clearCaptchaResponse(); NavController navController = Navigation.findNavController(register); RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret()); registrationService.requestVerificationCode(requireActivity(), mode, captcha, new RegistrationCodeRequest.SmsVerificationCodeCallback() { @Override public void onNeedCaptcha() { navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha()); cancelSpinning(register); enableAllEntries(); } @Override public void requestSent(@Nullable String fcmToken) { model.setFcmToken(fcmToken); model.markASuccessfulAttempt(); navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); cancelSpinning(register); enableAllEntries(); } @Override public void onError() { Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); cancelSpinning(register); enableAllEntries(); } }); } private void initializeSpinner(Spinner countrySpinner) { countrySpinnerAdapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item); countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); setCountryDisplay(getString(R.string.RegistrationActivity_select_your_country)); countrySpinner.setAdapter(countrySpinnerAdapter); countrySpinner.setOnTouchListener((view, event) -> { if (event.getAction() == MotionEvent.ACTION_UP) { pickCountry(view); } return true; }); countrySpinner.setOnKeyListener((view, keyCode, event) -> { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) { pickCountry(view); return true; } return false; }); } private void pickCountry(@NonNull View view) { Navigation.findNavController(view).navigate(R.id.action_pickCountry); } private void initNumber(@NonNull NumberViewState numberViewState) { int countryCode = numberViewState.getCountryCode(); long number = numberViewState.getNationalNumber(); String regionDisplayName = numberViewState.getCountryDisplayName(); this.countryCode.setText(String.valueOf(countryCode)); setCountryDisplay(regionDisplayName); String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode); setCountryFormatter(regionCode); if (number != 0) { this.number.setText(String.valueOf(number)); } } private void setCountryDisplay(String regionDisplayName) { countrySpinnerAdapter.clear(); if (regionDisplayName == null) { countrySpinnerAdapter.add(getString(R.string.RegistrationActivity_select_your_country)); } else { countrySpinnerAdapter.add(regionDisplayName); } } private class CountryCodeChangedListener implements TextWatcher { @Override public void afterTextChanged(Editable s) { if (TextUtils.isEmpty(s) || !TextUtils.isDigitsOnly(s)) { setCountryDisplay(null); countryFormatter = null; return; } int countryCode = Integer.parseInt(s.toString()); String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode); setCountryFormatter(regionCode); if (!TextUtils.isEmpty(regionCode) && !regionCode.equals("ZZ")) { number.requestFocus(); int numberLength = number.getText().length(); number.getInput().setSelection(numberLength, numberLength); } RegistrationViewModel model = getModel(); model.onCountrySelected(null, countryCode); setCountryDisplay(model.getNumber().getCountryDisplayName()); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } } private class NumberChangedListener implements TextWatcher { @Override public void afterTextChanged(Editable s) { String number = reformatText(s); if (number == null) return; RegistrationViewModel model = getModel(); model.setNationalNumber(Long.parseLong(number)); setCountryDisplay(model.getNumber().getCountryDisplayName()); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } } private String reformatText(Editable s) { if (countryFormatter == null) { return null; } if (TextUtils.isEmpty(s)) { return null; } countryFormatter.clear(); String number = s.toString().replaceAll("[^\\d.]", ""); String formattedNumber = null; for (int i = 0; i < number.length(); i++) { formattedNumber = countryFormatter.inputDigit(number.charAt(i)); } if (formattedNumber != null && !s.toString().equals(formattedNumber)) { s.replace(0, s.length(), formattedNumber); } return number; } private void setCountryFormatter(@Nullable String regionCode) { PhoneNumberUtil util = PhoneNumberUtil.getInstance(); countryFormatter = regionCode != null ? util.getAsYouTypeFormatter(regionCode) : null; reformatText(number.getText()); } private void handlePromptForNoPlayServices(@NonNull Context context, @NonNull String e164number) { new AlertDialog.Builder(context) .setTitle(R.string.RegistrationActivity_missing_google_play_services) .setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services) .setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, e164number, false)) .setNegativeButton(android.R.string.cancel, null) .show(); } }