Signal-Android/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java

323 lines
12 KiB
Java

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<Boolean>() {
@Override
public void onSuccess(Boolean result) {
handleSuccessfulRegistration();
}
});
}
@Override
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
model.setLockedTimeRemaining(timeRemaining);
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@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<Boolean>() {
@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<Boolean>() {
@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<Boolean>() {
@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<Integer> 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<Integer> convertVerificationCodeToDigits(@Nullable String code) {
if (code == null || code.length() != 6) {
return Collections.emptyList();
}
List<Integer> 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);
}
}