Delete registration V1.

This commit is contained in:
Nicholas Tinsley
2024-06-25 10:01:27 -04:00
parent f11028529e
commit d7b5c6bff3
140 changed files with 1658 additions and 9190 deletions

View File

@@ -1,101 +0,0 @@
package org.thoughtcrime.securesms.registration;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public final class RegistrationNavigationActivity extends AppCompatActivity {
private static final String TAG = Log.tag(RegistrationNavigationActivity.class);
public static final String RE_REGISTRATION_EXTRA = "re_registration";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private SmsRetrieverReceiver smsRetrieverReceiver;
private RegistrationViewModel viewModel;
public static Intent newIntentForNewRegistration(@NonNull Context context, @Nullable Intent originalIntent) {
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
intent.putExtra(RE_REGISTRATION_EXTRA, false);
if (originalIntent != null) {
intent.setData(originalIntent.getData());
}
return intent;
}
public static Intent newIntentForReRegistration(@NonNull Context context) {
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
intent.putExtra(RE_REGISTRATION_EXTRA, true);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
dynamicTheme.onCreate(this);
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class);
setContentView(R.layout.activity_registration_navigation);
initializeChallengeListener();
if (getIntent() != null && getIntent().getData() != null) {
CommunicationActions.handlePotentialProxyLinkUrl(this, getIntent().getDataString());
}
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.getData() != null) {
CommunicationActions.handlePotentialProxyLinkUrl(this, intent.getDataString());
}
viewModel.setIsReregister(isReregister(intent));
}
@Override
protected void onDestroy() {
super.onDestroy();
shutdownChallengeListener();
}
private boolean isReregister(@NonNull Intent intent) {
return intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false);
}
private void initializeChallengeListener() {
smsRetrieverReceiver = new SmsRetrieverReceiver(getApplication());
smsRetrieverReceiver.registerReceiver();
}
private void shutdownChallengeListener() {
if (smsRetrieverReceiver != null) {
smsRetrieverReceiver.unregisterReceiver();
smsRetrieverReceiver = null;
}
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data
package org.thoughtcrime.securesms.registration.data
import android.app.backup.BackupManager
import android.content.Context
@@ -45,12 +45,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.PushChallengeRequest
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.svr.Svr3Credentials

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
import org.signal.core.util.logging.Log

View File

@@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
import org.thoughtcrime.securesms.pin.SvrWrongPinException
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
/**
* This is a merging of the NetworkResult pattern and the Processor pattern of registration v1.

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
import org.signal.core.util.logging.Log
import org.whispersystems.signalservice.api.NetworkResult

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
import org.signal.core.util.logging.Log
import org.whispersystems.signalservice.api.NetworkResult

View File

@@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data.network
package org.thoughtcrime.securesms.registration.data.network
import okio.IOException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException

View File

@@ -1,24 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
public class AccountLockedFragment extends BaseAccountLockedFragment {
public AccountLockedFragment() {
super(R.layout.account_locked_fragment);
}
@Override
protected BaseRegistrationViewModel getViewModel() {
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
}
@Override
protected void onNext() {
requireActivity().finish();
}
}

View File

@@ -1,73 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
/**
* Base fragment used by registration and change number flow to show an account as locked.
*/
public abstract class BaseAccountLockedFragment extends LoggingFragment {
public BaseAccountLockedFragment(int contentLayoutId) {
super(contentLayoutId);
}
@Override
@CallSuper
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title));
TextView description = view.findViewById(R.id.account_locked_description);
BaseRegistrationViewModel viewModel = getViewModel();
viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(),
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
);
view.findViewById(R.id.account_locked_next).setOnClickListener(v -> onNext());
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(v -> learnMore());
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onNext();
}
});
}
private void learnMore() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
startActivity(intent);
}
private static long durationToDays(long duration) {
return duration != 0L ? getLockoutDays(duration) : 7;
}
private static int getLockoutDays(long timeRemainingMs) {
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
}
protected abstract BaseRegistrationViewModel getViewModel();
protected abstract void onNext();
}

View File

@@ -1,442 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.CallSuper;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.navigation.Navigation;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
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.ActionCountDownButton;
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
/**
* Base fragment used by registration and change number flow to input an SMS verification code or request a
* phone code after requesting SMS.
*
* @param <ViewModel> - The concrete view model used by the subclasses, for ease of access in said subclass
*/
public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistrationViewModel> extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback {
private static final String TAG = Log.tag(BaseEnterSmsCodeFragment.class);
private ScrollView scrollView;
private TextView subheader;
private VerificationCodeView verificationCodeView;
private VerificationPinKeyboard keyboard;
private ActionCountDownButton callMeCountDown;
private ActionCountDownButton resendSmsCountDown;
private MaterialButton wrongNumber;
private MaterialButton bottomSheetButton;
private boolean autoCompleting;
private ViewModel viewModel;
protected final LifecycleDisposable disposables = new LifecycleDisposable();
public BaseEnterSmsCodeFragment(@LayoutRes int contentLayoutId) {
super(contentLayoutId);
}
@Override
@CallSuper
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);
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);
resendSmsCountDown = view.findViewById(R.id.resend_sms_count_down);
wrongNumber = view.findViewById(R.id.wrong_number);
bottomSheetButton = view.findViewById(R.id.having_trouble_button);
new SignalStrengthPhoneStateListener(this, this);
connectKeyboard(verificationCodeView, keyboard);
ViewUtil.hideKeyboard(requireContext(), view);
setOnCodeFullyEnteredListener(verificationCodeView);
wrongNumber.setOnClickListener(v -> returnToPhoneEntryScreen());
bottomSheetButton.setOnClickListener( v -> showBottomSheet());
callMeCountDown.setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in);
resendSmsCountDown.setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in);
callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest());
resendSmsCountDown.setOnClickListener(v -> handleSmsRequest());
callMeCountDown.setListener((v, remaining) -> {
if (remaining <= 30) {
scrollView.smoothScrollTo(0, v.getBottom());
callMeCountDown.setListener(null);
}
});
resendSmsCountDown.setListener((v, remaining) -> {
if (remaining <= 30) {
scrollView.smoothScrollTo(0, v.getBottom());
resendSmsCountDown.setListener(null);
}
});
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = getViewModel();
viewModel.getIncorrectCodeAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
if (attempts >= 3) {
bottomSheetButton.setVisibility(View.VISIBLE);
}
});
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
viewModel.resetSession();
this.remove();
requireActivity().getOnBackPressedDispatcher().onBackPressed();
}
});
}
protected abstract ViewModel getViewModel();
protected abstract void handleSuccessfulVerify();
protected abstract void navigateToCaptcha();
protected abstract void navigateToRegistrationLock(long timeRemaining);
protected abstract void navigateToKbsAccountLocked();
private void returnToPhoneEntryScreen() {
viewModel.resetSession();
Navigation.findNavController(requireView()).navigateUp();
}
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
verificationCodeView.setOnCompleteListener(code -> {
callMeCountDown.setVisibility(View.INVISIBLE);
resendSmsCountDown.setVisibility(View.INVISIBLE);
wrongNumber.setVisibility(View.INVISIBLE);
keyboard.displayProgress();
Disposable verify = viewModel.verifyCodeWithoutRegistrationLock(code)
.observeOn(AndroidSchedulers.mainThread())
.subscribe((VerifyResponseProcessor processor) -> {
if (!processor.hasResult()) {
Log.w(TAG, "post verify: ", processor.getError());
}
if (processor.hasResult()) {
handleSuccessfulVerify();
} else if (processor.rateLimit()) {
handleRateLimited();
} else if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) {
LockedException lockedException = processor.getLockedException();
handleRegistrationLock(lockedException.getTimeRemaining());
} else if (processor.authorizationFailed()) {
handleIncorrectCodeError();
} else {
Log.w(TAG, "Unable to verify code", processor.getError());
handleGeneralError();
}
});
disposables.add(verify);
});
}
protected void displaySuccess(@NonNull Runnable runAfterAnimation) {
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
runAfterAnimation.run();
}
});
}
protected void handleRateLimited() {
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
builder.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);
resendSmsCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
})
.show();
}
});
}
protected void handleRegistrationLock(long timeRemaining) {
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
navigateToRegistrationLock(timeRemaining);
}
});
}
protected void handleSvrAccountLocked() {
navigateToKbsAccountLocked();
}
protected void handleIncorrectCodeError() {
viewModel.incrementIncorrectCodeAttempts();
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show();
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
callMeCountDown.setVisibility(View.VISIBLE);
resendSmsCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
}
});
}
protected void handleGeneralError() {
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);
resendSmsCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
}
});
}
@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 * 200L);
}
}
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() {
showConfirmNumberDialogIfTranslated(requireContext(),
R.string.RegistrationActivity_phone_number_verification_dialog_title,
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
viewModel.getNumber().getE164Number(),
() -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.PHONE_CALL),
this::returnToPhoneEntryScreen);
}
private void handleSmsRequest() {
showConfirmNumberDialogIfTranslated(requireContext(),
R.string.RegistrationActivity_phone_number_verification_dialog_title,
R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number,
viewModel.getNumber().getE164Number(),
() -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.SMS_WITH_LISTENER),
this::returnToPhoneEntryScreen);
}
private void handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode mode) {
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult()) {
Toast.makeText(requireContext(), getCodeRequestedToastText(mode), Toast.LENGTH_LONG).show();
} else if (processor.captchaRequired(viewModel.getExcludedChallenges())) {
navigateToCaptcha();
} else if (processor.rateLimit()) {
handleRateLimited();
} else {
Log.w(TAG, "Unable to request phone code", processor.getError());
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
}
});
disposables.add(request);
}
@StringRes
private int getCodeRequestedToastText(VerifyAccountRepository.Mode mode) {
switch (mode) {
case PHONE_CALL:
return R.string.RegistrationActivity_call_requested;
case SMS_WITH_LISTENER:
case SMS_WITHOUT_LISTENER:
return R.string.RegistrationActivity_sms_requested;
default:
return R.string.RegistrationActivity_code_requested;
}
}
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();
String sessionE164 = viewModel.getSessionE164();
if (sessionE164 == null) {
returnToPhoneEntryScreen();
return;
}
subheader.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
Disposable request = viewModel.validateSession(sessionE164)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (!processor.hasResult()) {
Log.d(TAG, "Network error.");
returnToPhoneEntryScreen();
} else if (processor.isInvalidSession()) {
Log.d(TAG, "Registration session is invalid.");
returnToPhoneEntryScreen();
} else if (processor.cannotSubmitVerificationAttempt()) {
Log.d(TAG, "Cannot submit any more verification attempts.");
returnToPhoneEntryScreen();
} else if (processor.mustWaitToSubmitProof()) {
Log.d(TAG, "Blocked from submitting proof at this time.");
handleRateLimited();
}
// else session state is valid and server is ready to accept code
});
disposables.add(request);
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> {
if (callAtTime > 0) {
callMeCountDown.setVisibility(View.VISIBLE);
callMeCountDown.startCountDownTo(callAtTime);
} else {
callMeCountDown.setVisibility(View.INVISIBLE);
}
});
viewModel.getCanSmsAtTime().observe(getViewLifecycleOwner(), smsAtTime -> {
if (smsAtTime > 0) {
resendSmsCountDown.setVisibility(View.VISIBLE);
resendSmsCountDown.startCountDownTo(smsAtTime);
} else {
resendSmsCountDown.setVisibility(View.INVISIBLE);
}
});
}
private void showBottomSheet() {
ContactSupportBottomSheetFragment bottomSheet = new ContactSupportBottomSheetFragment();
bottomSheet.show(getChildFragmentManager(), "support_bottom_sheet");
}
@Override
public void onNoCellSignalPresent() {
// TODO animate in bottom sheet
}
@Override
public void onCellSignalPresent() {
// TODO animate away bottom sheet
}
}

View File

