mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 10:51:27 +01:00
Delete registration V1.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
public final class BaseEnterCodeViewModelDelegate {
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user