package org.thoughtcrime.securesms.registration.fragments; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; import android.widget.TextView; 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 org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.registration.CallMeCountDownView; import org.thoughtcrime.securesms.components.registration.VerificationCodeView; import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.pin.PinRestoreRepository; import org.thoughtcrime.securesms.registration.ReceivedSmsEvent; import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest; import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; import org.thoughtcrime.securesms.registration.service.RegistrationService; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.SupportEmailUtil; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; public final class EnterCodeFragment extends BaseRegistrationFragment { private static final String TAG = Log.tag(EnterCodeFragment.class); private ScrollView scrollView; private TextView header; private VerificationCodeView verificationCodeView; private VerificationPinKeyboard keyboard; private CallMeCountDownView callMeCountDown; private View wrongNumber; private View noCodeReceivedHelp; private boolean autoCompleting; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_registration_enter_code, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header)); scrollView = view.findViewById(R.id.scroll_view); header = view.findViewById(R.id.verify_header); 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); connectKeyboard(verificationCodeView, keyboard); setOnCodeFullyEnteredListener(verificationCodeView); wrongNumber.setOnClickListener(v -> Navigation.findNavController(view).navigate(EnterCodeFragmentDirections.actionWrongNumber())); callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest()); callMeCountDown.setListener((v, remaining) -> { if (remaining <= 30) { scrollView.smoothScrollTo(0, v.getBottom()); callMeCountDown.setListener(null); } }); noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport()); RegistrationViewModel model = getModel(); model.getSuccessfulCodeRequestAttempts().observe(this, (attempts) -> { if (attempts >= 3) { noCodeReceivedHelp.setVisibility(View.VISIBLE); scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000); } }); model.onStartEnterCode(); } private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) { verificationCodeView.setOnCompleteListener(code -> { RegistrationViewModel model = getModel(); model.onVerificationCodeEntered(code); callMeCountDown.setVisibility(View.INVISIBLE); wrongNumber.setVisibility(View.INVISIBLE); keyboard.displayProgress(); RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null, new CodeVerificationRequest.VerifyCallback() { @Override public void onSuccessfulRegistration() { keyboard.displaySuccess().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { handleSuccessfulRegistration(); } }); } @Override public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) { model.setLockedTimeRemaining(timeRemaining); keyboard.displayLocked().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean r) { Navigation.findNavController(requireView()) .navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, true)); } }); } @Override public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull PinRestoreRepository.TokenData tokenData, @NonNull String kbsStorageCredentials) { model.setLockedTimeRemaining(timeRemaining); model.setKeyBackupTokenData(tokenData); keyboard.displayLocked().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean r) { Navigation.findNavController(requireView()) .navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false)); } }); } @Override public void onIncorrectKbsRegistrationLockPin(@NonNull PinRestoreRepository.TokenData tokenData) { throw new AssertionError("Unexpected, user has made no pin guesses"); } @Override public void onRateLimited() { keyboard.displayFailure().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean r) { new AlertDialog.Builder(requireContext()) .setTitle(R.string.RegistrationActivity_too_many_attempts) .setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) .setPositiveButton(android.R.string.ok, (dialog, which) -> { callMeCountDown.setVisibility(View.VISIBLE); wrongNumber.setVisibility(View.VISIBLE); verificationCodeView.clear(); keyboard.displayKeyboard(); }) .show(); } }); } @Override public void onKbsAccountLocked(@Nullable Long timeRemaining) { if (timeRemaining != null) { model.setLockedTimeRemaining(timeRemaining); } Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked()); } @Override public void onError() { Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show(); keyboard.displayFailure().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { callMeCountDown.setVisibility(View.VISIBLE); wrongNumber.setVisibility(View.VISIBLE); verificationCodeView.clear(); keyboard.displayKeyboard(); } }); } }); }); } private void handleSuccessfulRegistration() { Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration()); } @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); } @Subscribe(threadMode = ThreadMode.MAIN) public void onVerificationCodeReceived(@NonNull ReceivedSmsEvent event) { verificationCodeView.clear(); List parsedCode = convertVerificationCodeToDigits(event.getCode()); autoCompleting = true; final int size = parsedCode.size(); for (int i = 0; i < size; i++) { final int index = i; verificationCodeView.postDelayed(() -> { verificationCodeView.append(parsedCode.get(index)); if (index == size - 1) { autoCompleting = false; } }, i * 200); } } private static List convertVerificationCodeToDigits(@Nullable String code) { if (code == null || code.length() != 6) { return Collections.emptyList(); } List result = new ArrayList<>(code.length()); try { for (int i = 0; i < code.length(); i++) { result.add(Integer.parseInt(Character.toString(code.charAt(i)))); } } catch (NumberFormatException e) { Log.w(TAG, "Failed to convert code into digits.", e); return Collections.emptyList(); } return result; } private void handlePhoneCallRequest() { RegistrationViewModel model = getModel(); String captcha = model.getCaptchaToken(); model.clearCaptchaResponse(); model.onCallRequested(); NavController navController = Navigation.findNavController(callMeCountDown); RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); registrationService.requestVerificationCode(requireActivity(), RegistrationCodeRequest.Mode.PHONE_CALL, captcha, new RegistrationCodeRequest.SmsVerificationCodeCallback() { @Override public void onNeedCaptcha() { navController.navigate(EnterCodeFragmentDirections.actionRequestCaptcha()); } @Override public void requestSent(@Nullable String fcmToken) { model.setFcmToken(fcmToken); model.markASuccessfulAttempt(); } @Override public void onRateLimited() { Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show(); } @Override public void onError() { Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); } }); } private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) { keyboard.setOnKeyPressListener(key -> { if (!autoCompleting) { if (key >= 0) { verificationCodeView.append(key); } else { verificationCodeView.delete(); } } }); } @Override public void onResume() { super.onResume(); RegistrationViewModel model = getModel(); model.getLiveNumber().observe(this, (s) -> header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, s.getFullFormattedNumber()))); model.getCanCallAtTime().observe(this, callAtTime -> callMeCountDown.startCountDownTo(callAtTime)); } private void sendEmailToSupport() { String body = SupportEmailUtil.generateSupportEmailBody(requireContext(), getString(R.string.RegistrationActivity_code_support_subject), null, null); CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), getString(R.string.RegistrationActivity_code_support_subject), body); } }