@@ -1,283 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
/**
* Base fragment used by registration and change number flow to deal with a registration locked account.
*/
public abstract class BaseRegistrationLockFragment extends LoggingFragment {
private static final String TAG = Log.tag(BaseRegistrationLockFragment.class);
/**
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
*/
public static final int MINIMUM_PIN_LENGTH = 4;
private EditText pinEntry;
private View forgotPin;
protected CircularProgressMaterialButton pinButton;
private TextView errorLabel;
private MaterialButton keyboardToggle;
private long timeRemaining;
private BaseRegistrationViewModel viewModel;
private final LifecycleDisposable disposables = new LifecycleDisposable();
public BaseRegistrationLockFragment(int contentLayoutId) {
super(contentLayoutId);
}
@Override
@CallSuper
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
forgotPin = view.findViewById(R.id.kbs_lock_forgot_pin);
RegistrationLockFragmentArgs args = RegistrationLockFragmentArgs.fromBundle(requireArguments());
timeRemaining = args.getTimeRemaining();
forgotPin.setVisibility(View.GONE);
forgotPin.setOnClickListener(v -> handleForgottenPin(timeRemaining));
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v);
handlePinEntry();
return true;
}
return false;
});
enableAndFocusPinEntry();
pinButton.setOnClickListener((v) -> {
ViewUtil.hideKeyboard(requireContext(), pinEntry);
handlePinEntry();
});
keyboardToggle.setOnClickListener((v) -> {
PinKeyboardType keyboardType = getPinEntryKeyboardType();
updateKeyboard(keyboardType.getOther());
keyboardToggle.setIconResource(keyboardType.getIconResource());
});
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
keyboardToggle.setIconResource(keyboardType.getIconResource());
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = getViewModel();
viewModel.getLockedTimeRemaining()
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
Integer triesRemaining = viewModel.getSvrTriesRemaining();
if (triesRemaining != null) {
if (triesRemaining <= 3) {
int daysRemaining = getLockoutDays(timeRemaining);
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
}
if (triesRemaining < 5) {
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining));
}
}
}
protected abstract BaseRegistrationViewModel getViewModel();
private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) {
Resources resources = requireContext().getResources();
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining);
return tries + " " + days;
}
protected PinKeyboardType getPinEntryKeyboardType() {
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
}
private void handlePinEntry() {
pinEntry.setEnabled(false);
final String pin = pinEntry.getText().toString();
int trimmedLength = pin.replace(" ", "").length();
if (trimmedLength == 0) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
enableAndFocusPinEntry();
return;
}
if (trimmedLength < MINIMUM_PIN_LENGTH) {
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
enableAndFocusPinEntry();
return;
}
pinButton.setSpinning();
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult()) {
handleSuccessfulPinEntry(pin);
} else if (processor.wrongPin()) {
onIncorrectKbsRegistrationLockPin(Objects.requireNonNull(processor.getSvrTriesRemaining()));
} else if (processor.isRegistrationLockPresentAndSvrExhausted() || processor.registrationLock()) {
onKbsAccountLocked();
} else if (processor.rateLimit()) {
onRateLimited();
} else {
Log.w(TAG, "Unable to verify code with registration lock", processor.getError());
onError();
}
});
disposables.add(verify);
}
public void onIncorrectKbsRegistrationLockPin(int svrTriesRemaining) {
pinButton.cancelSpinning();
pinEntry.getText().clear();
enableAndFocusPinEntry();
viewModel.setSvrTriesRemaining(svrTriesRemaining);
if (svrTriesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.");
onAccountLocked();
return;
}
if (svrTriesRemaining == 3) {
int daysRemaining = getLockoutDays(timeRemaining);
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
.setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.show();
}
if (svrTriesRemaining > 5) {
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
} else {
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining));
forgotPin.setVisibility(View.VISIBLE);
}
}
public void onRateLimited() {
pinButton.cancelSpinning();
enableAndFocusPinEntry();
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
}
public void onKbsAccountLocked() {
onAccountLocked();
}
public void onError() {
pinButton.cancelSpinning();
enableAndFocusPinEntry();
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
private void handleForgottenPin(long timeRemainingMs) {
int lockoutDays = getLockoutDays(timeRemainingMs);
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
}
private static int getLockoutDays(long timeRemainingMs) {
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
}
private void onAccountLocked() {
navigateToAccountLocked();
}
protected abstract void navigateToAccountLocked();
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
pinEntry.getText().clear();
}
private void enableAndFocusPinEntry() {
pinEntry.setEnabled(true);
pinEntry.setFocusable(true);
ViewUtil.focusAndShowKeyboard(pinEntry);
}
protected abstract void handleSuccessfulPinEntry(@NonNull String pin);
protected abstract void sendEmailToSupport();
}

View File

@@ -1,83 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import java.io.Serializable;
/**
* Fragment that displays a Captcha in a WebView.
*/
public final class CaptchaFragment extends LoggingFragment {
public static final String EXTRA_VIEW_MODEL_PROVIDER = "view_model_provider";
private BaseRegistrationViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_captcha, container, false);
}
@Override
@SuppressLint("SetJavaScriptEnabled")
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
WebView webView = view.findViewById(R.id.registration_captcha_web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.clearCache(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null && url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
handleToken(url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length()));
return true;
}
return false;
}
});
webView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL);
CaptchaViewModelProvider provider = null;
if (getArguments() != null) {
provider = (CaptchaViewModelProvider) requireArguments().getSerializable(EXTRA_VIEW_MODEL_PROVIDER);
}
if (provider == null) {
viewModel = new ViewModelProvider(
requireActivity()).get(RegistrationViewModel.class);
} else {
viewModel = provider.get(this);
}
}
private void handleToken(@NonNull String token) {
viewModel.setCaptchaResponse(token);
NavHostFragment.findNavController(this).navigateUp();
}
public interface CaptchaViewModelProvider extends Serializable {
@NonNull BaseRegistrationViewModel get(@NonNull CaptchaFragment fragment);
}
}

View File

@@ -1,80 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.navigation.Navigation;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
public class ChooseBackupFragment extends LoggingFragment {
private static final String TAG = Log.tag(ChooseBackupFragment.class);
private static final short OPEN_FILE_REQUEST_CODE = 3862;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_registration_choose_backup, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
View chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
TextView learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
ChooseBackupFragmentDirections.ActionRestore restore = ChooseBackupFragmentDirections.actionRestore();
restore.setUri(data.getData());
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), restore);
}
}
private void onChooseBackupSelected(@NonNull View view) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("application/octet-stream");
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
if (Build.VERSION.SDK_INT >= 26) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory());
}
try {
startActivityForResult(intent, OPEN_FILE_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Toast.makeText(requireContext(), R.string.ChooseBackupFragment__no_file_browser_available, Toast.LENGTH_LONG).show();
Log.w(TAG, "No matching activity!", e);
}
}
}

View File

@@ -1,3 +1,8 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
@@ -20,8 +25,8 @@ import androidx.loader.content.Loader;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel;
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import java.util.ArrayList;
import java.util.Map;
@@ -32,7 +37,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
public static final String KEY_COUNTRY_CODE = "country_code";
private EditText countryFilter;
private RegistrationViewModel model;
private ChangeNumberViewModel model;
private String resultKey;
@Override
@@ -50,7 +55,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
}
if (resultKey == null) {
model = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
model = new ViewModelProvider(requireActivity()).get(ChangeNumberViewModel.class);
}
countryFilter = view.findViewById(R.id.country_search);
@@ -67,7 +72,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
String countryName = item.get("country_name");
if (resultKey == null) {
model.onCountrySelected(countryName, countryCode);
model.setNewCountry(countryCode, countryName);
} else {
Bundle result = new Bundle();
result.putString(KEY_COUNTRY, countryName);

View File

@@ -1,494 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ScrollView;
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;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
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.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;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController;
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
public final class EnterPhoneNumberFragment extends LoggingFragment implements RegistrationNumberInputController.Callbacks {
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
private TextInputLayout countryCode;
private TextInputLayout number;
private CircularProgressMaterialButton register;
private View cancel;
private ScrollView scrollView;
private RegistrationViewModel viewModel;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@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);
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(),
this,
Objects.requireNonNull(number.getEditText()),
countryCode);
register.setOnClickListener(v -> handleRegister(requireContext()));
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
if (viewModel.isReregister()) {
cancel.setVisibility(View.VISIBLE);
cancel.setOnClickListener(v -> requireActivity().finish());
} else {
cancel.setVisibility(View.GONE);
}
viewModel.getLiveNumber().observe(getViewLifecycleOwner(), controller::updateNumberFormatter);
if (viewModel.hasCaptchaToken()) {
ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250);
}
Toolbar toolbar = view.findViewById(R.id.toolbar);
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
final ActionBar supportActionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setTitle(null);
}
final NumberViewState viewModelNumber = viewModel.getNumber();
if (viewModelNumber.getCountryCode() == 0) {
controller.prepopulateCountryCode();
}
controller.setNumberAndCountryCode(viewModelNumber);
ViewUtil.focusAndShowKeyboard(number.getEditText());
if (viewModel.hasUserSkippedReRegisterFlow() && viewModel.shouldAutoShowSmsConfirmDialog()) {
viewModel.setAutoShowSmsConfirmDialog(false);
ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250);
}
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.enter_phone_number, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.phone_menu_use_proxy) {
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), EnterPhoneNumberFragmentDirections.actionEditProxy());
return true;
} else {
return false;
}
}
private void handleRegister(@NonNull Context context) {
if (viewModel.getNumber().getCountryCode() == 0) {
showErrorDialog(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code));
return;
}
if (TextUtils.isEmpty(viewModel.getNumber().getNationalNumber())) {
showErrorDialog(context, getString(R.string.RegistrationActivity_please_enter_a_valid_phone_number_to_register));
return;
}
final NumberViewState number = viewModel.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) {
confirmNumberPrompt(context, e164number, () -> onE164EnteredSuccessfully(context, true));
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context));
} 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 onE164EnteredSuccessfully(@NonNull Context context, boolean fcmSupported) {
enterInProgressUiState();
Log.d(TAG, "E164 entered successfully.");
Disposable disposable = viewModel.canEnterSkipSmsFlow()
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturnItem(false)
.subscribe(canEnter -> {
if (canEnter) {
Log.i(TAG, "Entering skip flow.");
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment());
} else {
Log.i(TAG, "Unable to collect necessary data to enter skip flow, returning to normal");
handleRequestVerification(context, fcmSupported);
}
});
disposables.add(disposable);
}
private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) {
if (fcmSupported) {
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
AtomicBoolean handled = new AtomicBoolean(false);
Debouncer debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(5));
debouncer.publish(() -> {
if (!handled.getAndSet(true)) {
Log.w(TAG, "Timed out waiting for SMS listener!");
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
}
});
task.addOnSuccessListener(none -> {
if (!handled.getAndSet(true)) {
Log.i(TAG, "Successfully registered SMS listener.");
requestVerificationCode(Mode.SMS_WITH_LISTENER);
} else {
Log.w(TAG, "Successfully registered listener after timeout.");
}
debouncer.clear();
});
task.addOnFailureListener(e -> {
if (!handled.getAndSet(true)) {
Log.w(TAG, "Failed to register SMS listener.", e);
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
} else {
Log.w(TAG, "Failed to register listener after timeout.");
}
debouncer.clear();
});
task.addOnCanceledListener(() -> {
if (!handled.getAndSet(true)) {
Log.w(TAG, "SMS listener registration canceled.");
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
} else {
Log.w(TAG, "SMS listener registration canceled after timeout.");
}
debouncer.clear();
});
} else {
Log.i(TAG, "FCM is not supported, using no SMS listener");
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
}
}
private void enterInProgressUiState() {
register.setSpinning();
countryCode.setEnabled(false);
number.setEnabled(false);
cancel.setVisibility(View.GONE);
}
private void exitInProgressUiState() {
register.cancelSpinning();
countryCode.setEnabled(true);
number.setEnabled(true);
if (viewModel.isReregister()) {
cancel.setVisibility(View.VISIBLE);
}
}
private void requestVerificationCode(@NonNull Mode mode) {
NavController navController = NavHostFragment.findNavController(this);
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
final DialogInterface.OnClickListener proceedToNextScreen = (dialog, which) -> SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc())
.doOnSubscribe(unused -> SignalStore.account().setRegistered(false))
.observeOn(AndroidSchedulers.mainThread())
.subscribe((RegistrationSessionProcessor processor) -> {
Context context = getContext();
if (context == null) {
Log.w(TAG, "[requestVerificationCode] Invalid context! Skipping.");
return;
}
if (processor.verificationCodeRequestSuccess()) {
disposables.add(updateFcmTokenValue());
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
} else if (processor.captchaRequired(viewModel.getExcludedChallenges())) {
Log.i(TAG, "Unable to request sms code due to captcha required");
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
} else if (processor.exhaustedVerificationCodeAttempts()) {
Log.i(TAG, "Unable to request sms code due to exhausting attempts");
showErrorDialog(context, context.getString(R.string.RegistrationActivity_rate_limited_to_service));
} else if (processor.rateLimit()) {
Log.i(TAG, "Unable to request sms code due to rate limit");
showErrorDialog(context, context.getString(R.string.RegistrationActivity_rate_limited_to_try_again, formatMillisecondsToString(processor.getRateLimit())));
} else if (processor.isImpossibleNumber()) {
Log.w(TAG, "Impossible number", processor.getError());
Dialogs.showAlertDialog(requireContext(),
context.getString(R.string.RegistrationActivity_invalid_number),
String.format(context.getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber()));
} else if (processor.isNonNormalizedNumber()) {
handleNonNormalizedNumberError(processor.getOriginalNumber(), processor.getNormalizedNumber(), mode);
} else if (processor.isTokenRejected()) {
Log.i(TAG, "The server did not accept the information.", processor.getError());
showErrorDialog(context, context.getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human));
} else if (processor.externalServiceFailure()) {
Log.w(TAG, "The server reported a failure with an external service.", processor.getError());
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen);
} else if (processor.invalidTransportModeFailure()) {
Log.w(TAG, "The server reported an invalid transport mode failure.", processor.getError());
new MaterialAlertDialogBuilder(context)
.setMessage(R.string.RegistrationActivity_we_couldnt_send_you_a_verification_code)
.setPositiveButton(R.string.RegistrationActivity_voice_call, (dialog, which) -> requestVerificationCode(Mode.PHONE_CALL))
.setNegativeButton(R.string.RegistrationActivity_cancel, null)
.show();
} else if ( processor.isMalformedRequest()){
Log.w(TAG, "The server reported a malformed request.", processor.getError());
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen);
} else if (processor.isRetryException()) {
Log.w(TAG, "The server reported a failure that is retryable.", processor.getError());
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen);
} else {
Log.i(TAG, "Unknown error during verification code request", processor.getError());
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service));
}
exitInProgressUiState();
});
disposables.add(request);
}
private Disposable updateFcmTokenValue() {
return viewModel.updateFcmTokenValue().subscribe();
}
private String formatMillisecondsToString(long milliseconds) {
long totalSeconds = milliseconds / 1000;
long HH = totalSeconds / 3600;
long MM = (totalSeconds % 3600) / 60;
long SS = totalSeconds % 60;
return String.format(Locale.getDefault(), "%02d:%02d:%02d", HH, MM, SS);
}
public void showErrorDialog(Context context, String msg) {
showErrorDialog(context, msg, null);
}
public void showErrorDialog(Context context, String msg, DialogInterface.OnClickListener positiveButtonListener) {
new MaterialAlertDialogBuilder(context).setMessage(msg).setPositiveButton(android.R.string.ok, positiveButtonListener).show();
}
@Override
public void onNumberFocused() {
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250);
}
@Override
public void onNumberInputDone(@NonNull View view) {
ViewUtil.hideKeyboard(requireContext(), view);
handleRegister(requireContext());
}
@Override
public void setNationalNumber(@NonNull String number) {
viewModel.setNationalNumber(number);
}
@Override
public void setCountry(int countryCode) {
viewModel.onCountrySelected(null, countryCode);
}
@Override
public void onStart() {
super.onStart();
String sessionE164 = viewModel.getSessionE164();
if (sessionE164 != null && viewModel.getSessionId() != null && viewModel.getCaptchaToken() == null) {
checkIfSessionIsInProgressAndAdvance(sessionE164);
}
}
private void checkIfSessionIsInProgressAndAdvance(@NonNull String sessionE164) {
NavController navController = NavHostFragment.findNavController(this);
Disposable request = viewModel.validateSession(sessionE164)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult() && processor.canSubmitProofImmediately()) {
try {
viewModel.restorePhoneNumberStateFromE164(sessionE164);
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
} catch (NumberParseException numberParseException) {
viewModel.resetSession();
}
} else {
viewModel.resetSession();
}
});
disposables.add(request);
}
private void handleNonNormalizedNumberError(@NonNull String originalNumber, @NonNull String normalizedNumber, @NonNull Mode mode) {
try {
Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null);
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationActivity_non_standard_number_format)
.setMessage(getString(R.string.RegistrationActivity_the_number_you_entered_appears_to_be_a_non_standard, originalNumber, normalizedNumber))
.setNegativeButton(android.R.string.no, (d, i) -> d.dismiss())
.setNeutralButton(R.string.RegistrationActivity_contact_signal_support, (d, i) -> {
String subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format);
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null);
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body);
d.dismiss();
})
.setPositiveButton(R.string.yes, (d, i) -> {
countryCode.getEditText().setText(String.valueOf(phoneNumber.getCountryCode()));
number.getEditText().setText(String.valueOf(phoneNumber.getNationalNumber()));
requestVerificationCode(mode);
d.dismiss();
})
.show();
} catch (NumberParseException e) {
Log.w(TAG, "Failed to parse number!", e);
Dialogs.showAlertDialog(requireContext(),
getString(R.string.RegistrationActivity_invalid_number),
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber()));
}
}
private void handlePromptForNoPlayServices(@NonNull Context context) {
Log.d(TAG, "Device does not have Play Services, showing consent dialog.");
new MaterialAlertDialogBuilder(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) -> onE164EnteredSuccessfully(context, false))
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void confirmNumberPrompt(@NonNull Context context,
@NonNull String e164number,
@NonNull Runnable onConfirmed)
{
enterInProgressUiState();
disposables.add(
viewModel.canEnterSkipSmsFlow()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(canSkipSms -> {
Log.d(TAG, "Showing confirm number dialog. canSkipSms = " + canSkipSms + " hasUserSkipped = " + viewModel.hasUserSkippedReRegisterFlow());
final EditText editText = this.number.getEditText();
showConfirmNumberDialogIfTranslated(context,
viewModel.hasUserSkippedReRegisterFlow() ? R.string.RegistrationActivity_additional_verification_required
: R.string.RegistrationActivity_phone_number_verification_dialog_title,
canSkipSms ? null
: R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number,
e164number,
() -> {
Log.d(TAG, "User confirmed number.");
if (editText != null) {
ViewUtil.hideKeyboard(context, editText);
}
onConfirmed.run();
},
() -> {
Log.d(TAG, "User canceled confirm number, returning to edit number.");
exitInProgressUiState();
if (editText != null) {
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(editText);
}
});
}
)
);
}
}

