mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Implement session-based account registration API.
This commit is contained in:
committed by
Greyson Parrelli
parent
3de17fa2d0
commit
a47e3900c1
@@ -17,8 +17,7 @@ object KbsAuthTokens : AndroidBackupItem {
|
||||
}
|
||||
|
||||
override fun getDataForBackup(): ByteArray {
|
||||
val registrationRecoveryTokenList = SignalStore.kbsValues().kbsAuthTokenList
|
||||
val proto = KbsAuthToken(tokens = registrationRecoveryTokenList)
|
||||
val proto = KbsAuthToken(tokens = SignalStore.kbsValues().kbsAuthTokenList)
|
||||
return proto.encode()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.components.registration
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ActionCountDownButton @JvmOverloads constructor(
|
||||
@@ -11,6 +11,12 @@ class ActionCountDownButton @JvmOverloads constructor(
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
) : MaterialButton(context, attrs, defStyle) {
|
||||
@StringRes
|
||||
private var enabledText = 0
|
||||
|
||||
@StringRes
|
||||
private var disabledText = 0
|
||||
|
||||
private var countDownToTime: Long = 0
|
||||
private var listener: Listener? = null
|
||||
|
||||
@@ -24,8 +30,8 @@ class ActionCountDownButton @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setCallEnabled() {
|
||||
setText(R.string.RegistrationActivity_call)
|
||||
private fun setActionEnabled() {
|
||||
setText(enabledText)
|
||||
isEnabled = true
|
||||
alpha = 1.0f
|
||||
}
|
||||
@@ -38,11 +44,11 @@ class ActionCountDownButton @JvmOverloads constructor(
|
||||
val totalRemainingSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingMillis).toInt()
|
||||
val minutesRemaining = totalRemainingSeconds / 60
|
||||
val secondsRemaining = totalRemainingSeconds % 60
|
||||
text = resources.getString(R.string.RegistrationActivity_call_me_instead_available_in, minutesRemaining, secondsRemaining)
|
||||
text = resources.getString(disabledText, minutesRemaining, secondsRemaining)
|
||||
listener?.onRemaining(this, totalRemainingSeconds)
|
||||
postDelayed({ updateCountDown() }, 250)
|
||||
} else {
|
||||
setCallEnabled()
|
||||
setActionEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +56,11 @@ class ActionCountDownButton @JvmOverloads constructor(
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
fun setTextResources(@StringRes enabled: Int, @StringRes disabled: Int) {
|
||||
enabledText = enabled
|
||||
disabledText = disabled
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onRemaining(view: ActionCountDownButton, secondsRemaining: Int)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
|
||||
@@ -48,17 +49,15 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
val mccMncProducer = MccMncProducer(requireContext())
|
||||
lifecycleDisposable += viewModel
|
||||
.ensureDecryptionsDrained()
|
||||
.onErrorComplete()
|
||||
.andThen(viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER))
|
||||
.andThen(viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, mccMncProducer.mcc, mccMncProducer.mnc))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
|
||||
@@ -83,14 +83,15 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
int registrationId = SignalStore.account().getRegistrationId();
|
||||
boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced();
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
String registrationLockV1 = null;
|
||||
String registrationLockV2 = null;
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId();
|
||||
int registrationId = SignalStore.account().getRegistrationId();
|
||||
boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced();
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
String registrationLockV1 = null;
|
||||
String registrationLockV2 = null;
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId();
|
||||
String registrationRecoveryPassword = kbsValues.getRegistrationRecoveryPassword();
|
||||
|
||||
if (kbsValues.isV2RegistrationLockEnabled()) {
|
||||
registrationLockV2 = kbsValues.getRegistrationLockToken();
|
||||
@@ -121,7 +122,8 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
capabilities,
|
||||
phoneNumberDiscoverable,
|
||||
encryptedDeviceName,
|
||||
pniRegistrationId);
|
||||
pniRegistrationId,
|
||||
registrationRecoveryPassword);
|
||||
|
||||
hasRefreshedThisAppCycle = true;
|
||||
}
|
||||
|
||||
@@ -149,6 +149,15 @@ public final class KbsValues extends SignalStoreValues {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @Nullable String getRegistrationRecoveryPassword() {
|
||||
MasterKey masterKey = getPinBackedMasterKey();
|
||||
if (masterKey == null) {
|
||||
return null;
|
||||
} else {
|
||||
return masterKey.deriveRegistrationRecoveryPassword();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @Nullable String getPin() {
|
||||
return getString(PIN, null);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -11,6 +12,8 @@ public final class RegistrationValues extends SignalStoreValues {
|
||||
private static final String REGISTRATION_COMPLETE = "registration.complete";
|
||||
private static final String PIN_REQUIRED = "registration.pin_required";
|
||||
private static final String HAS_UPLOADED_PROFILE = "registration.has_uploaded_profile";
|
||||
private static final String SESSION_E164 = "registration.session_e164";
|
||||
private static final String SESSION_ID = "registration.session_id";
|
||||
|
||||
RegistrationValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -60,4 +63,22 @@ public final class RegistrationValues extends SignalStoreValues {
|
||||
public void clearHasUploadedProfile() {
|
||||
putBoolean(HAS_UPLOADED_PROFILE, false);
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
putString(SESSION_ID, sessionId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSessionId() {
|
||||
return getString(SESSION_ID, null);
|
||||
}
|
||||
|
||||
public void setSessionE164(String sessionE164) {
|
||||
putString(SESSION_E164, sessionE164);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSessionE164() {
|
||||
return getString(SESSION_E164, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.keyvalue.SignalStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -27,14 +28,14 @@ public final class PushChallengeRequest {
|
||||
*
|
||||
* @param accountManager Account manager to request the push from.
|
||||
* @param fcmToken Optional FCM token. If not present will return absent.
|
||||
* @param e164number Local number.
|
||||
* @param sessionId Local number.
|
||||
* @param timeoutMs Timeout in milliseconds
|
||||
* @return Either returns a challenge, or absent.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static Optional<String> getPushChallengeBlocking(@NonNull SignalServiceAccountManager accountManager,
|
||||
@NonNull String sessionId,
|
||||
@NonNull Optional<String> fcmToken,
|
||||
@NonNull String e164number,
|
||||
long timeoutMs)
|
||||
{
|
||||
if (!fcmToken.isPresent()) {
|
||||
@@ -44,8 +45,7 @@ public final class PushChallengeRequest {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Log.i(TAG, "Requesting a push challenge");
|
||||
|
||||
Request request = new Request(accountManager, fcmToken.get(), e164number, timeoutMs);
|
||||
Request request = new Request(accountManager, fcmToken.get(), sessionId, timeoutMs);
|
||||
|
||||
Optional<String> challenge = request.requestAndReceiveChallengeBlocking();
|
||||
|
||||
@@ -69,19 +69,19 @@ public final class PushChallengeRequest {
|
||||
private final AtomicReference<String> challenge;
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final String fcmToken;
|
||||
private final String e164number;
|
||||
private final String sessionId;
|
||||
private final long timeoutMs;
|
||||
|
||||
private Request(@NonNull SignalServiceAccountManager accountManager,
|
||||
@NonNull String fcmToken,
|
||||
@NonNull String e164number,
|
||||
@NonNull String sessionId,
|
||||
long timeoutMs)
|
||||
{
|
||||
this.latch = new CountDownLatch(1);
|
||||
this.challenge = new AtomicReference<>();
|
||||
this.accountManager = accountManager;
|
||||
this.fcmToken = fcmToken;
|
||||
this.e164number = e164number;
|
||||
this.sessionId = sessionId;
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public final class PushChallengeRequest {
|
||||
|
||||
eventBus.register(this);
|
||||
try {
|
||||
accountManager.requestRegistrationPushChallenge(fcmToken, e164number);
|
||||
accountManager.requestRegistrationPushChallenge(sessionId, fcmToken);
|
||||
|
||||
latch.await(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
|
||||
@@ -117,5 +117,9 @@ public final class PushChallengeRequest {
|
||||
PushChallengeEvent(String challenge) {
|
||||
this.challenge = challenge;
|
||||
}
|
||||
|
||||
public String getChallenge() {
|
||||
return challenge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ data class RegistrationData(
|
||||
val registrationId: Int,
|
||||
val profileKey: ProfileKey,
|
||||
val fcmToken: String?,
|
||||
val pniRegistrationId: Int
|
||||
val pniRegistrationId: Int,
|
||||
val recoveryPassword: String?
|
||||
) {
|
||||
val isFcm: Boolean = fcmToken != null
|
||||
val isNotFcm: Boolean = fcmToken == null
|
||||
|
||||
@@ -209,4 +209,9 @@ public final class RegistrationRepository {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getRecoveryPassword() {
|
||||
return SignalStore.kbsValues().getRegistrationRecoveryPassword();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ExternalServiceFailureException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ImpossiblePhoneNumberException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.InvalidTransportModeException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MustRequestNewCodeException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Makes the server's response describing the state of the registration session as digestible as possible.
|
||||
*/
|
||||
sealed class RegistrationSessionProcessor(response: ServiceResponse<RegistrationSessionMetadataResponse>) : ServiceResponseProcessor<RegistrationSessionMetadataResponse>(response) {
|
||||
|
||||
companion object {
|
||||
const val CAPTCHA_KEY = "captcha"
|
||||
const val PUSH_CHALLENGE_KEY = "pushChallenge"
|
||||
val REQUESTABLE_INFORMATION = listOf(PUSH_CHALLENGE_KEY, CAPTCHA_KEY)
|
||||
}
|
||||
|
||||
public override fun captchaRequired(): Boolean {
|
||||
return super.captchaRequired() || (hasResult() && CAPTCHA_KEY == getChallenge())
|
||||
}
|
||||
|
||||
public override fun rateLimit(): Boolean {
|
||||
return error is RateLimitException
|
||||
}
|
||||
|
||||
public override fun getError(): Throwable? {
|
||||
return super.getError()
|
||||
}
|
||||
|
||||
fun pushChallengeRequired(): Boolean {
|
||||
return PUSH_CHALLENGE_KEY == getChallenge()
|
||||
}
|
||||
|
||||
fun isImpossibleNumber(): Boolean {
|
||||
return error is ImpossiblePhoneNumberException
|
||||
}
|
||||
|
||||
fun isNonNormalizedNumber(): Boolean {
|
||||
return error is NonNormalizedPhoneNumberException
|
||||
}
|
||||
|
||||
fun getRateLimit(): Long {
|
||||
Preconditions.checkState(error is RateLimitException, "This can only be called when isRateLimited()")
|
||||
return (error as RateLimitException).retryAfterMilliseconds.orElse(-1L)
|
||||
}
|
||||
|
||||
/**
|
||||
* The soonest time at which the server will accept a request to send a new code via SMS.
|
||||
* @return a unix timestamp in milliseconds, or 0 to represent null
|
||||
*/
|
||||
fun getNextCodeViaSmsAttempt(): Long {
|
||||
return deriveTimestamp(result.body.nextSms)
|
||||
}
|
||||
|
||||
/**
|
||||
* The soonest time at which the server will accept a request to send a new code via a voice call.
|
||||
* @return a unix timestamp in milliseconds, or 0 to represent null
|
||||
*/
|
||||
fun getNextCodeViaCallAttempt(): Long {
|
||||
return deriveTimestamp(result.body.nextCall)
|
||||
}
|
||||
|
||||
fun canSubmitProofImmediately(): Boolean {
|
||||
Preconditions.checkState(hasResult(), "This can only be called when result is present!")
|
||||
return 0 == result.body.nextVerificationAttempt
|
||||
}
|
||||
|
||||
/**
|
||||
* The soonest time at which the server will accept a submission of proof of ownership.
|
||||
* @return a unix timestamp in milliseconds, or 0 to represent null
|
||||
*/
|
||||
fun getNextProofSubmissionAttempt(): Long {
|
||||
Preconditions.checkState(hasResult(), "This can only be called when result is present!")
|
||||
return deriveTimestamp(result.body.nextVerificationAttempt)
|
||||
}
|
||||
|
||||
fun exhaustedVerificationCodeAttempts(): Boolean {
|
||||
return rateLimit() && getRateLimit() == -1L
|
||||
}
|
||||
|
||||
fun isInvalidSession(): Boolean {
|
||||
return error is NoSuchSessionException
|
||||
}
|
||||
|
||||
fun getSessionId(): String {
|
||||
Preconditions.checkState(hasResult(), "This can only be called when result is present!")
|
||||
return result.body.id
|
||||
}
|
||||
|
||||
fun isAllowedToRequestCode(): Boolean {
|
||||
Preconditions.checkState(hasResult(), "This can only be called when result is present!")
|
||||
return result.body.allowedToRequestCode
|
||||
}
|
||||
|
||||
fun getChallenge(): String? {
|
||||
Preconditions.checkState(hasResult(), "This can only be called when result is present!")
|
||||
return result.body.requestedInformation.firstOrNull { REQUESTABLE_INFORMATION.contains(it) }
|
||||
}
|
||||
|
||||
fun isVerified(): Boolean {
|
||||
return hasResult() && result.body.verified
|
||||
}
|
||||
|
||||
/** Should only be called if [isNonNormalizedNumber] */
|
||||
fun getOriginalNumber(): String {
|
||||
if (error !is NonNormalizedPhoneNumberException) {
|
||||
throw IllegalStateException("This can only be called when isNonNormalizedNumber()")
|
||||
}
|
||||
|
||||
return (error as NonNormalizedPhoneNumberException).originalNumber
|
||||
}
|
||||
|
||||
/** Should only be called if [isNonNormalizedNumber] */
|
||||
fun getNormalizedNumber(): String {
|
||||
if (error !is NonNormalizedPhoneNumberException) {
|
||||
throw IllegalStateException("This can only be called when isNonNormalizedNumber()")
|
||||
}
|
||||
|
||||
return (error as NonNormalizedPhoneNumberException).normalizedNumber
|
||||
}
|
||||
|
||||
fun cannotSubmitVerificationAttempt(): Boolean {
|
||||
return !hasResult() || result.body.nextVerificationAttempt == null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param deltaSeconds the number of whole seconds to be added to the server timestamp
|
||||
* @return a unix timestamp in milliseconds, or 0 to represent null
|
||||
*/
|
||||
private fun deriveTimestamp(deltaSeconds: Int?): Long {
|
||||
Preconditions.checkState(hasResult(), "This can only be called when result is present!")
|
||||
|
||||
if (deltaSeconds == null) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
val timestamp: Long = result.headers.timestamp
|
||||
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
|
||||
}
|
||||
|
||||
abstract fun verificationCodeRequestSuccess(): Boolean
|
||||
|
||||
class RegistrationSessionProcessorForSession(response: ServiceResponse<RegistrationSessionMetadataResponse>) : RegistrationSessionProcessor(response) {
|
||||
|
||||
override fun verificationCodeRequestSuccess(): Boolean = false
|
||||
}
|
||||
|
||||
class RegistrationSessionProcessorForVerification(response: ServiceResponse<RegistrationSessionMetadataResponse>) : RegistrationSessionProcessor(response) {
|
||||
override fun verificationCodeRequestSuccess(): Boolean = hasResult()
|
||||
|
||||
fun isAlreadyVerified(): Boolean {
|
||||
return error is AlreadyVerifiedException
|
||||
}
|
||||
|
||||
fun mustRequestNewCode(): Boolean {
|
||||
return error is MustRequestNewCodeException
|
||||
}
|
||||
|
||||
fun externalServiceFailure(): Boolean {
|
||||
return error is ExternalServiceFailureException
|
||||
}
|
||||
|
||||
fun invalidTransportModeFailure(): Boolean {
|
||||
return error is InvalidTransportModeException
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ImpossiblePhoneNumberException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.LocalRateLimitException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse
|
||||
|
||||
/**
|
||||
* Process responses from requesting an SMS or Phone code from the server.
|
||||
*/
|
||||
class RequestVerificationCodeResponseProcessor(response: ServiceResponse<RequestVerificationCodeResponse>) : ServiceResponseProcessor<RequestVerificationCodeResponse>(response) {
|
||||
public override fun captchaRequired(): Boolean {
|
||||
return super.captchaRequired()
|
||||
}
|
||||
|
||||
public override fun rateLimit(): Boolean {
|
||||
return super.rateLimit()
|
||||
}
|
||||
|
||||
public override fun getError(): Throwable? {
|
||||
return super.getError()
|
||||
}
|
||||
|
||||
fun localRateLimit(): Boolean {
|
||||
return error is LocalRateLimitException
|
||||
}
|
||||
|
||||
fun isImpossibleNumber(): Boolean {
|
||||
return error is ImpossiblePhoneNumberException
|
||||
}
|
||||
|
||||
fun isNonNormalizedNumber(): Boolean {
|
||||
return error is NonNormalizedPhoneNumberException
|
||||
}
|
||||
|
||||
/** Should only be called if [isNonNormalizedNumber] */
|
||||
fun getOriginalNumber(): String {
|
||||
if (error !is NonNormalizedPhoneNumberException) {
|
||||
throw IllegalStateException("This can only be called when isNonNormalizedNumber()")
|
||||
}
|
||||
|
||||
return (error as NonNormalizedPhoneNumberException).originalNumber
|
||||
}
|
||||
|
||||
/** Should only be called if [isNonNormalizedNumber] */
|
||||
fun getNormalizedNumber(): String {
|
||||
if (error !is NonNormalizedPhoneNumberException) {
|
||||
throw IllegalStateException("This can only be called when isNonNormalizedNumber()")
|
||||
}
|
||||
|
||||
return (error as NonNormalizedPhoneNumberException).normalizedNumber
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forLocalRateLimit(): RequestVerificationCodeResponseProcessor {
|
||||
val response: ServiceResponse<RequestVerificationCodeResponse> = ServiceResponse.forExecutionError(LocalRateLimitException())
|
||||
return RequestVerificationCodeResponseProcessor(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,29 @@ package org.thoughtcrime.securesms.registration
|
||||
import android.app.Application
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.AppCapabilities
|
||||
import org.thoughtcrime.securesms.gcm.FcmUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest.PushChallengeEvent
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -28,31 +33,105 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
class VerifyAccountRepository(private val context: Application) {
|
||||
|
||||
fun requestVerificationCode(
|
||||
fun validateSession(
|
||||
sessionId: String?,
|
||||
e164: String,
|
||||
password: String
|
||||
): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
return if (sessionId.isNullOrBlank()) {
|
||||
Single.just(ServiceResponse.forApplicationError(NoSuchSessionException(), 409, null))
|
||||
} else {
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
Single.fromCallable { accountManager.getRegistrationSession(sessionId) }.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
fun requestValidSession(
|
||||
e164: String,
|
||||
password: String,
|
||||
mode: Mode,
|
||||
captchaToken: String? = null
|
||||
): Single<ServiceResponse<RequestVerificationCodeResponse>> {
|
||||
mcc: String?,
|
||||
mnc: String?
|
||||
): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
return Single.fromCallable {
|
||||
val fcmToken: String? = FcmUtil.getToken(context).orElse(null)
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
if (fcmToken == null) {
|
||||
return@fromCallable accountManager.createRegistrationSession(null, mcc, mnc)
|
||||
} else {
|
||||
return@fromCallable createSessionAndBlockForPushChallenge(accountManager, fcmToken, mcc, mnc)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun createSessionAndBlockForPushChallenge(accountManager: SignalServiceAccountManager, fcmToken: String, mcc: String?, mnc: String?): ServiceResponse<RegistrationSessionMetadataResponse> {
|
||||
val subscriber = PushTokenChallengeSubscriber()
|
||||
val eventBus = EventBus.getDefault()
|
||||
eventBus.register(subscriber)
|
||||
|
||||
val response: ServiceResponse<RegistrationSessionMetadataResponse> = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
|
||||
|
||||
if (!response.result.isPresent) {
|
||||
return response
|
||||
}
|
||||
|
||||
subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
|
||||
eventBus.unregister(subscriber)
|
||||
|
||||
val challenge = subscriber.challenge
|
||||
|
||||
return if (challenge != null) {
|
||||
accountManager.submitPushChallengeToken(response.result.get().body.id, challenge)
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAndVerifyPushToken(
|
||||
sessionId: String,
|
||||
e164: String,
|
||||
password: String
|
||||
): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
val fcmToken: Optional<String> = FcmUtil.getToken(context)
|
||||
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, sessionId, fcmToken, PUSH_REQUEST_TIMEOUT)
|
||||
return Single.fromCallable {
|
||||
return@fromCallable accountManager.submitPushChallengeToken(sessionId, pushChallenge.orElse(null))
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun verifyCaptcha(
|
||||
sessionId: String,
|
||||
captcha: String,
|
||||
e164: String,
|
||||
password: String
|
||||
): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
return Single.fromCallable {
|
||||
return@fromCallable accountManager.submitCaptchaToken(sessionId, captcha)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun requestVerificationCode(
|
||||
sessionId: String,
|
||||
e164: String,
|
||||
password: String,
|
||||
mode: Mode
|
||||
): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
Log.d(TAG, "SMS Verification requested")
|
||||
|
||||
return Single.fromCallable {
|
||||
val fcmToken: Optional<String> = FcmUtil.getToken(context)
|
||||
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, e164, PUSH_REQUEST_TIMEOUT)
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.ofNullable(captchaToken), pushChallenge, fcmToken)
|
||||
return@fromCallable accountManager.requestVoiceVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported)
|
||||
} else {
|
||||
accountManager.requestSmsVerificationCode(Locale.getDefault(), mode.isSmsRetrieverSupported, Optional.ofNullable(captchaToken), pushChallenge, fcmToken)
|
||||
return@fromCallable accountManager.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun verifyAccount(registrationData: RegistrationData): Single<ServiceResponse<VerifyResponse>> {
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
fun verifyAccount(sessionId: String, registrationData: RegistrationData): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
|
||||
context,
|
||||
registrationData.e164,
|
||||
@@ -61,21 +140,14 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
)
|
||||
|
||||
return Single.fromCallable {
|
||||
val response = accountManager.verifyAccount(
|
||||
accountManager.verifyAccount(
|
||||
registrationData.code,
|
||||
registrationData.registrationId,
|
||||
registrationData.isNotFcm,
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(true),
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable,
|
||||
registrationData.pniRegistrationId
|
||||
sessionId
|
||||
)
|
||||
VerifyResponse.from(response, null, null)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun verifyAccountWithPin(registrationData: RegistrationData, pin: String, kbsPinDataProducer: KbsPinDataProducer): Single<ServiceResponse<VerifyResponse>> {
|
||||
fun registerAccount(sessionId: String, registrationData: RegistrationData, pin: String? = null, kbsPinDataProducer: KbsPinDataProducer? = null): Single<ServiceResponse<VerifyResponse>> {
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
@@ -86,30 +158,27 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
registrationData.password
|
||||
)
|
||||
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
val kbsData = kbsPinDataProducer.produceKbsPinData()
|
||||
val registrationLockV2: String = kbsData.masterKey.deriveRegistrationLock()
|
||||
val kbsData = kbsPinDataProducer?.produceKbsPinData()
|
||||
val registrationLockV2: String? = kbsData?.masterKey?.deriveRegistrationLock()
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.verifyAccountWithRegistrationLockPin(
|
||||
registrationData.code,
|
||||
registrationData.registrationId,
|
||||
registrationData.isNotFcm,
|
||||
registrationLockV2,
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(true),
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable,
|
||||
registrationData.pniRegistrationId
|
||||
)
|
||||
VerifyResponse.from(response, kbsData, pin)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: IOException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
val accountAttributes = AccountAttributes(
|
||||
signalingKey = null,
|
||||
registrationId = registrationData.registrationId,
|
||||
isFetchesMessages = registrationData.isNotFcm,
|
||||
pin = pin,
|
||||
registrationLock = registrationLockV2,
|
||||
unidentifiedAccessKey = unidentifiedAccessKey,
|
||||
isUnrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
|
||||
capabilities = AppCapabilities.getCapabilities(true),
|
||||
isDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable,
|
||||
name = null,
|
||||
pniRegistrationId = registrationData.pniRegistrationId,
|
||||
recoveryPassword = registrationData.recoveryPassword
|
||||
)
|
||||
|
||||
return Single.fromCallable {
|
||||
val response = accountManager.registerAccount(sessionId, registrationData.recoveryPassword, accountAttributes, true)
|
||||
VerifyResponse.from(response, kbsData, pin)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -124,6 +193,17 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
PHONE_CALL(false);
|
||||
}
|
||||
|
||||
private class PushTokenChallengeSubscriber {
|
||||
var challenge: String? = null
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
@Subscribe
|
||||
fun onChallengeEvent(pushChallengeEvent: PushChallengeEvent) {
|
||||
challenge = pushChallengeEvent.challenge
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(VerifyAccountRepository::class.java)
|
||||
private val PUSH_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.registration
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
@@ -32,6 +33,10 @@ sealed class VerifyResponseProcessor(response: ServiceResponse<VerifyResponse>)
|
||||
return super.getError()
|
||||
}
|
||||
|
||||
fun invalidSession(): Boolean {
|
||||
return error is NoSuchSessionException
|
||||
}
|
||||
|
||||
fun getLockedException(): LockedException {
|
||||
return error as LockedException
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ScrollView;
|
||||
@@ -11,6 +10,7 @@ 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.dialog.MaterialAlertDialogBuilder;
|
||||
@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
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;
|
||||
@@ -59,6 +60,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
private VerificationCodeView verificationCodeView;
|
||||
private VerificationPinKeyboard keyboard;
|
||||
private ActionCountDownButton callMeCountDown;
|
||||
private ActionCountDownButton resendSmsCountDown;
|
||||
private View wrongNumber;
|
||||
private boolean autoCompleting;
|
||||
|
||||
@@ -82,6 +84,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
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);
|
||||
|
||||
new SignalStrengthPhoneStateListener(this, this);
|
||||
@@ -91,9 +94,13 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
|
||||
setOnCodeFullyEnteredListener(verificationCodeView);
|
||||
|
||||
wrongNumber.setOnClickListener(v -> onWrongNumber());
|
||||
wrongNumber.setOnClickListener(v -> returnToPhoneEntryScreen());
|
||||
|
||||
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) {
|
||||
@@ -102,16 +109,21 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
}
|
||||
});
|
||||
|
||||
resendSmsCountDown.setListener((v, remaining) -> {
|
||||
if (remaining <= 30) {
|
||||
scrollView.smoothScrollTo(0, v.getBottom());
|
||||
resendSmsCountDown.setListener(null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = getViewModel();
|
||||
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
|
||||
if (attempts >= 3) {
|
||||
// TODO Add bottom sheet for help
|
||||
new ContactSupportBottomSheetFragment(this::openTroubleshootingSteps, this::sendEmailToSupport).show(getChildFragmentManager(), "support_bottom_sheet");
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.onStartEnterCode();
|
||||
}
|
||||
|
||||
protected abstract ViewModel getViewModel();
|
||||
@@ -124,7 +136,8 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
|
||||
protected abstract void navigateToKbsAccountLocked();
|
||||
|
||||
private void onWrongNumber() {
|
||||
private void returnToPhoneEntryScreen() {
|
||||
viewModel.resetSession();
|
||||
Navigation.findNavController(requireView()).navigateUp();
|
||||
}
|
||||
|
||||
@@ -132,6 +145,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
verificationCodeView.setOnCompleteListener(code -> {
|
||||
|
||||
callMeCountDown.setVisibility(View.INVISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.INVISIBLE);
|
||||
wrongNumber.setVisibility(View.INVISIBLE);
|
||||
keyboard.displayProgress();
|
||||
|
||||
@@ -181,6 +195,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
.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();
|
||||
@@ -209,6 +224,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
@@ -222,6 +238,7 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
@@ -285,19 +302,29 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
|
||||
viewModel.getNumber().getE164Number(),
|
||||
this::handlePhoneCallRequestAfterConfirm,
|
||||
this::onWrongNumber);
|
||||
() -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.PHONE_CALL),
|
||||
this::returnToPhoneEntryScreen);
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequestAfterConfirm() {
|
||||
Disposable request = viewModel.requestVerificationCode(VerifyAccountRepository.Mode.PHONE_CALL)
|
||||
private void handleSmsRequest() {
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_a_verification_code_will_be_sent_to,
|
||||
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(), R.string.RegistrationActivity_call_requested, Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(requireContext(), getCodeRequestedToastText(mode), Toast.LENGTH_LONG).show();
|
||||
} else if (processor.captchaRequired()) {
|
||||
navigateToCaptcha();
|
||||
} else if (processor.rateLimit()) {
|
||||
long rateLimit = processor.getRateLimit();
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request phone code", processor.getError());
|
||||
@@ -308,6 +335,19 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
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) {
|
||||
@@ -323,10 +363,52 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
@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()));
|
||||
|
||||
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
|
||||
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
|
||||
Disposable request = viewModel.validateSession(sessionE164, mccMncProducer.getMcc(), mccMncProducer.getMnc())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (!processor.hasResult()) {
|
||||
returnToPhoneEntryScreen();
|
||||
} else if (processor.isInvalidSession()) {
|
||||
returnToPhoneEntryScreen();
|
||||
} else if (processor.cannotSubmitVerificationAttempt()) {
|
||||
returnToPhoneEntryScreen();
|
||||
} else if (!processor.canSubmitProofImmediately()) {
|
||||
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 openTroubleshootingSteps() {
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.support_center_url));
|
||||
}
|
||||
|
||||
private void sendEmailToSupport() {
|
||||
@@ -347,6 +429,6 @@ public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistratio
|
||||
|
||||
@Override
|
||||
public void onCellSignalPresent() {
|
||||
// TODO animate away bottom sheet
|
||||
// TODO animate away bottom sheet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
|
||||
/**
|
||||
* Helpful bottom sheet dialog displayed during registration when the user enters the wrong verification code too many times.
|
||||
*/
|
||||
class ContactSupportBottomSheetFragment(private val troubleshootingStepsListener: Runnable, private val contactSupportListener: Runnable) : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {
|
||||
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_title))
|
||||
}
|
||||
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_1))
|
||||
pushStringAnnotation(
|
||||
tag = "URL",
|
||||
annotation = TROUBLESHOOTING_STEPS_KEY
|
||||
)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
) {
|
||||
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_2))
|
||||
}
|
||||
pop()
|
||||
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_3))
|
||||
pushStringAnnotation(
|
||||
tag = "URL",
|
||||
annotation = CONTACT_SUPPORT_KEY
|
||||
)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
) {
|
||||
append(stringResource(R.string.RegistrationActivity_support_bottom_sheet_body_part_4))
|
||||
}
|
||||
pop()
|
||||
}
|
||||
|
||||
return Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentSize(Alignment.Center)
|
||||
) {
|
||||
Handle()
|
||||
ClickableText(
|
||||
text = annotatedText,
|
||||
onClick = { offset ->
|
||||
// We check if there is an *URL* annotation attached to the text
|
||||
// at the clicked position
|
||||
annotatedText.getStringAnnotations(
|
||||
tag = "URL",
|
||||
start = offset,
|
||||
end = offset
|
||||
)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
when (annotation.item) {
|
||||
TROUBLESHOOTING_STEPS_KEY -> troubleshootingStepsListener.run()
|
||||
CONTACT_SUPPORT_KEY -> contactSupportListener.run()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private const val TROUBLESHOOTING_STEPS_KEY = "troubleshooting"
|
||||
private const val CONTACT_SUPPORT_KEY = "contact_support"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -49,9 +50,11 @@ import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
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;
|
||||
@@ -153,7 +156,8 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
||||
}
|
||||
|
||||
private void handleRegister(@NonNull Context context) {
|
||||
if (TextUtils.isEmpty(countryCode.getEditText().getText())) {
|
||||
final EditText countryCodeEditText = countryCode.getEditText();
|
||||
if (countryCodeEditText == null || TextUtils.isEmpty(countryCodeEditText.getText())) {
|
||||
showErrorDialog(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code));
|
||||
return;
|
||||
}
|
||||
@@ -245,23 +249,23 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
||||
}
|
||||
|
||||
private void requestVerificationCode(@NonNull Mode mode) {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
|
||||
Disposable request = viewModel.requestVerificationCode(mode)
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
|
||||
Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc())
|
||||
.doOnSubscribe(unused -> SignalStore.account().setRegistered(false))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit");
|
||||
if (processor.verificationCodeRequestSuccess()) {
|
||||
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} else if (processor.captchaRequired()) {
|
||||
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(register.getContext(), 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(register.getContext(), getString(R.string.RegistrationActivity_rate_limited_to_service));
|
||||
showErrorDialog(register.getContext(), 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(),
|
||||
@@ -281,6 +285,14 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
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) {
|
||||
new MaterialAlertDialogBuilder(context).setMessage(msg).setPositiveButton(R.string.ok, null).show();
|
||||
}
|
||||
@@ -306,6 +318,31 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
||||
viewModel.onCountrySelected(null, countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
String sessionE164 = viewModel.getSessionE164();
|
||||
if (sessionE164 != null && viewModel.getSessionId() != null) {
|
||||
checkIfSessionIsInProgressAndAdvance(sessionE164);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIfSessionIsInProgressAndAdvance(@NonNull String sessionE164) {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
|
||||
Disposable request = viewModel.validateSession(sessionE164, mccMncProducer.getMcc(), mccMncProducer.getMnc())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult() && processor.canSubmitProofImmediately()) {
|
||||
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} 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);
|
||||
@@ -346,9 +383,9 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
||||
.show();
|
||||
}
|
||||
|
||||
protected final void confirmNumberPrompt(@NonNull Context context,
|
||||
@NonNull String e164number,
|
||||
@NonNull Runnable onConfirmed)
|
||||
private void confirmNumberPrompt(@NonNull Context context,
|
||||
@NonNull String e164number,
|
||||
@NonNull Runnable onConfirmed)
|
||||
{
|
||||
showConfirmNumberDialogIfTranslated(context,
|
||||
R.string.RegistrationActivity_a_verification_code_will_be_sent_to,
|
||||
|
||||
@@ -9,19 +9,18 @@ import androidx.lifecycle.ViewModel;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository;
|
||||
import org.thoughtcrime.securesms.pin.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.RequestVerificationCodeResponseProcessor;
|
||||
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.VerifyResponseWithFailedKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithSuccessfulKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -33,9 +32,6 @@ import io.reactivex.rxjava3.core.Single;
|
||||
*/
|
||||
public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
|
||||
private static final long FIRST_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(64);
|
||||
private static final long SUBSEQUENT_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(300);
|
||||
|
||||
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";
|
||||
@@ -45,6 +41,7 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
private static final String STATE_KBS_TOKEN = "KBS_TOKEN";
|
||||
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";
|
||||
|
||||
protected final SavedStateHandle savedState;
|
||||
protected final VerifyAccountRepository verifyAccountRepository;
|
||||
@@ -73,6 +70,27 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String getSessionId() {
|
||||
return SignalStore.registrationValues().getSessionId();
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
SignalStore.registrationValues().setSessionId(sessionId);
|
||||
}
|
||||
|
||||
public @Nullable String getSessionE164() {
|
||||
return SignalStore.registrationValues().getSessionE164();
|
||||
}
|
||||
|
||||
public void setSessionE164(String sessionE164) {
|
||||
SignalStore.registrationValues().setSessionE164(sessionE164);
|
||||
}
|
||||
|
||||
public void resetSession() {
|
||||
setSessionE164(null);
|
||||
setSessionId(null);
|
||||
}
|
||||
|
||||
public @NonNull NumberViewState getNumber() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_NUMBER);
|
||||
@@ -138,15 +156,6 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
return savedState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
}
|
||||
|
||||
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_REQUEST_RATE_LIMITER);
|
||||
}
|
||||
|
||||
public void updateLimiter() {
|
||||
savedState.set(STATE_REQUEST_RATE_LIMITER, savedState.get(STATE_REQUEST_RATE_LIMITER));
|
||||
}
|
||||
|
||||
public @Nullable TokenData getKeyBackupCurrentToken() {
|
||||
return savedState.get(STATE_KBS_TOKEN);
|
||||
}
|
||||
@@ -163,43 +172,118 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
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 onStartEnterCode() {
|
||||
savedState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
|
||||
public void setCanCallAtTime(long callingTimestamp) {
|
||||
savedState.getLiveData(STATE_CAN_CALL_AT_TIME).postValue(callingTimestamp);
|
||||
}
|
||||
|
||||
public void onCallRequested() {
|
||||
savedState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
|
||||
public void setCanSmsAtTime(long smsTimestamp) {
|
||||
savedState.getLiveData(STATE_CAN_SMS_AT_TIME).postValue(smsTimestamp);
|
||||
}
|
||||
|
||||
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull Mode mode) {
|
||||
String captcha = getCaptchaToken();
|
||||
clearCaptchaResponse();
|
||||
public Single<RegistrationSessionProcessor> requestVerificationCode(@NonNull Mode mode, @Nullable String mcc, @Nullable String mnc) {
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
onCallRequested();
|
||||
} else if (!getRequestLimiter().canRequest(mode, getNumber().getE164Number(), System.currentTimeMillis())) {
|
||||
return Single.just(RequestVerificationCodeResponseProcessor.forLocalRateLimit());
|
||||
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()) {
|
||||
markASuccessfulAttempt();
|
||||
}
|
||||
|
||||
if (processor.hasResult() && processor.isAllowedToRequestCode()) {
|
||||
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
|
||||
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<RegistrationSessionProcessor.RegistrationSessionProcessorForSession> validateSession(String e164, @Nullable String mcc, @Nullable String mnc) {
|
||||
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, mcc, mnc)
|
||||
.flatMap(processor -> {
|
||||
if (processor.isInvalidSession()) {
|
||||
return verifyAccountRepository.requestValidSession(e164, getRegistrationSecret(), mcc, mnc)
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
|
||||
} else {
|
||||
return Single.just(processor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<RegistrationSessionProcessor> handleRequiredChallenges(RegistrationSessionProcessor processor, String e164) {
|
||||
final String sessionId = processor.getSessionId();
|
||||
|
||||
if (processor.isAllowedToRequestCode()) {
|
||||
return Single.just(processor);
|
||||
}
|
||||
|
||||
return verifyAccountRepository.requestVerificationCode(getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
mode,
|
||||
captcha)
|
||||
.map(RequestVerificationCodeResponseProcessor::new)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
markASuccessfulAttempt();
|
||||
getRequestLimiter().onSuccessfulRequest(mode, getNumber().getE164Number(), System.currentTimeMillis());
|
||||
} else {
|
||||
getRequestLimiter().onUnsuccessfulRequest();
|
||||
}
|
||||
updateLimiter();
|
||||
});
|
||||
if (hasCaptchaToken() && processor.captchaRequired()) {
|
||||
return verifyAccountRepository.verifyCaptcha(sessionId, Objects.requireNonNull(getCaptchaToken()), e164, getRegistrationSecret())
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
|
||||
} else {
|
||||
String challenge = processor.getChallenge();
|
||||
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) {
|
||||
@@ -261,4 +345,5 @@ public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
protected abstract Single<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor);
|
||||
|
||||
protected abstract Single<VerifyResponseWithRegistrationLockProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin);
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.pin.KbsRepository;
|
||||
import org.thoughtcrime.securesms.pin.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationRepository;
|
||||
import org.thoughtcrime.securesms.registration.RequestVerificationCodeResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
|
||||
@@ -23,10 +23,13 @@ import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -96,43 +99,69 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
|
||||
return completed != null ? completed : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull VerifyAccountRepository.Mode mode) {
|
||||
return super.requestVerificationCode(mode)
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
setFcmToken(processor.getResult().getFcmToken().orElse(null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithoutRegistrationLock() {
|
||||
return verifyAccountRepository.verifyAccount(getRegistrationData())
|
||||
.flatMap(verifyAccountWithoutKbsResponse -> {
|
||||
VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(verifyAccountWithoutKbsResponse);
|
||||
String pin = SignalStore.kbsValues().getPin();
|
||||
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 -> {
|
||||
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 {
|
||||
return Single.just(ServiceResponse.<VerifyResponse, RegistrationSessionMetadataResponse>coerceError(processor.getResponse()));
|
||||
}
|
||||
})
|
||||
.flatMap(verifyAccountWithoutKbsResponse -> {
|
||||
VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(verifyAccountWithoutKbsResponse);
|
||||
String pin = SignalStore.kbsValues().getPin();
|
||||
|
||||
if (processor.registrationLock() && SignalStore.kbsValues().getRegistrationLockToken() != null && pin != null) {
|
||||
KbsPinData pinData = new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse());
|
||||
if (processor.registrationLock() && SignalStore.kbsValues().getRegistrationLockToken() != null && pin != null) {
|
||||
KbsPinData pinData = new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse());
|
||||
|
||||
return verifyAccountRepository.verifyAccountWithPin(getRegistrationData(), pin, () -> pinData)
|
||||
.map(verifyAccountWithPinResponse -> {
|
||||
if (verifyAccountWithPinResponse.getResult().isPresent() && verifyAccountWithPinResponse.getResult().get().getKbsData() != null) {
|
||||
return verifyAccountWithPinResponse;
|
||||
} else {
|
||||
return verifyAccountWithoutKbsResponse;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Single.just(verifyAccountWithoutKbsResponse);
|
||||
}
|
||||
});
|
||||
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> pinData)
|
||||
.map(verifyAccountWithPinResponse -> {
|
||||
if (verifyAccountWithPinResponse.getResult().isPresent() && verifyAccountWithPinResponse.getResult().get().getKbsData() != null) {
|
||||
return verifyAccountWithPinResponse;
|
||||
} else {
|
||||
return verifyAccountWithoutKbsResponse;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Single.just(verifyAccountWithoutKbsResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull TokenData kbsTokenData) {
|
||||
return verifyAccountRepository.verifyAccountWithPin(getRegistrationData(), pin, () -> Objects.requireNonNull(KbsRepository.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse())));
|
||||
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());
|
||||
}
|
||||
})
|
||||
.flatMap( processor -> {
|
||||
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
|
||||
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> Objects.requireNonNull(KbsRepository.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse())));
|
||||
} else {
|
||||
return Single.just(ServiceResponse.coerceError(processor.getResponse()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,7 +183,8 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken(),
|
||||
registrationRepository.getPniRegistrationId());
|
||||
registrationRepository.getPniRegistrationId(),
|
||||
registrationRepository.getRecoveryPassword());
|
||||
}
|
||||
|
||||
public static final class Factory extends AbstractSavedStateViewModelFactory {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.util.dualsim
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* The mobile country code consists of three decimal digits and the mobile network code consists of two or three decimal digits.
|
||||
*/
|
||||
class MccMncProducer(context: Context) {
|
||||
var mcc: String? = null
|
||||
private set
|
||||
var mnc: String? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
if (context.packageManager.hasSystemFeature(FEATURE_TELEPHONY_RADIO_ACCESS)) {
|
||||
val tel = ContextCompat.getSystemService(context, TelephonyManager::class.java)
|
||||
val networkOperator = tel?.networkOperator
|
||||
|
||||
if (networkOperator?.isNotBlank() == true && networkOperator.length >= 5) {
|
||||
mcc = networkOperator.substring(0, 3)
|
||||
mnc = networkOperator.substring(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user