View File

@@ -1,59 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import java.io.IOException;
public final class EnterSmsCodeFragment extends BaseEnterSmsCodeFragment<RegistrationViewModel> implements SignalStrengthPhoneStateListener.Callback {
private static final String TAG = Log.tag(EnterSmsCodeFragment.class);
public EnterSmsCodeFragment() {
super(R.layout.fragment_registration_enter_code);
}
@Override
protected @NonNull RegistrationViewModel getViewModel() {
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
}
@Override
protected void handleSuccessfulVerify() {
SimpleTask.run(() -> {
long startTime = System.currentTimeMillis();
try {
RemoteConfig.refreshSync();
Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags.");
} catch (IOException e) {
Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e);
}
return null;
}, none -> displaySuccess(() -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), EnterSmsCodeFragmentDirections.actionSuccessfulRegistration())));
}
@Override
protected void navigateToRegistrationLock(long timeRemaining) {
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
EnterSmsCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
}
@Override
protected void navigateToCaptcha() {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterSmsCodeFragmentDirections.actionRequestCaptcha());
}
@Override
protected void navigateToKbsAccountLocked() {
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionAccountLocked());
}
}

View File

@@ -1,103 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.fragments
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
import org.thoughtcrime.securesms.util.BackupUtil
/**
* Fragment displayed during registration which allows a user to read through
* what permissions are granted to Signal and why, and a means to either skip
* granting those permissions or continue to grant via system dialogs.
*/
class GrantPermissionsFragment : ComposeFragment() {
private val args by navArgs<GrantPermissionsFragmentArgs>()
private val viewModel by activityViewModels<RegistrationViewModel>()
private val isSearchingForBackup = mutableStateOf(false)
@Composable
override fun FragmentContent() {
val isSearchingForBackup by this.isSearchingForBackup
GrantPermissionsScreen(
deviceBuildVersion = Build.VERSION.SDK_INT,
isSearchingForBackup = isSearchingForBackup,
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
onNextClicked = this::onNextClicked,
onNotNowClicked = this::onNotNowClicked
)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
private fun onNextClicked() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> {
WelcomeFragment.continueClicked(
this,
viewModel,
{ isSearchingForBackup.value = true },
{ isSearchingForBackup.value = false },
GrantPermissionsFragmentDirections.actionSkipRestore(),
GrantPermissionsFragmentDirections.actionRestore()
)
}
WelcomeAction.RESTORE_BACKUP -> {
WelcomeFragment.restoreFromBackupClicked(
this,
viewModel,
GrantPermissionsFragmentDirections.actionTransferOrRestore()
)
}
}
}
private fun onNotNowClicked() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> {
WelcomeFragment.gatherInformationAndContinue(
this,
viewModel,
{ isSearchingForBackup.value = true },
{ isSearchingForBackup.value = false },
GrantPermissionsFragmentDirections.actionSkipRestore(),
GrantPermissionsFragmentDirections.actionRestore()
)
}
WelcomeAction.RESTORE_BACKUP -> {
WelcomeFragment.gatherInformationAndChooseBackup(
this,
viewModel,
GrantPermissionsFragmentDirections.actionTransferOrRestore()
)
}
}
}
/**
* Which welcome action the user selected which prompted this
* screen.
*/
enum class WelcomeAction {
CONTINUE,
RESTORE_BACKUP
}
}

View File

@@ -1,282 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.PinRestoreEntryFragmentBinding
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor
import org.thoughtcrime.securesms.registration.viewmodel.ReRegisterWithPinViewModel
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Using a recovery password or restored KBS token attempt to register in the skip flow.
*/
class ReRegisterWithPinFragment : LoggingFragment(R.layout.pin_restore_entry_fragment) {
companion object {
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
}
private var _binding: PinRestoreEntryFragmentBinding? = null
private val binding: PinRestoreEntryFragmentBinding
get() = _binding!!
private val registrationViewModel: RegistrationViewModel by activityViewModels()
private val reRegisterViewModel: ReRegisterWithPinViewModel by viewModels()
private val disposables = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = PinRestoreEntryFragmentBinding.bind(view)
disposables.bindTo(viewLifecycleOwner.lifecycle)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.pinRestorePinTitle)
binding.pinRestorePinDescription.setText(R.string.RegistrationLockFragment__enter_the_pin_you_created_for_your_account)
binding.pinRestoreForgotPin.visibility = View.GONE
binding.pinRestoreForgotPin.setOnClickListener { onNeedHelpClicked() }
binding.pinRestoreSkipButton.setOnClickListener { onSkipClicked() }
binding.pinRestorePinInput.imeOptions = EditorInfo.IME_ACTION_DONE
binding.pinRestorePinInput.setOnEditorActionListener { v, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v!!)
handlePinEntry()
return@setOnEditorActionListener true
}
false
}
enableAndFocusPinEntry()
binding.pinRestorePinConfirm.setOnClickListener {
handlePinEntry()
}
binding.pinRestoreKeyboardToggle.setOnClickListener {
val currentKeyboardType: PinKeyboardType = getPinEntryKeyboardType()
updateKeyboard(currentKeyboardType.other)
binding.pinRestoreKeyboardToggle.setIconResource(currentKeyboardType.iconResource)
}
binding.pinRestoreKeyboardToggle.setIconResource(getPinEntryKeyboardType().other.iconResource)
reRegisterViewModel.updateSvrTriesRemaining(registrationViewModel.svrTriesRemaining)
disposables += reRegisterViewModel.triesRemaining.subscribe(this::updateTriesRemaining)
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
private fun handlePinEntry() {
val pin: String? = binding.pinRestorePinInput.text?.toString()
val trimmedLength = pin?.trim()?.length ?: 0
if (trimmedLength == 0) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
if (trimmedLength < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) {
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
disposables += registrationViewModel.verifyReRegisterWithPin(pin!!)
.doOnSubscribe {
ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput)
binding.pinRestorePinInput.isEnabled = false
binding.pinRestorePinConfirm.setSpinning()
}
.doAfterTerminate {
binding.pinRestorePinInput.isEnabled = true
binding.pinRestorePinConfirm.cancelSpinning()
}
.subscribe { processor ->
if (processor.hasResult()) {
Log.i(TAG, "Successfully re-registered via skip flow")
try {
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment)
return@subscribe
} catch (ise: IllegalStateException) {
Log.w(TAG, "Could not get parent activity fragment manager!")
}
try {
val hostActivity = activity
if (hostActivity != null) {
Navigation.findNavController(hostActivity, R.id.nav_host_fragment).safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment)
return@subscribe
} else {
Log.w(TAG, "Could not get parent activity!")
}
} catch (ise: IllegalStateException) {
Log.w(TAG, "Could not find navigation host fragment!")
}
activity?.let {
Log.w(TAG, "Could not navigate to registration complete. Finishing activity gracefully.")
it.finish()
}
return@subscribe
}
reRegisterViewModel.hasIncorrectGuess = true
if (processor is VerifyResponseWithRegistrationLockProcessor && processor.wrongPin()) {
reRegisterViewModel.updateSvrTriesRemaining(processor.svrTriesRemaining)
if (processor.svrTriesRemaining != null) {
registrationViewModel.svrTriesRemaining = processor.svrTriesRemaining
}
return@subscribe
} else if (processor.isRegistrationLockPresentAndSvrExhausted()) {
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
onAccountLocked()
} else if (processor.isIncorrectRegistrationRecoveryPassword()) {
Log.w(TAG, "Registration recovery password was incorrect. Moving to SMS verification.")
onSkipPinEntry()
} else if (processor.isServerSentError()) {
Log.i(TAG, "Error from server, not likely recoverable", processor.error)
genericErrorDialog()
} else {
Log.i(TAG, "Unexpected error occurred", processor.error)
genericErrorDialog()
}
}
}
private fun updateTriesRemaining(triesRemaining: Int) {
if (reRegisterViewModel.hasIncorrectGuess) {
if (triesRemaining == 1 && !reRegisterViewModel.isLocalVerification) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
if (triesRemaining > 5) {
binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin)
} else {
binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)
}
binding.pinRestoreForgotPin.visibility = View.VISIBLE
} else {
if (triesRemaining == 1) {
binding.pinRestoreForgotPin.visibility = View.VISIBLE
if (!reRegisterViewModel.isLocalVerification) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
if (triesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.")
onAccountLocked()
}
}
private fun onAccountLocked() {
Log.d(TAG, "Showing Incorrect PIN dialog. Is local verification: ${reRegisterViewModel.isLocalVerification}")
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() }
.setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) }
.show()
}
private fun enableAndFocusPinEntry() {
binding.pinRestorePinInput.isEnabled = true
binding.pinRestorePinInput.isFocusable = true
ViewUtil.focusAndShowKeyboard(binding.pinRestorePinInput)
}
private fun getPinEntryKeyboardType(): PinKeyboardType {
val isNumeric = binding.pinRestorePinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
}
private fun updateKeyboard(keyboard: PinKeyboardType) {
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
binding.pinRestorePinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
binding.pinRestorePinInput.text?.clear()
}
private fun onNeedHelpClicked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_need_help)
.setMessage(getString(message, SvrConstants.MINIMUM_PIN_LENGTH))
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ ->
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null)
CommunicationActions.openEmail(
requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(R.string.ReRegisterWithPinFragment_support_email_subject),
body
)
}
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show()
}
private fun onSkipClicked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
.setMessage(message)
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show()
}
private fun onSkipPinEntry() {
Log.d(TAG, "User skipping PIN entry.")
registrationViewModel.setUserSkippedReRegisterFlow(true)
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_enterPhoneNumberFragment)
}
private fun genericErrorDialog() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.RegistrationActivity_error_connecting_to_service)
.setPositiveButton(android.R.string.ok, null)
.show()
}
}

View File

@@ -1,91 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.navigation.ActivityNavigator
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.pin.PinRestoreActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
/**
* [RegistrationCompleteFragment] is not visible to the user, but functions as basically a redirect towards one of:
* - [PIN Restore flow activity](org.thoughtcrime.securesms.pin.PinRestoreActivity)
* - [Profile](org.thoughtcrime.securesms.profiles.edit.EditProfileActivity) / [PIN creation](org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity) flow activities (this class chains the necessary activities together as an intent)
* - Exit registration flow and progress to conversation list
*/
class RegistrationCompleteFragment : LoggingFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_registration_blank, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity()
val viewModel: RegistrationViewModel by viewModels(ownerProducer = { requireActivity() })
if (SignalStore.misc.hasLinkedDevices) {
SignalStore.misc.shouldShowLinkedDevicesReminder = viewModel.isReregister
}
if (SignalStore.storageService.needsAccountRestore()) {
Log.i(TAG, "Performing pin restore.")
activity.startActivity(Intent(activity, PinRestoreActivity::class.java))
} else {
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id)
val needsProfile = isProfileNameEmpty || isAvatarEmpty
val needsPin = !SignalStore.svr.hasPin() && !viewModel.isReregister
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
if (!needsProfile && !needsPin) {
AppDependencies.jobManager
.startChain(ProfileUploadJob())
.then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob()))
.enqueue()
RegistrationUtil.maybeMarkRegistrationComplete()
}
var startIntent = MainActivity.clearTop(activity)
if (needsPin) {
startIntent = chainIntents(CreateSvrPinActivity.getIntentForPinCreate(activity), startIntent)
}
if (needsProfile) {
startIntent = chainIntents(CreateProfileActivity.getIntentForUserProfile(activity), startIntent)
}
activity.startActivity(startIntent)
}
activity.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
}
private fun chainIntents(sourceIntent: Intent, nextIntent: Intent): Intent {
sourceIntent.putExtra("next_intent", nextIntent)
return sourceIntent
}
companion object {
private val TAG = Log.tag(RegistrationCompleteFragment::class.java)
}
}

View File

@@ -1,3 +1,8 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.fragments;
public final class RegistrationConstants {

View File

@@ -1,92 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.signal.core.util.Stopwatch;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public final class RegistrationLockFragment extends BaseRegistrationLockFragment {
private static final String TAG = Log.tag(RegistrationLockFragment.class);
public RegistrationLockFragment() {
super(R.layout.fragment_registration_lock);
}
@Override
protected BaseRegistrationViewModel getViewModel() {
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
}
@Override
protected void navigateToAccountLocked() {
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionAccountLocked());
}
@Override
protected void handleSuccessfulPinEntry(@NonNull String pin) {
SignalStore.pin().setKeyboardType(getPinEntryKeyboardType());
SimpleTask.run(() -> {
SignalStore.onboarding().clearAll();
Stopwatch stopwatch = new Stopwatch("RegistrationLockRestore");
AppDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
stopwatch.split("AccountRestore");
AppDependencies
.getJobManager()
.startChain(new StorageSyncJob())
.then(new ReclaimUsernameAndLinkJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");
try {
RemoteConfig.refreshSync();
} catch (IOException e) {
Log.w(TAG, "Failed to refresh flags.", e);
}
stopwatch.split("RemoteConfig");
stopwatch.stop(TAG);
return null;
}, none -> {
pinButton.cancelSpinning();
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionSuccessfulRegistration());
});
}
@Override
protected void sendEmailToSupport() {
int subject = R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin;
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
subject,
null,
null);
CommunicationActions.openEmail(requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(subject),
body);
}
}

View File

@@ -1,494 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.AppInitialization;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupEvent;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.FullBackupImporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import java.io.IOException;
import java.util.Locale;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
public final class RestoreBackupFragment extends LoggingFragment {
private static final String TAG = Log.tag(RestoreBackupFragment.class);
private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782;
private TextView restoreBackupSize;
private TextView restoreBackupTime;
private TextView restoreBackupProgress;
private CircularProgressMaterialButton restoreButton;
private View skipRestoreButton;
private RegistrationViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_restore_backup, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
Log.i(TAG, "Backup restore.");
restoreBackupSize = view.findViewById(R.id.backup_size_text);
restoreBackupTime = view.findViewById(R.id.backup_created_text);
restoreBackupProgress = view.findViewById(R.id.backup_progress_text);
restoreButton = view.findViewById(R.id.restore_button);
skipRestoreButton = view.findViewById(R.id.skip_restore_button);
skipRestoreButton.setOnClickListener((v) -> {
Log.i(TAG, "User skipped backup restore.");
SafeNavigation.safeNavigate(Navigation.findNavController(view),
RestoreBackupFragmentDirections.actionSkip());
});
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
if (viewModel.isReregister()) {
Log.i(TAG, "Skipping backup restore during re-register.");
SafeNavigation.safeNavigate(Navigation.findNavController(view),
RestoreBackupFragmentDirections.actionSkipNoReturn());
return;
}
if (viewModel.hasBackupCompleted()) {
onBackupComplete();
return;
}
if (SignalStore.settings().isBackupEnabled()) {
Log.i(TAG, "Backups enabled, so a backup must have been previously restored.");
SafeNavigation.safeNavigate(Navigation.findNavController(view),
RestoreBackupFragmentDirections.actionSkipNoReturn());
return;
}
RestoreBackupFragmentArgs args = RestoreBackupFragmentArgs.fromBundle(requireArguments());
if ((Build.VERSION.SDK_INT < 29 || BackupUtil.isUserSelectionRequired(requireContext())) && args.getUri() != null) {
Log.i(TAG, "Restoring backup from passed uri");
initializeBackupForUri(view, args.getUri());
return;
}
if (BackupUtil.canUserAccessBackupDirectory(requireContext())) {
initializeBackupDetection(view);
} else {
Log.i(TAG, "Skipping backup detection. We don't have the permission.");
SafeNavigation.safeNavigate(Navigation.findNavController(view),
RestoreBackupFragmentDirections.actionSkipNoReturn());
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == OPEN_DOCUMENT_TREE_RESULT_CODE && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
Uri backupDirectoryUri = data.getData();
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
requireContext().getContentResolver()
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
enableBackups(requireContext());
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
RestoreBackupFragmentDirections.actionBackupRestored());
}
}
private void initializeBackupForUri(@NonNull View view, @NonNull Uri uri) {
getFromUri(requireContext(), uri, backup -> handleBackupInfo(view, backup));
}
@SuppressLint("StaticFieldLeak")
private void initializeBackupDetection(@NonNull View view) {
searchForBackup(backup -> handleBackupInfo(view, backup));
}
private void handleBackupInfo(@NonNull View view, @Nullable BackupUtil.BackupInfo backup) {
Context context = getContext();
if (context == null) {
Log.i(TAG, "No context on fragment, must have navigated away.");
return;
}
if (backup == null) {
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
SafeNavigation.safeNavigate(Navigation.findNavController(view),
RestoreBackupFragmentDirections.actionNoBackupFound());
} else {
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp())));
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
}
}
interface OnBackupSearchResultListener {
@MainThread
void run(@Nullable BackupUtil.BackupInfo backup);
}
static void searchForBackup(@NonNull OnBackupSearchResultListener listener) {
new AsyncTask<Void, Void, BackupUtil.BackupInfo>() {
@Override
protected @Nullable
BackupUtil.BackupInfo doInBackground(Void... voids) {
try {
return BackupUtil.getLatestBackup();
} catch (NoExternalStorageException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(@Nullable BackupUtil.BackupInfo backup) {
listener.run(backup);
}
}.execute();
}
static void getFromUri(@NonNull Context context,
@NonNull Uri backupUri,
@NonNull OnBackupSearchResultListener listener)
{
SimpleTask.run(() -> {
try {
return BackupUtil.getBackupInfoFromSingleUri(context, backupUri);
} catch (BackupUtil.BackupFileException e) {
Log.w(TAG, "Could not restore backup.", e);
postToastForBackupRestorationFailure(context, e);
return null;
}
},
listener::run);
}
private static void postToastForBackupRestorationFailure(@NonNull Context context, @NonNull BackupUtil.BackupFileException exception) {
final @StringRes int errorResId;
switch (exception.getState()) {
case READABLE:
throw new AssertionError("Unexpected error state.");
case NOT_FOUND:
errorResId = R.string.RestoreBackupFragment__backup_not_found;
break;
case UNSUPPORTED_FILE_EXTENSION:
errorResId = R.string.RestoreBackupFragment__backup_has_a_bad_extension;
break;
default:
errorResId = R.string.RestoreBackupFragment__backup_could_not_be_read;
}
ThreadUtil.postToMain(() -> Toast.makeText(context, errorResId, Toast.LENGTH_LONG).show());
}
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
prompt.addTextChangedListener(new PassphraseAsYouTypeFormatter());
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.RegistrationActivity_enter_backup_passphrase)
.setView(view)
.setPositiveButton(R.string.RegistrationActivity_restore, (dialog, which) -> {
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(prompt.getWindowToken(), 0);
restoreButton.setSpinning();
skipRestoreButton.setVisibility(View.INVISIBLE);
String passphrase = prompt.getText().toString();
restoreAsynchronously(context, backup, passphrase);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
Log.i(TAG, "Prompt for backup passphrase shown to user.");
}
@SuppressLint("StaticFieldLeak")
private void restoreAsynchronously(@NonNull Context context,
@NonNull BackupUtil.BackupInfo backup,
@NonNull String passphrase)
{
new AsyncTask<Void, Void, BackupImportResult>() {
@Override
protected BackupImportResult doInBackground(Void... voids) {
try {
Log.i(TAG, "Starting backup restore.");
DataRestoreConstraint.setRestoringData(true);
SQLiteDatabase database = SignalDatabase.getBackupDatabase();
BackupPassphrase.set(context, passphrase);
if (!FullBackupImporter.validatePassphrase(context, backup.getUri(), passphrase)) {
return BackupImportResult.FAILURE_UNKNOWN;
}
FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
backup.getUri(),
passphrase);
SignalDatabase.runPostBackupRestoreTasks(database);
NotificationChannels.getInstance().restoreContactNotificationChannels();
enableBackups(context);
AppInitialization.onPostBackupRestore(context);
Log.i(TAG, "Backup restore complete.");
return BackupImportResult.SUCCESS;
} catch (FullBackupImporter.DatabaseDowngradeException e) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
return BackupImportResult.FAILURE_VERSION_DOWNGRADE;
} catch (FullBackupImporter.ForeignKeyViolationException e) {
Log.w(TAG, "Failed due to foreign key constraint violations.", e);
return BackupImportResult.FAILURE_FOREIGN_KEY;
} catch (IOException e) {
Log.w(TAG, e);
return BackupImportResult.FAILURE_UNKNOWN;
} finally {
DataRestoreConstraint.setRestoringData(false);
}
}
@Override
protected void onPostExecute(@NonNull BackupImportResult result) {
viewModel.markBackupCompleted();
restoreButton.cancelSpinning();
skipRestoreButton.setVisibility(View.VISIBLE);
restoreBackupProgress.setText("");
switch (result) {
case SUCCESS:
Log.i(TAG, "Successful backup restore.");
break;
case FAILURE_VERSION_DOWNGRADE:
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show();
break;
case FAILURE_FOREIGN_KEY:
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show();
break;
case FAILURE_UNKNOWN:
Toast.makeText(context, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show();
break;
}
}
}.execute();
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onResume() {
super.onResume();
if (viewModel != null && viewModel.hasBackupCompleted()) {
onBackupComplete();
}
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(@NonNull BackupEvent event) {
long count = event.getCount();
if (count == 0) {
restoreBackupProgress.setText(R.string.RegistrationActivity_checking);
} else {
restoreBackupProgress.setText(getString(R.string.RegistrationActivity_d_messages_so_far, count));
}
restoreButton.setSpinning();
skipRestoreButton.setVisibility(View.INVISIBLE);
if (event.getType() == BackupEvent.Type.FINISHED) {
onBackupComplete();
}
}
private void onBackupComplete() {
if (BackupUtil.isUserSelectionRequired(requireContext()) && !BackupUtil.canUserAccessBackupDirectory(requireContext())) {
displayConfirmationDialog(requireContext());
} else {
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
RestoreBackupFragmentDirections.actionBackupRestored());
}
}
private void enableBackups(@NonNull Context context) {
if (BackupUtil.canUserAccessBackupDirectory(context)) {
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
SignalStore.settings().setBackupEnabled(true);
LocalBackupListener.schedule(context);
}
}
@RequiresApi(29)
private void displayConfirmationDialog(@NonNull Context context) {
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.RestoreBackupFragment__restore_complete)
.setMessage(R.string.RestoreBackupFragment__to_continue_using_backups_please_choose_a_folder)
.setPositiveButton(R.string.RestoreBackupFragment__choose_folder, (dialog, which) -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, OPEN_DOCUMENT_TREE_RESULT_CODE);
})
.setNegativeButton(R.string.RestoreBackupFragment__not_now, (dialog, which) -> {
BackupPassphrase.set(context, null);
dialog.dismiss();
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
RestoreBackupFragmentDirections.actionBackupRestored());
})
.setCancelable(false)
.show();
}
private enum BackupImportResult {
SUCCESS,
FAILURE_VERSION_DOWNGRADE,
FAILURE_FOREIGN_KEY,
FAILURE_UNKNOWN
}
public static class PassphraseAsYouTypeFormatter implements TextWatcher {
private static final int GROUP_SIZE = 5;
@Override
public void afterTextChanged(Editable editable) {
removeSpans(editable);
addSpans(editable);
}
private static void removeSpans(Editable editable) {
SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class);
for (SpaceSpan span : paddingSpans) {
editable.removeSpan(span);
}
}
private static void addSpans(Editable editable) {
final int length = editable.length();
for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) {
editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (editable.length() > BackupUtil.PASSPHRASE_LENGTH) {
editable.delete(BackupUtil.PASSPHRASE_LENGTH, editable.length());
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
/**
* A {@link ReplacementSpan} adds a small space after a single character.
* Based on https://stackoverflow.com/a/51949578
*/
private static class SpaceSpan extends ReplacementSpan {
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return (int) (paint.measureText(text, start, end) * 1.7f);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
canvas.drawText(text.subSequence(start, end).toString(), x, y, paint);
}
}
}

View File

@@ -1,257 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.ActivityNavigator;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import java.util.Optional;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
public final class WelcomeFragment extends LoggingFragment {
private static final String TAG = Log.tag(WelcomeFragment.class);
private CircularProgressMaterialButton continueButton;
private RegistrationViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_welcome, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
if (viewModel.isReregister()) {
if (viewModel.hasRestoreFlowBeenShown()) {
Log.i(TAG, "We've come back to the home fragment on a restore, user must be backing out");
if (!Navigation.findNavController(view).popBackStack()) {
FragmentActivity activity = requireActivity();
activity.finish();
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
}
return;
}
initializeNumber(requireContext(), viewModel);
Log.i(TAG, "Skipping restore because this is a reregistration.");
viewModel.setWelcomeSkippedOnRestore();
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
WelcomeFragmentDirections.actionSkipRestore());
} else {
setDebugLogSubmitMultiTapView(view.findViewById(R.id.image));
setDebugLogSubmitMultiTapView(view.findViewById(R.id.title));
continueButton = view.findViewById(R.id.welcome_continue_button);
continueButton.setOnClickListener(v -> onContinueClicked());
Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
restoreFromBackup.setOnClickListener(v -> onRestoreFromBackupClicked());
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
if (!canUserSelectBackup()) {
restoreFromBackup.setText(R.string.registration_activity__transfer_account);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onResume() {
super.onResume();
if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null) {
Log.i(TAG, "Found existing transferStatus, redirect to transfer flow");
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_welcomeFragment_to_deviceTransferSetup);
} else {
DeviceToDeviceTransferService.stop(requireContext());
}
}
private void onContinueClicked() {
if (Permissions.isRuntimePermissionsRequired()) {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.CONTINUE));
} else {
gatherInformationAndContinue(
this,
viewModel,
() -> continueButton.setSpinning(),
() -> continueButton.cancelSpinning(),
WelcomeFragmentDirections.actionSkipRestore(),
WelcomeFragmentDirections.actionRestore()
);
}
}
private void onRestoreFromBackupClicked() {
if (Permissions.isRuntimePermissionsRequired()) {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP));
} else {
gatherInformationAndChooseBackup(this, viewModel, WelcomeFragmentDirections.actionTransferOrRestore());
}
}
static void continueClicked(@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull Runnable onSearchForBackupStarted,
@NonNull Runnable onSearchForBackupFinished,
@NonNull NavDirections actionSkipRestore,
@NonNull NavDirections actionRestore)
{
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
Permissions.with(fragment)
.request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
.ifNecessary()
.onAnyResult(() -> gatherInformationAndContinue(fragment,
viewModel,
onSearchForBackupStarted,
onSearchForBackupFinished,
actionSkipRestore,
actionRestore))
.execute();
}
static void restoreFromBackupClicked(@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull NavDirections actionTransferOrRestore)
{
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
Permissions.with(fragment)
.request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
.ifNecessary()
.onAnyResult(() -> gatherInformationAndChooseBackup(fragment, viewModel, actionTransferOrRestore))
.execute();
}
static void gatherInformationAndContinue(
@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull Runnable onSearchForBackupStarted,
@NonNull Runnable onSearchForBackupFinished,
@NonNull NavDirections actionSkipRestore,
@NonNull NavDirections actionRestore
) {
onSearchForBackupStarted.run();
RestoreBackupFragment.searchForBackup(backup -> {
Context context = fragment.getContext();
if (context == null) {
Log.i(TAG, "No context on fragment, must have navigated away.");
return;
}
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
initializeNumber(fragment.requireContext(), viewModel);
onSearchForBackupFinished.run();
if (backup == null) {
Log.i(TAG, "Skipping backup. No backup found, or no permission to look.");
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
actionSkipRestore);
} else {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
actionRestore);
}
});
}
static void gatherInformationAndChooseBackup(@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull NavDirections actionTransferOrRestore) {
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
initializeNumber(fragment.requireContext(), viewModel);
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
actionTransferOrRestore);
}
@SuppressLint("MissingPermission")
private static void initializeNumber(@NonNull Context context, @NonNull RegistrationViewModel viewModel) {
Optional<Phonenumber.PhoneNumber> localNumber = Optional.empty();
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
localNumber = Util.getDeviceNumber(context);
} else {
Log.i(TAG, "No phone permission");
}
if (localNumber.isPresent()) {
Log.i(TAG, "Phone number detected");
Phonenumber.PhoneNumber phoneNumber = localNumber.get();
String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
} else {
Log.i(TAG, "No number detected");
Optional<String> simCountryIso = Util.getSimCountryIso(context);
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
}
}
}
private void onTermsClicked() {
CommunicationActions.openBrowserLink(requireContext(), RegistrationConstants.TERMS_AND_CONDITIONS_URL);
}
private boolean canUserSelectBackup() {
return BackupUtil.isUserSelectionRequired(requireContext()) &&
!viewModel.isReregister() &&
!SignalStore.settings().isBackupEnabled();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 Signal Messenger, LLC
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui
package org.thoughtcrime.securesms.registration.ui
import android.content.Context
import android.content.Intent
@@ -23,19 +23,19 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* Activity to hold the entire registration process.
*/
class RegistrationV2Activity : BaseActivity() {
class RegistrationActivity : BaseActivity() {
private val TAG = Log.tag(RegistrationV2Activity::class.java)
private val TAG = Log.tag(RegistrationActivity::class.java)
private val dynamicTheme = DynamicNoActionBarTheme()
val sharedViewModel: RegistrationV2ViewModel by viewModels()
val sharedViewModel: RegistrationViewModel by viewModels()
private var smsRetrieverReceiver: SmsRetrieverReceiver? = null
@@ -49,6 +49,8 @@ class RegistrationV2Activity : BaseActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration_navigation_v2)
sharedViewModel.isReregister = intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false)
sharedViewModel.checkpoint.observe(this) {
if (it >= RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE) {
handleSuccessfulVerify()
@@ -85,11 +87,11 @@ class RegistrationV2Activity : BaseActivity() {
val startIntent = MainActivity.clearTop(this).apply {
if (needsPin) {
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity))
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationActivity))
} else if (!SignalStore.registration.hasSkippedTransferOrRestore() && RemoteConfig.messageBackups) {
putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationV2Activity))
putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationActivity))
} else if (needsProfile) {
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity))
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationActivity))
}
}
@@ -113,12 +115,21 @@ class RegistrationV2Activity : BaseActivity() {
}
companion object {
const val RE_REGISTRATION_EXTRA: String = "re_registration"
@JvmStatic
fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent {
return Intent(context, RegistrationV2Activity::class.java).apply {
return Intent(context, RegistrationActivity::class.java).apply {
putExtra(RE_REGISTRATION_EXTRA, false)
setData(originalIntent.data)
}
}
@JvmStatic
fun newIntentForReRegistration(context: Context): Intent {
return Intent(context, RegistrationActivity::class.java).apply {
putExtra(RE_REGISTRATION_EXTRA, true)
}
}
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui
package org.thoughtcrime.securesms.registration.ui
/**
* An ordered list of checkpoints of the registration process.

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui
package org.thoughtcrime.securesms.registration.ui
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber

View File

@@ -3,21 +3,21 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui
package org.thoughtcrime.securesms.registration.ui
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
/**
* State holder shared across all of registration.
*/
data class RegistrationV2State(
data class RegistrationState(
val sessionId: String? = null,
val enteredCode: String = "",
val phoneNumber: Phonenumber.PhoneNumber? = fetchExistingE164FromValues(),
@@ -50,7 +50,7 @@ data class RegistrationV2State(
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
companion object {
private val TAG = Log.tag(RegistrationV2State::class)
private val TAG = Log.tag(RegistrationState::class)
private fun fetchExistingE164FromValues(): Phonenumber.PhoneNumber? {
val existingE164 = SignalStore.registration.sessionE164

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui
package org.thoughtcrime.securesms.registration.ui
import android.Manifest
import android.content.Context
@@ -36,29 +36,29 @@ import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.pin.SvrWrongPinException
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AlreadyVerified
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ExternalServiceFailure
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ImpossibleNumber
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MalformedRequest
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NoSuchSession
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NonNormalizedNumber
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RateLimited
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RegistrationLocked
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AttemptsExhausted
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ExternalServiceFailure
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ImpossibleNumber
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MalformedRequest
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MustRetry
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NoSuchSession
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NonNormalizedNumber
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RateLimited
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RegistrationLocked
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
@@ -74,9 +74,9 @@ import kotlin.time.Duration.Companion.minutes
/**
* ViewModel shared across all of registration.
*/
class RegistrationV2ViewModel : ViewModel() {
class RegistrationViewModel : ViewModel() {
private val store = MutableStateFlow(RegistrationV2State())
private val store = MutableStateFlow(RegistrationState())
private val password = Util.getSecret(18)
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
@@ -102,8 +102,13 @@ class RegistrationV2ViewModel : ViewModel() {
val svrTriesRemaining: Int
get() = store.value.svrTriesRemaining
val isReregister: Boolean
var isReregister: Boolean
get() = store.value.isReRegister
set(value) {
store.update {
it.copy(isReRegister = value)
}
}
val phoneNumber: Phonenumber.PhoneNumber?
get() = store.value.phoneNumber
@@ -857,7 +862,7 @@ class RegistrationV2ViewModel : ViewModel() {
}
companion object {
private val TAG = Log.tag(RegistrationV2ViewModel::class.java)
private val TAG = Log.tag(RegistrationViewModel::class.java)
private suspend fun restoreBackupTier() = withContext(Dispatchers.IO) {
val startTime = System.currentTimeMillis()

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.accountlocked
package org.thoughtcrime.securesms.registration.ui.accountlocked
import android.content.Intent
import android.net.Uri
@@ -15,14 +15,14 @@ import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import kotlin.time.Duration.Companion.milliseconds
/**
* Screen educating the user that they need to wait some number of days to register.
*/
class AccountLockedV2Fragment : LoggingFragment(R.layout.account_locked_fragment) {
private val viewModel by activityViewModels<RegistrationV2ViewModel>()
class AccountLockedFragment : LoggingFragment(R.layout.account_locked_fragment) {
private val viewModel by activityViewModels<RegistrationViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.captcha
package org.thoughtcrime.securesms.registration.ui.captcha
import android.annotation.SuppressLint
import android.os.Bundle
@@ -17,12 +17,12 @@ import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaV2Binding
import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaBinding
import org.thoughtcrime.securesms.registration.fragments.RegistrationConstants
abstract class CaptchaV2Fragment : LoggingFragment(R.layout.fragment_registration_captcha_v2) {
abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_captcha) {
private val binding: FragmentRegistrationCaptchaV2Binding by ViewBinderDelegate(FragmentRegistrationCaptchaV2Binding::bind)
private val binding: FragmentRegistrationCaptchaBinding by ViewBinderDelegate(FragmentRegistrationCaptchaBinding::bind)
private val backListener = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {

View File

@@ -3,22 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.captcha
package org.thoughtcrime.securesms.registration.ui.captcha
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
/**
* Screen that displays a captcha as part of the registration flow.
* This subclass plugs in [RegistrationV2ViewModel] to the shared super class.
* This subclass plugs in [RegistrationViewModel] to the shared super class.
*
* @see CaptchaV2Fragment
* @see CaptchaFragment
*/
class RegistrationCaptchaV2Fragment : CaptchaV2Fragment() {
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
class RegistrationCaptchaFragment : CaptchaFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.addPresentedChallenge(Challenge.CAPTCHA)

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.entercode
package org.thoughtcrime.securesms.registration.ui.entercode
import android.content.DialogInterface
import android.os.Bundle
@@ -21,16 +21,16 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
@@ -38,16 +38,16 @@ import org.thoughtcrime.securesms.util.visible
/**
* The final screen of account registration, where the user enters their verification code.
*/
class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_code_v2) {
class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) {
companion object {
private const val BOTTOM_SHEET_TAG = "support_bottom_sheet"
}
private val TAG = Log.tag(EnterCodeV2Fragment::class.java)
private val TAG = Log.tag(EnterCodeFragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val binding: FragmentRegistrationEnterCodeV2Binding by ViewBinderDelegate(FragmentRegistrationEnterCodeV2Binding::bind)
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val binding: FragmentRegistrationEnterCodeBinding by ViewBinderDelegate(FragmentRegistrationEnterCodeBinding::bind)
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
@@ -158,7 +158,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionAccountLocked())
findNavController().safeNavigate(EnterCodeFragmentDirections.actionAccountLocked())
}
}
)
@@ -168,7 +168,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionRequireKbsLockPin(timeRemaining))
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining))
}
}
)

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
package org.thoughtcrime.securesms.registration.ui.grantpermissions
import android.app.Activity
import android.content.pm.PackageManager
@@ -25,8 +25,8 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -35,10 +35,10 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
* Screen in account registration that provides rationales for the suggested runtime permissions.
*/
@RequiresApi(23)
class GrantPermissionsV2Fragment : ComposeFragment() {
class GrantPermissionsFragment : ComposeFragment() {
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val args by navArgs<GrantPermissionsFragmentArgs>()
private val isSearchingForBackup = mutableStateOf(false)
private val requestPermissionLauncher = registerForActivityResult(
@@ -50,7 +50,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
when (val resultCode = result.resultCode) {
Activity.RESULT_OK -> {
sharedViewModel.onBackupSuccessfullyRestored()
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
}
Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.")
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
@@ -102,7 +102,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
private fun proceedToNextScreen() {
when (welcomeAction) {
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
WelcomeAction.RESTORE_BACKUP -> {
val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
launchRestoreActivity.launch(restoreIntent)
@@ -120,6 +120,6 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
}
companion object {
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
private val TAG = Log.tag(GrantPermissionsFragment::class.java)
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.content.Context
import android.content.DialogInterface
@@ -40,19 +40,19 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberV2Binding
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberBinding
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationState
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.v2.ui.toE164
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.PlayServicesUtil
@@ -67,12 +67,12 @@ import kotlin.time.Duration.Companion.milliseconds
/**
* Screen in registration where the user enters their phone number.
*/
class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number_v2) {
class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number) {
private val TAG = Log.tag(EnterPhoneNumberV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val fragmentViewModel by viewModels<EnterPhoneNumberV2ViewModel>()
private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind)
private val TAG = Log.tag(EnterPhoneNumberFragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val fragmentViewModel by viewModels<EnterPhoneNumberViewModel>()
private val binding: FragmentRegistrationEnterPhoneNumberBinding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberBinding::bind)
private val skipToNextScreen: DialogInterface.OnClickListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> moveToVerificationEntryScreen() }
@@ -139,7 +139,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
sharedViewModel.setPhoneNumber(null)
}
if (fragmentState.error != EnterPhoneNumberV2State.Error.NONE) {
if (fragmentState.error != EnterPhoneNumberState.Error.NONE) {
presentLocalError(fragmentState)
}
}
@@ -237,7 +237,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
}
private fun presentRegisterButton(sharedState: RegistrationV2State) {
private fun presentRegisterButton(sharedState: RegistrationState) {
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber)
if (sharedState.inProgress) {
binding.registerButton.setSpinning()
@@ -246,11 +246,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
}
private fun presentLocalError(state: EnterPhoneNumberV2State) {
private fun presentLocalError(state: EnterPhoneNumberState) {
when (state.error) {
EnterPhoneNumberV2State.Error.NONE -> Unit
EnterPhoneNumberState.Error.NONE -> Unit
EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER -> {
EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_invalid_number)
setMessage(
@@ -266,15 +266,15 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING -> {
EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING -> {
handlePromptForNoPlayServices()
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show()
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT -> {
EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_play_services_error)
setMessage(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)
@@ -340,7 +340,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
private fun moveToCaptcha() {
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionRequestCaptcha())
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha())
}
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
@@ -400,7 +400,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
sharedViewModel.uiState.value?.let { value ->
val now = System.currentTimeMillis()
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)
} else if (now < value.nextSmsTimestamp) {
moveToVerificationEntryScreen()
@@ -411,9 +411,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
}
private fun onFcmTokenRetrieved(value: RegistrationV2State) {
private fun onFcmTokenRetrieved(value: RegistrationState) {
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)
} else {
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = false)
@@ -440,23 +440,23 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
PlayServicesUtil.PlayServicesStatus.MISSING -> {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
return false
}
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE)
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE)
return false
}
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT)
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT)
return false
}
null -> {
Log.w(TAG, "Null result received from PlayServicesUtil, marking Play Services as missing.")
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
return false
}
}
@@ -516,12 +516,12 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
private fun moveToEnterPinScreen() {
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment())
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionReRegisterWithPinV2Fragment())
sharedViewModel.setInProgress(false)
}
private fun moveToVerificationEntryScreen() {
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode())
sharedViewModel.setInProgress(false)
}
@@ -530,8 +530,8 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
findNavController().popBackStack()
}
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationV2State>(sharedViewModel.uiState) {
override fun onValue(value: RegistrationV2State): Boolean {
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationState>(sharedViewModel.uiState) {
override fun onValue(value: RegistrationState): Boolean {
val fcmRetrieved = value.isFcmSupported
if (fcmRetrieved) {
onFcmTokenRetrieved(value)
@@ -547,7 +547,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return if (menuItem.itemId == R.id.phone_menu_use_proxy) {
NavHostFragment.findNavController(this@EnterPhoneNumberV2Fragment).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEditProxy())
NavHostFragment.findNavController(this@EnterPhoneNumberFragment).safeNavigate(EnterPhoneNumberFragmentDirections.actionEditProxy())
true
} else {
false

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.text.TextWatcher
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
/**
* State holder for the phone number entry screen, including phone number and Play Services errors.
*/
data class EnterPhoneNumberState(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) {
enum class Error {
NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.telephony.PhoneNumberFormattingTextWatcher
import android.text.TextWatcher
@@ -19,17 +19,17 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
/**
* ViewModel for the phone number entry screen.
*/
class EnterPhoneNumberV2ViewModel : ViewModel() {
class EnterPhoneNumberViewModel : ViewModel() {
private val TAG = Log.tag(EnterPhoneNumberV2ViewModel::class.java)
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
private val store = MutableStateFlow(EnterPhoneNumberV2State())
private val store = MutableStateFlow(EnterPhoneNumberState())
val uiState = store.asLiveData()
val formatter: TextWatcher?
@@ -85,11 +85,11 @@ class EnterPhoneNumberV2ViewModel : ViewModel() {
}
}
fun parsePhoneNumber(state: EnterPhoneNumberV2State): PhoneNumber {
fun parsePhoneNumber(state: EnterPhoneNumberState): PhoneNumber {
return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode)
}
fun isEnteredNumberValid(state: EnterPhoneNumberV2State): Boolean {
fun isEnteredNumberValid(state: EnterPhoneNumberState): Boolean {
return try {
PhoneNumberUtil.getInstance().isValidNumber(parsePhoneNumber(state))
} catch (ex: NumberParseException) {
@@ -114,10 +114,10 @@ class EnterPhoneNumberV2ViewModel : ViewModel() {
}
fun clearError() {
setError(EnterPhoneNumberV2State.Error.NONE)
setError(EnterPhoneNumberState.Error.NONE)
}
fun setError(error: EnterPhoneNumberV2State.Error) {
fun setError(error: EnterPhoneNumberState.Error) {
store.update {
it.copy(error = error)
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.registrationlock
package org.thoughtcrime.securesms.registration.ui.registrationlock
import android.os.Bundle
import android.text.InputType
@@ -23,25 +23,25 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.concurrent.TimeUnit
class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registration_lock) {
class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_lock) {
companion object {
private val TAG = Log.tag(RegistrationLockV2Fragment::class.java)
private val TAG = Log.tag(RegistrationLockFragment::class.java)
}
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind)
private val viewModel by activityViewModels<RegistrationV2ViewModel>()
private val viewModel by activityViewModels<RegistrationViewModel>()
private var timeRemaining: Long = 0
@@ -49,7 +49,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
val args: RegistrationLockV2FragmentArgs = RegistrationLockV2FragmentArgs.fromBundle(requireArguments())
val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments())
timeRemaining = args.getTimeRemaining()
@@ -140,7 +140,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
is VerificationCodeRequestResult.Success -> Unit
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
is VerificationCodeRequestResult.AttemptsExhausted -> {
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
}
is VerificationCodeRequestResult.RegistrationLocked -> {
@@ -162,7 +162,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
is RegisterAccountResult.Success -> Unit
is RegisterAccountResult.RateLimited -> onRateLimited()
is RegisterAccountResult.AttemptsExhausted -> {
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
}
is RegisterAccountResult.RegistrationLocked -> {
@@ -174,7 +174,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
is RegisterAccountResult.SvrWrongPin -> onIncorrectKbsRegistrationLockPin(result.triesRemaining)
is RegisterAccountResult.SvrNoData -> {
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
}
else -> {
@@ -191,7 +191,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
if (svrTriesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.")
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
return
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
import android.os.Bundle
import android.text.InputType
@@ -21,23 +21,23 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationPinRestoreEntryV2Binding
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationState
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
companion object {
private val TAG = Log.tag(ReRegisterWithPinV2Fragment::class.java)
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
}
private val registrationViewModel by activityViewModels<RegistrationV2ViewModel>()
private val reRegisterViewModel by viewModels<ReRegisterWithPinV2ViewModel>()
private val registrationViewModel by activityViewModels<RegistrationViewModel>()
private val reRegisterViewModel by viewModels<ReRegisterWithPinViewModel>()
private val binding: FragmentRegistrationPinRestoreEntryV2Binding by ViewBinderDelegate(FragmentRegistrationPinRestoreEntryV2Binding::bind)
@@ -79,11 +79,11 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati
registrationViewModel.uiState.observe(viewLifecycleOwner, ::updateViewState)
}
private fun updateViewState(state: RegistrationV2State) {
private fun updateViewState(state: RegistrationState) {
if (state.networkError != null) {
genericErrorDialog()
} else if (!state.canSkipSms) {
findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
} else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) {
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
onAccountLocked()
@@ -263,7 +263,7 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati
is RegisterAccountResult.IncorrectRecoveryPassword -> {
registrationViewModel.setUserSkippedReRegisterFlow(true)
findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
}
is RegisterAccountResult.AttemptsExhausted,

View File

@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
data class ReRegisterWithPinV2State(
data class ReRegisterWithPinState(
val isLocalVerification: Boolean = false,
val hasIncorrectGuess: Boolean = false,
val localPinMatches: Boolean = false

View File

@@ -3,19 +3,19 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.signal.core.util.logging.Log
class ReRegisterWithPinV2ViewModel : ViewModel() {
class ReRegisterWithPinViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(ReRegisterWithPinV2ViewModel::class.java)
private val TAG = Log.tag(ReRegisterWithPinViewModel::class.java)
}
private val store = MutableStateFlow(ReRegisterWithPinV2State())
private val store = MutableStateFlow(ReRegisterWithPinState())
val isLocalVerification: Boolean
get() = store.value.isLocalVerification

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.restore
package org.thoughtcrime.securesms.registration.ui.restore
import android.content.Context
import android.content.Intent

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.welcome
package org.thoughtcrime.securesms.registration.ui.welcome
import android.app.Activity
import android.content.pm.PackageManager
@@ -18,13 +18,13 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Binding
import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeBinding
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.grantpermissions.GrantPermissionsFragment
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.CommunicationActions
@@ -36,15 +36,15 @@ import org.thoughtcrime.securesms.util.visible
/**
* First screen that is displayed on the very first app launch.
*/
class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome_v2) {
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome) {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val binding: FragmentRegistrationWelcomeBinding by ViewBinderDelegate(FragmentRegistrationWelcomeBinding::bind)
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
when (val resultCode = result.resultCode) {
Activity.RESULT_OK -> {
sharedViewModel.onBackupSuccessfullyRestored()
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionGoToRegistration())
findNavController().safeNavigate(WelcomeFragmentDirections.actionGoToRegistration())
}
Activity.RESULT_CANCELED -> {
Log.w(TAG, "Backup restoration canceled.")
@@ -67,10 +67,10 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
private fun onContinueClicked() {
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsFragment.WelcomeAction.CONTINUE))
} else {
sharedViewModel.maybePrefillE164(requireContext())
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
findNavController().safeNavigate(WelcomeFragmentDirections.actionSkipRestore())
}
}
@@ -85,7 +85,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
private fun onTransferOrRestoreClicked() {
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.RESTORE_BACKUP))
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP))
} else {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
@@ -95,7 +95,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
}
companion object {
private val TAG = Log.tag(WelcomeV2Fragment::class.java)
private val TAG = Log.tag(WelcomeFragment::class.java)
private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"
}
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.util
data class CountryPrefix(val digits: Int, val regionCode: String) {
override fun toString(): String {
return "+$digits"
}
}

View File

@@ -1,179 +0,0 @@
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.toString() }
private val spinnerAdapter: ArrayAdapter<CountryPrefix> = ArrayAdapter<CountryPrefix>(context, R.layout.registration_country_code_dropdown_item, supportedCountryPrefixes)
private val countryCodeEntryListener = CountryCodeEntryListener()
private var countryFormatter: AsYouTypeFormatter? = null
private var isUpdating = true
init {
setUpNumberInput()
spinnerView.threshold = 100
spinnerView.setAdapter(spinnerAdapter)
spinnerView.addTextChangedListener(countryCodeEntryListener)
}
fun prepopulateCountryCode() {
if (spinnerView.editableText.isBlank()) {
spinnerView.setText(supportedCountryPrefixes[0].toString())
}
}
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 setNumberAndCountryCode(numberViewState: NumberViewState) {
val countryCode = numberViewState.countryCode
isUpdating = true
phoneNumberInputLayout.setText(numberViewState.nationalNumber)
if (numberViewState.countryCode != 0) {
spinnerView.setText(supportedCountryPrefixes.first { it.digits == numberViewState.countryCode }.toString())
}
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode)
setCountryFormatter(regionCode)
isUpdating = false
}
fun updateNumberFormatter(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"
}
}

View File

@@ -1,18 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
import android.text.TextWatcher
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
/**
* State holder for the phone number entry screen, including phone number and Play Services errors.
*/
data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) {
enum class Error {
NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT
}
}

View File

@@ -1,4 +0,0 @@
package org.thoughtcrime.securesms.registration.viewmodel;
public final class BaseEnterCodeViewModelDelegate {
}

View File

@@ -1,397 +0,0 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import org.thoughtcrime.securesms.registration.VerifyResponse;
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
import org.thoughtcrime.securesms.registration.VerifyResponseHitRegistrationLock;
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
import org.whispersystems.signalservice.internal.ServiceResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
/**
* Base view model used in registration and change number flow. Handles the storage of all data
* shared between the two flows, orchestrating verification, and calling to subclasses to perform
* the specific verify operations for each flow.
*/
public abstract class BaseRegistrationViewModel extends ViewModel {
private static final String TAG = Log.tag(BaseRegistrationViewModel.class);
private static final String STATE_NUMBER = "NUMBER";
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
private static final String STATE_CAPTCHA = "CAPTCHA";
private static final String STATE_PUSH_TIMED_OUT = "PUSH_TIMED_OUT";
private static final String STATE_INCORRECT_CODE_ATTEMPTS = "STATE_INCORRECT_CODE_ATTEMPTS";
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
private static final String STATE_SVR_AUTH = "SVR_AUTH";
private static final String STATE_SVR_TRIES_REMAINING = "SVR_TRIES_REMAINING";
private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME";
private static final String STATE_CAN_SMS_AT_TIME = "CAN_SMS_AT_TIME";
private static final String STATE_RECOVERY_PASSWORD = "RECOVERY_PASSWORD";
protected final SavedStateHandle savedState;
protected final VerifyAccountRepository verifyAccountRepository;
public BaseRegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
@NonNull VerifyAccountRepository verifyAccountRepository,
@NonNull String password)
{
this.savedState = savedStateHandle;
this.verifyAccountRepository = verifyAccountRepository;
setInitialDefaultValue(STATE_NUMBER, NumberViewState.INITIAL);
setInitialDefaultValue(STATE_REGISTRATION_SECRET, password);
setInitialDefaultValue(STATE_VERIFICATION_CODE, "");
setInitialDefaultValue(STATE_INCORRECT_CODE_ATTEMPTS, 0);
setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.svr().getRecoveryPassword());
setInitialDefaultValue(STATE_PUSH_TIMED_OUT, false);
}
protected <T> void setInitialDefaultValue(@NonNull String key, @Nullable T initialValue) {
if (!savedState.contains(key) || savedState.get(key) == null) {
savedState.set(key, initialValue);
}
}
public @Nullable String getSessionId() {
return SignalStore.registration().getSessionId();
}
public void setSessionId(String sessionId) {
SignalStore.registration().setSessionId(sessionId);
}
public @Nullable String getSessionE164() {
return SignalStore.registration().getSessionE164();
}
public void setSessionE164(String sessionE164) {
SignalStore.registration().setSessionE164(sessionE164);
}
public void resetSession() {
setSessionE164(null);
setSessionId(null);
}
public @NonNull NumberViewState getNumber() {
//noinspection ConstantConditions
return savedState.get(STATE_NUMBER);
}
public @NonNull LiveData<NumberViewState> getLiveNumber() {
return savedState.getLiveData(STATE_NUMBER);
}
public void restorePhoneNumberStateFromE164(String e164) throws NumberParseException {
Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(e164, null);
onCountrySelected(null, phoneNumber.getCountryCode());
setNationalNumber(String.valueOf(phoneNumber.getNationalNumber()));
}
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
setViewState(getNumber().toBuilder()
.selectedCountryDisplayName(selectedCountryName)
.countryCode(countryCode)
.build());
}
public void setNationalNumber(String number) {
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
setViewState(numberViewState);
}
protected void setViewState(NumberViewState numberViewState) {
if (!numberViewState.equals(getNumber())) {
savedState.set(STATE_NUMBER, numberViewState);
}
}
public @NonNull String getRegistrationSecret() {
//noinspection ConstantConditions
return savedState.get(STATE_REGISTRATION_SECRET);
}
public @NonNull String getTextCodeEntered() {
//noinspection ConstantConditions
return savedState.get(STATE_VERIFICATION_CODE);
}
public @Nullable String getCaptchaToken() {
return savedState.get(STATE_CAPTCHA);
}
public boolean hasCaptchaToken() {
return getCaptchaToken() != null;
}
public void setCaptchaResponse(@Nullable String captchaToken) {
savedState.set(STATE_CAPTCHA, captchaToken);
}
public void clearCaptchaResponse() {
setCaptchaResponse(null);
}
public void onVerificationCodeEntered(String code) {
savedState.set(STATE_VERIFICATION_CODE, code);
}
public void incrementIncorrectCodeAttempts() {
//noinspection ConstantConditions
savedState.set(STATE_INCORRECT_CODE_ATTEMPTS, (Integer) savedState.get(STATE_INCORRECT_CODE_ATTEMPTS) + 1);
}
public LiveData<Integer> getIncorrectCodeAttempts() {
return savedState.getLiveData(STATE_INCORRECT_CODE_ATTEMPTS, 0);
}
public void markPushChallengeTimedOut() {
savedState.set(STATE_PUSH_TIMED_OUT, true);
}
public List<String> getExcludedChallenges() {
ArrayList<String> challengeKeys = new ArrayList<>();
if (Boolean.TRUE.equals(savedState.get(STATE_PUSH_TIMED_OUT))) {
challengeKeys.add(RegistrationSessionProcessor.PUSH_CHALLENGE_KEY);
}
return challengeKeys;
}
protected void setSvrAuthCredentials(SvrAuthCredentialSet credentials) {
savedState.set(STATE_SVR_AUTH, credentials);
}
protected @Nullable SvrAuthCredentialSet getSvrAuthCredentials() {
return savedState.get(STATE_SVR_AUTH);
}
public @Nullable Integer getSvrTriesRemaining() {
return savedState.get(STATE_SVR_TRIES_REMAINING);
}
public void setSvrTriesRemaining(@Nullable Integer triesRemaining) {
savedState.set(STATE_SVR_TRIES_REMAINING, triesRemaining);
}
public void setRecoveryPassword(@Nullable String recoveryPassword) {
savedState.set(STATE_RECOVERY_PASSWORD, recoveryPassword);
}
public @Nullable String getRecoveryPassword() {
return savedState.get(STATE_RECOVERY_PASSWORD);
}
public LiveData<Long> getLockedTimeRemaining() {
return savedState.getLiveData(STATE_TIME_REMAINING, 0L);
}
public LiveData<Long> getCanCallAtTime() {
return savedState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L);
}
public LiveData<Long> getCanSmsAtTime() {
return savedState.getLiveData(STATE_CAN_SMS_AT_TIME, 0L);
}
public void setLockedTimeRemaining(long lockedTimeRemaining) {
savedState.set(STATE_TIME_REMAINING, lockedTimeRemaining);
}
public void setCanCallAtTime(long callingTimestamp) {
savedState.getLiveData(STATE_CAN_CALL_AT_TIME).postValue(callingTimestamp);
}
public void setCanSmsAtTime(long smsTimestamp) {
savedState.getLiveData(STATE_CAN_SMS_AT_TIME).postValue(smsTimestamp);
}
public Single<RegistrationSessionProcessor> requestVerificationCode(@NonNull Mode mode, @Nullable String mcc, @Nullable String mnc) {
final String e164 = getNumber().getE164Number();
return getValidSession(e164, mcc, mnc)
.flatMap(processor -> {
if (!processor.hasResult()) {
return Single.just(processor);
}
String sessionId = processor.getSessionId();
setSessionId(sessionId);
setSessionE164(e164);
return handleRequiredChallenges(processor, e164);
})
.flatMap(processor -> {
if (!processor.hasResult()) {
return Single.just(processor);
}
if (!processor.isAllowedToRequestCode()) {
return Single.just(processor);
}
String sessionId = processor.getSessionId();
clearCaptchaResponse();
return verifyAccountRepository.requestVerificationCode(sessionId,
getNumber().getE164Number(),
getRegistrationSecret(),
mode)
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new);
})
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess((RegistrationSessionProcessor processor) -> {
if (processor.hasResult() && processor.isAllowedToRequestCode()) {
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
}
});
}
public Single<RegistrationSessionProcessor.RegistrationSessionProcessorForSession> validateSession(String e164) {
String storedSessionId = null;
if (e164.equals(getSessionE164())) {
storedSessionId = getSessionId();
}
return verifyAccountRepository.validateSession(storedSessionId, e164, getRegistrationSecret())
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
}
public Single<RegistrationSessionProcessor.RegistrationSessionProcessorForSession> getValidSession(String e164, @Nullable String mcc, @Nullable String mnc) {
return validateSession(e164)
.flatMap(processor -> {
if (processor.isInvalidSession()) {
return verifyAccountRepository.requestValidSession(e164, getRegistrationSecret(), mcc, mnc)
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new)
.doOnSuccess(createSessionProcessor -> {
if (createSessionProcessor.pushChallengeTimedOut()) {
Log.w(TAG, "Registration push challenge timed out.");
markPushChallengeTimedOut();
}
});
} else {
return Single.just(processor);
}
});
}
public Single<RegistrationSessionProcessor> handleRequiredChallenges(RegistrationSessionProcessor processor, String e164) {
final String sessionId = processor.getSessionId();
if (processor.isAllowedToRequestCode()) {
Log.d(TAG, "All challenges satisfied.");
return Single.just(processor);
}
if (hasCaptchaToken() && processor.captchaRequired(getExcludedChallenges())) {
Log.d(TAG, "Submitting completed captcha challenge");
final String captcha = Objects.requireNonNull(getCaptchaToken());
clearCaptchaResponse();
return verifyAccountRepository.verifyCaptcha(sessionId, captcha, e164, getRegistrationSecret())
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
} else {
String challenge = processor.getChallenge(getExcludedChallenges());
Log.d(TAG, "Handling challenge of type " + challenge);
if (challenge != null) {
switch (challenge) {
case RegistrationSessionProcessor.PUSH_CHALLENGE_KEY:
return verifyAccountRepository.requestAndVerifyPushToken(sessionId,
getNumber().getE164Number(),
getRegistrationSecret())
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
case RegistrationSessionProcessor.CAPTCHA_KEY:
// fall through to passing the processor back so that the eventual subscriber will check captchaRequired() and handle accordingly
default:
break;
}
}
}
return Single.just(processor);
}
public Single<VerifyResponseProcessor> verifyCodeWithoutRegistrationLock(@NonNull String code) {
onVerificationCodeEntered(code);
return verifyAccountWithoutRegistrationLock()
.flatMap(response -> {
if (response.getResult().isPresent() && response.getResult().get().getMasterKey() != null) {
return onVerifySuccessWithRegistrationLock(new VerifyResponseWithRegistrationLockProcessor(response, null), response.getResult().get().getPin());
}
VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(response);
if (processor.hasResult()) {
return onVerifySuccess(processor);
} else if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) {
return Single.just(new VerifyResponseHitRegistrationLock(processor.getResponse()));
}
return Single.just(processor);
})
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(processor -> {
if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) {
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
setSvrTriesRemaining(processor.getSvrTriesRemaining());
setSvrAuthCredentials(processor.getSvrAuthCredentials());
} else if (processor.isRegistrationLockPresentAndSvrExhausted()) {
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
}
});
}
public Single<VerifyResponseWithRegistrationLockProcessor> verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) {
SvrAuthCredentialSet authCredentials = Objects.requireNonNull(getSvrAuthCredentials());
return verifyAccountWithRegistrationLock(pin, authCredentials)
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, authCredentials))
.flatMap(processor -> {
if (processor.hasResult()) {
return onVerifySuccessWithRegistrationLock(processor, pin);
} else if (processor.wrongPin()) {
return Single.just(new VerifyResponseWithRegistrationLockProcessor(processor.getResponse(), authCredentials));
}
return Single.just(processor);
})
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(processor -> {
if (processor.wrongPin()) {
setSvrTriesRemaining(processor.getSvrTriesRemaining());
}
});
}
protected abstract Single<ServiceResponse<VerifyResponse>> verifyAccountWithoutRegistrationLock();
protected abstract Single<ServiceResponse<VerifyResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull SvrAuthCredentialSet svrAuthCredentials);
protected abstract Single<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor);
protected abstract Single<VerifyResponseWithRegistrationLockProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin);
}

View File

@@ -1,104 +0,0 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public final class LocalCodeRequestRateLimiter implements Parcelable {
private final long timePeriod;
private final Map<Mode, Data> dataMap;
public LocalCodeRequestRateLimiter(long timePeriod) {
this.timePeriod = timePeriod;
this.dataMap = new HashMap<>();
}
@MainThread
public boolean canRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
Data data = dataMap.get(mode);
return data == null || !data.limited(e164Number, currentTime);
}
/**
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
*/
@MainThread
public void onSuccessfulRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
}
/**
* Call this if a mode was unsuccessful in sending.
*/
@MainThread
public void onUnsuccessfulRequest() {
dataMap.clear();
}
static class Data {
final String e164Number;
final long limitedUntil;
Data(@NonNull String e164Number, long limitedUntil) {
this.e164Number = e164Number;
this.limitedUntil = limitedUntil;
}
boolean limited(String e164Number, long currentTime) {
return this.e164Number.equals(e164Number) && currentTime < limitedUntil;
}
}
public static final Creator<LocalCodeRequestRateLimiter> CREATOR = new Creator<LocalCodeRequestRateLimiter>() {
@Override
public LocalCodeRequestRateLimiter createFromParcel(Parcel in) {
long timePeriod = in.readLong();
int numberOfMapEntries = in.readInt();
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
for (int i = 0; i < numberOfMapEntries; i++) {
Mode mode = Mode.values()[in.readInt()];
String e164Number = in.readString();
long limitedUntil = in.readLong();
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
}
return localCodeRequestRateLimiter;
}
@Override
public LocalCodeRequestRateLimiter[] newArray(int size) {
return new LocalCodeRequestRateLimiter[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(timePeriod);
dest.writeInt(dataMap.size());
for (Map.Entry<Mode, Data> a : dataMap.entrySet()) {
dest.writeInt(a.getKey().ordinal());
dest.writeString(a.getValue().e164Number);
dest.writeLong(a.getValue().limitedUntil);
}
}
}

View File

@@ -1,31 +0,0 @@
package org.thoughtcrime.securesms.registration.viewmodel
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.BehaviorSubject
/**
* Used during re-registration flow when pin entry is required to skip SMS verification. Mostly tracks
* guesses remaining in both the local and remote check flows.
*/
class ReRegisterWithPinViewModel : ViewModel() {
var isLocalVerification: Boolean = false
private set
var hasIncorrectGuess: Boolean = false
private val _triesRemaining: BehaviorSubject<Int> = BehaviorSubject.createDefault(10)
val triesRemaining: Observable<Int> = _triesRemaining.observeOn(AndroidSchedulers.mainThread())
fun updateSvrTriesRemaining(triesRemaining: Int?) {
if (triesRemaining == null) {
isLocalVerification = true
if (hasIncorrectGuess) {
_triesRemaining.onNext((_triesRemaining.value!! - 1).coerceAtLeast(0))
}
} else {
_triesRemaining.onNext(triesRemaining)
}
}
}

View File

@@ -1,458 +0,0 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import androidx.savedstate.SavedStateRegistryOwner;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.pin.SvrWrongPinException;
import org.thoughtcrime.securesms.pin.SvrRepository;
import org.thoughtcrime.securesms.registration.RegistrationData;
import org.thoughtcrime.securesms.registration.RegistrationRepository;
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.VerifyResponse;
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SvrNoDataException;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException;
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
import org.signal.core.util.Base64;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class RegistrationViewModel extends BaseRegistrationViewModel {
private static final String TAG = Log.tag(RegistrationViewModel.class);
private static final String STATE_FCM_TOKEN = "FCM_TOKEN";
private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN";
private static final String STATE_IS_REREGISTER = "IS_REREGISTER";
private static final String STATE_BACKUP_COMPLETED = "BACKUP_COMPLETED";
private final RegistrationRepository registrationRepository;
private boolean userSkippedReRegisterFlow = false;
private boolean autoShowSmsConfirmDialog = false;
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
boolean isReregister,
@NonNull VerifyAccountRepository verifyAccountRepository,
@NonNull RegistrationRepository registrationRepository)
{
super(savedStateHandle, verifyAccountRepository, Util.getSecret(18));
this.registrationRepository = registrationRepository;
setInitialDefaultValue(STATE_RESTORE_FLOW_SHOWN, false);
setInitialDefaultValue(STATE_BACKUP_COMPLETED, false);
this.savedState.set(STATE_IS_REREGISTER, isReregister);
}
public boolean isReregister() {
//noinspection ConstantConditions
return savedState.get(STATE_IS_REREGISTER);
}
public void onNumberDetected(int countryCode, String nationalNumber) {
setViewState(getNumber().toBuilder()
.countryCode(countryCode)
.nationalNumber(nationalNumber)
.build());
}
public @Nullable String getFcmToken() {
String token = savedState.get(STATE_FCM_TOKEN);
if (token == null || token.isEmpty()) {
return null;
}
return token;
}
@MainThread
public void setFcmToken(@Nullable String fcmToken) {
savedState.set(STATE_FCM_TOKEN, fcmToken);
}
public void setWelcomeSkippedOnRestore() {
savedState.set(STATE_RESTORE_FLOW_SHOWN, true);
}
public boolean hasRestoreFlowBeenShown() {
//noinspection ConstantConditions
return savedState.get(STATE_RESTORE_FLOW_SHOWN);
}
public void setIsReregister(boolean isReregister) {
savedState.set(STATE_IS_REREGISTER, isReregister);
}
public void markBackupCompleted() {
savedState.set(STATE_BACKUP_COMPLETED, true);
}
public boolean hasBackupCompleted() {
Boolean completed = savedState.get(STATE_BACKUP_COMPLETED);
return completed != null ? completed : false;
}
public boolean hasUserSkippedReRegisterFlow() {
return userSkippedReRegisterFlow;
}
public void setUserSkippedReRegisterFlow(boolean userSkippedReRegisterFlow) {
Log.i(TAG, "User skipped re-register flow.");
this.userSkippedReRegisterFlow = userSkippedReRegisterFlow;
if (userSkippedReRegisterFlow) {
setAutoShowSmsConfirmDialog(true);
}
}
public boolean shouldAutoShowSmsConfirmDialog() {
return autoShowSmsConfirmDialog;
}
public void setAutoShowSmsConfirmDialog(boolean autoShowSmsConfirmDialog) {
this.autoShowSmsConfirmDialog = autoShowSmsConfirmDialog;
}
@Override
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithoutRegistrationLock() {
final String sessionId = getSessionId();
if (sessionId == null) {
throw new IllegalStateException("No valid registration session");
}
return verifyAccountRepository.verifyAccount(sessionId, getRegistrationData())
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new)
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(processor -> {
if (processor.hasResult()) {
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
}
})
.observeOn(Schedulers.io())
.flatMap(processor -> {
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), null, null);
} else if (processor.getError() == null) {
return Single.just(ServiceResponse.<VerifyResponse>forApplicationError(new IncorrectCodeException(), 403, null));
} else {
return Single.just(ServiceResponse.<VerifyResponse, RegistrationSessionMetadataResponse>coerceError(processor.getResponse()));
}
})
.flatMap(verifyAccountWithoutKbsResponse -> {
VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(verifyAccountWithoutKbsResponse);
String pin = SignalStore.svr().getPin();
if ((processor.isRegistrationLockPresentAndSvrExhausted() || processor.registrationLock()) && SignalStore.svr().getRegistrationLockToken() != null && pin != null) {
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> SignalStore.svr().getOrCreateMasterKey())
.map(verifyAccountWithPinResponse -> {
if (verifyAccountWithPinResponse.getResult().isPresent() && verifyAccountWithPinResponse.getResult().get().getMasterKey() != null) {
return verifyAccountWithPinResponse;
} else {
return verifyAccountWithoutKbsResponse;
}
});
} else {
return Single.just(verifyAccountWithoutKbsResponse);
}
})
.onErrorReturn(ServiceResponse::forUnknownError);
}
@Override
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull SvrAuthCredentialSet svrAuthCredentials) {
final String sessionId = getSessionId();
if (sessionId == null) {
throw new IllegalStateException("No valid registration session");
}
return verifyAccountRepository.verifyAccount(sessionId, getRegistrationData())
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new)
.doOnSuccess(processor -> {
if (processor.hasResult()) {
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
}
})
.<ServiceResponse<VerifyResponse>>flatMap(processor -> {
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin));
} else {
return Single.just(ServiceResponse.coerceError(processor.getResponse()));
}
})
.onErrorReturn(ServiceResponse::forUnknownError);
}
@Override
protected Single<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor) {
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), false)
.map(VerifyResponseWithoutKbs::new);
}
@Override
protected Single<VerifyResponseWithRegistrationLockProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin) {
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), true)
.map(processor::updatedIfRegistrationFailed);
}
private RegistrationData getRegistrationData() {
return new RegistrationData(getTextCodeEntered(),
getNumber().getE164Number(),
getRegistrationSecret(),
registrationRepository.getRegistrationId(),
registrationRepository.getProfileKey(getNumber().getE164Number()),
getFcmToken(),
registrationRepository.getPniRegistrationId(),
getSessionId() != null ? null : getRecoveryPassword());
}
public @NonNull Single<VerifyResponseProcessor> verifyReRegisterWithPin(@NonNull String pin) {
return Single.fromCallable(() -> verifyReRegisterWithPinInternal(pin))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.flatMap(data -> {
if (data.canProceed) {
return updateFcmTokenValue().subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.onErrorReturnItem("")
.flatMap(s -> verifyReRegisterWithRecoveryPassword(pin, data.masterKey));
} else {
throw new IncorrectRegistrationRecoveryPasswordException();
}
})
.onErrorReturn(t -> new VerifyResponseWithRegistrationLockProcessor(ServiceResponse.forUnknownError(t), getSvrAuthCredentials()))
.map(p -> {
if (p instanceof VerifyResponseWithRegistrationLockProcessor) {
VerifyResponseWithRegistrationLockProcessor lockProcessor = (VerifyResponseWithRegistrationLockProcessor) p;
if (lockProcessor.wrongPin() && lockProcessor.getSvrTriesRemaining() != null) {
return new VerifyResponseWithRegistrationLockProcessor(lockProcessor.getResponse(), lockProcessor.getSvrAuthCredentials());
}
}
return p;
})
.doOnSuccess(p -> {
if (p.hasResult()) {
restoreFromStorageService();
}
})
.observeOn(AndroidSchedulers.mainThread());
}
@WorkerThread
private @NonNull ReRegistrationData verifyReRegisterWithPinInternal(@NonNull String pin)
throws SvrWrongPinException, IOException, SvrNoDataException
{
String localPinHash = SignalStore.svr().getLocalPinHash();
if (hasRecoveryPassword() && localPinHash != null) {
if (PinHashUtil.verifyLocalPinHash(localPinHash, pin)) {
Log.i(TAG, "Local pin matches input, attempting registration");
return ReRegistrationData.canProceed(SignalStore.svr().getOrCreateMasterKey());
} else {
throw new SvrWrongPinException(0);
}
} else {
SvrAuthCredentialSet authCredentials = getSvrAuthCredentials();
if (authCredentials == null) {
Log.w(TAG, "No SVR auth credentials, abort skip flow");
return ReRegistrationData.cannotProceed();
}
MasterKey masterKey = SvrRepository.restoreMasterKeyPreRegistration(authCredentials, pin);
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword());
setSvrTriesRemaining(10);
return ReRegistrationData.canProceed(masterKey);
}
}
private Single<VerifyResponseProcessor> verifyReRegisterWithRecoveryPassword(@NonNull String pin, @NonNull MasterKey masterKey) {
RegistrationData registrationData = getRegistrationData();
if (registrationData.getRecoveryPassword() == null) {
throw new IllegalStateException("No valid recovery password");
}
return verifyAccountRepository.registerAccount(null, registrationData, null, null)
.observeOn(Schedulers.io())
.onErrorReturn(ServiceResponse::forUnknownError)
.map(VerifyResponseWithoutKbs::new)
.flatMap(processor -> {
if (processor.registrationLock()) {
setSvrAuthCredentials(processor.getSvrAuthCredentials());
return verifyAccountRepository.registerAccount(null, registrationData, pin, () -> masterKey)
.onErrorReturn(ServiceResponse::forUnknownError)
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, processor.getSvrAuthCredentials()));
} else {
return Single.just(processor);
}
})
.flatMap(processor -> {
if (processor.hasResult()) {
VerifyResponse verifyResponse = processor.getResult();
boolean setRegistrationLockEnabled = verifyResponse.getMasterKey() != null;
if (!setRegistrationLockEnabled) {
verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), masterKey, pin, verifyResponse.getAciPreKeyCollection(), verifyResponse.getPniPreKeyCollection());
}
return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled)
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, getSvrAuthCredentials()));
} else {
return Single.just(processor);
}
});
}
public @NonNull Single<Boolean> canEnterSkipSmsFlow() {
if (userSkippedReRegisterFlow) {
Log.d(TAG, "User skipped re-register flow.");
return Single.just(false);
}
Log.d(TAG, "Querying if user can enter skip SMS flow.");
return Single.just(hasRecoveryPassword())
.flatMap(hasRecoveryPassword -> {
Log.i(TAG, "Checking if user has existing recovery password: " + hasRecoveryPassword);
if (hasRecoveryPassword) {
return Single.just(true);
} else {
return checkForValidSvrAuthCredentials();
}
});
}
private Single<Boolean> checkForValidSvrAuthCredentials() {
final List<String> svrAuthTokenList = SignalStore.svr().getSvr2AuthTokens();
List<String> usernamePasswords = svrAuthTokenList
.stream()
.limit(10)
.map(t -> {
try {
return new String(Base64.decode(t.replace("Basic ", "").trim()), StandardCharsets.ISO_8859_1);
} catch (IOException e) {
return null;
}
})
.collect(Collectors.toList());
if (usernamePasswords.isEmpty()) {
Log.d(TAG, "No valid SVR tokens in local store.");
return Single.just(false);
}
Log.d(TAG, "Valid tokens in local store, validating with SVR.");
return registrationRepository.getSvrAuthCredential(getRegistrationData(), usernamePasswords)
.flatMap(p -> {
if (p.hasValidSvr2AuthCredential()) {
Log.d(TAG, "Saving valid SVR2 auth credential.");
setSvrAuthCredentials(new SvrAuthCredentialSet(p.requireSvr2AuthCredential(), null));
return Single.just(true);
} else {
Log.d(TAG, "SVR2 response contained no valid SVR2 auth credentials.");
return Single.just(false);
}
})
.onErrorReturnItem(false)
.observeOn(AndroidSchedulers.mainThread());
}
public Single<String> updateFcmTokenValue() {
return verifyAccountRepository.getFcmToken().observeOn(AndroidSchedulers.mainThread()).doOnSuccess(this::setFcmToken);
}
private void restoreFromStorageService() {
SignalStore.onboarding().clearAll();
Stopwatch stopwatch = new Stopwatch("ReRegisterRestore");
AppDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
stopwatch.split("AccountRestore");
AppDependencies
.getJobManager()
.startChain(new StorageSyncJob())
.then(new ReclaimUsernameAndLinkJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");
try {
RemoteConfig.refreshSync();
} catch (IOException e) {
Log.w(TAG, "Failed to refresh flags.", e);
}
stopwatch.split("RemoteConfig");
stopwatch.stop(TAG);
}
private boolean hasRecoveryPassword() {
return getRecoveryPassword() != null && Objects.equals(getRegistrationData().getE164(), SignalStore.account().getE164());
}
private static class ReRegistrationData {
public boolean canProceed;
public MasterKey masterKey;
private ReRegistrationData(boolean canProceed, @Nullable MasterKey masterKey) {
this.canProceed = canProceed;
this.masterKey = masterKey;
}
public static ReRegistrationData cannotProceed() {
return new ReRegistrationData(false, null);
}
public static ReRegistrationData canProceed(@NonNull MasterKey masterKey) {
return new ReRegistrationData(true, masterKey);
}
}
public static final class Factory extends AbstractSavedStateViewModelFactory {
private final boolean isReregister;
public Factory(@NonNull SavedStateRegistryOwner owner, boolean isReregister) {
super(owner, null);
this.isReregister = isReregister;
}
@Override
protected @NonNull <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle) {
//noinspection ConstantConditions
return modelClass.cast(new RegistrationViewModel(handle,
isReregister,
new VerifyAccountRepository(AppDependencies.getApplication()),
new RegistrationRepository(AppDependencies.getApplication())));
}
}
}