kopia lustrzana https://github.com/ryukoposting/Signal-Android
Clarify networking call order during registration flow.
rodzic
a3d72fc06c
commit
8e32592218
|
@ -0,0 +1,141 @@
|
|||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.lock.PinHashing;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Using provided or already stored authorization, provides various get token data from KBS
|
||||
* and generate {@link KbsPinData}.
|
||||
*/
|
||||
public final class KbsRepository {
|
||||
|
||||
private static final String TAG = Log.tag(KbsRepository.class);
|
||||
|
||||
public void getToken(@NonNull Consumer<Optional<TokenData>> callback) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
callback.accept(Optional.fromNullable(getTokenSync(null)));
|
||||
} catch (IOException e) {
|
||||
callback.accept(Optional.absent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param authorization If this is being called before the user is registered (i.e. as part of
|
||||
* reglock), you must pass in an authorization token that can be used to
|
||||
* retrieve a backup. Otherwise, pass in null and we'll fetch one.
|
||||
*/
|
||||
public Single<ServiceResponse<TokenData>> getToken(@Nullable String authorization) {
|
||||
return Single.<ServiceResponse<TokenData>>fromCallable(() -> {
|
||||
try {
|
||||
return ServiceResponse.forResult(getTokenSync(authorization), 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
private @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException {
|
||||
TokenData firstKnownTokenData = null;
|
||||
|
||||
for (KbsEnclave enclave : KbsEnclaves.all()) {
|
||||
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
|
||||
|
||||
authorization = authorization == null ? kbs.getAuthorization() : authorization;
|
||||
|
||||
TokenResponse token = kbs.getToken(authorization);
|
||||
TokenData tokenData = new TokenData(enclave, authorization, token);
|
||||
|
||||
if (tokenData.getTriesRemaining() > 0) {
|
||||
Log.i(TAG, "Found data! " + enclave.getEnclaveName());
|
||||
return tokenData;
|
||||
} else if (firstKnownTokenData == null) {
|
||||
Log.i(TAG, "No data, but storing as the first response. " + enclave.getEnclaveName());
|
||||
firstKnownTokenData = tokenData;
|
||||
} else {
|
||||
Log.i(TAG, "No data, and we already have a 'first response'. " + enclave.getEnclaveName());
|
||||
}
|
||||
}
|
||||
|
||||
return Objects.requireNonNull(firstKnownTokenData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked during registration to restore the master key based on the server response during
|
||||
* verification.
|
||||
*
|
||||
* Does not affect {@link PinState}.
|
||||
*/
|
||||
public static synchronized @Nullable KbsPinData restoreMasterKey(@Nullable String pin,
|
||||
@NonNull KbsEnclave enclave,
|
||||
@Nullable String basicStorageCredentials,
|
||||
@NonNull TokenResponse tokenResponse)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
Log.i(TAG, "restoreMasterKey()");
|
||||
|
||||
if (pin == null) return null;
|
||||
|
||||
if (basicStorageCredentials == null) {
|
||||
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
|
||||
}
|
||||
|
||||
Log.i(TAG, "Preparing to restore from " + enclave.getEnclaveName());
|
||||
return restoreMasterKeyFromEnclave(enclave, pin, basicStorageCredentials, tokenResponse);
|
||||
}
|
||||
|
||||
private static @NonNull KbsPinData restoreMasterKeyFromEnclave(@NonNull KbsEnclave enclave,
|
||||
@NonNull String pin,
|
||||
@NonNull String basicStorageCredentials,
|
||||
@NonNull TokenResponse tokenResponse)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(enclave);
|
||||
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, tokenResponse);
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Restoring pin from KBS");
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, session);
|
||||
KbsPinData kbsData = session.restorePin(hashedPin);
|
||||
|
||||
if (kbsData != null) {
|
||||
Log.i(TAG, "Found registration lock token on KBS.");
|
||||
} else {
|
||||
throw new AssertionError("Null not expected");
|
||||
}
|
||||
|
||||
return kbsData;
|
||||
} catch (UnauthenticatedResponseException | InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to restore key", e);
|
||||
throw new IOException(e);
|
||||
} catch (KeyBackupServicePinException e) {
|
||||
Log.w(TAG, "Incorrect pin", e);
|
||||
throw new KeyBackupSystemWrongPinException(e.getToken());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.registration.service;
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
|
@ -1,24 +1,15 @@
|
|||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
@ -31,52 +22,12 @@ public class PinRestoreRepository {
|
|||
|
||||
private final Executor executor = SignalExecutors.UNBOUNDED;
|
||||
|
||||
void getToken(@NonNull Callback<Optional<TokenData>> callback) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
callback.onComplete(Optional.fromNullable(getTokenSync(null)));
|
||||
} catch (IOException e) {
|
||||
callback.onComplete(Optional.absent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param authorization If this is being called before the user is registered (i.e. as part of
|
||||
* reglock), you must pass in an authorization token that can be used to
|
||||
* retrieve a backup. Otherwise, pass in null and we'll fetch one.
|
||||
*/
|
||||
public @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException {
|
||||
TokenData firstKnownTokenData = null;
|
||||
|
||||
for (KbsEnclave enclave : KbsEnclaves.all()) {
|
||||
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
|
||||
|
||||
authorization = authorization == null ? kbs.getAuthorization() : authorization;
|
||||
|
||||
TokenResponse token = kbs.getToken(authorization);
|
||||
TokenData tokenData = new TokenData(enclave, authorization, token);
|
||||
|
||||
if (tokenData.getTriesRemaining() > 0) {
|
||||
Log.i(TAG, "Found data! " + enclave.getEnclaveName());
|
||||
return tokenData;
|
||||
} else if (firstKnownTokenData == null) {
|
||||
Log.i(TAG, "No data, but storing as the first response. " + enclave.getEnclaveName());
|
||||
firstKnownTokenData = tokenData;
|
||||
} else {
|
||||
Log.i(TAG, "No data, and we already have a 'first response'. " + enclave.getEnclaveName());
|
||||
}
|
||||
}
|
||||
|
||||
return Objects.requireNonNull(firstKnownTokenData);
|
||||
}
|
||||
|
||||
void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback<PinResultData> callback) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
Stopwatch stopwatch = new Stopwatch("PinSubmission");
|
||||
|
||||
KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse());
|
||||
KbsPinData kbsData = KbsRepository.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse());
|
||||
PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin);
|
||||
stopwatch.split("MasterKey");
|
||||
|
||||
|
@ -103,83 +54,6 @@ public class PinRestoreRepository {
|
|||
void onComplete(@NonNull T value);
|
||||
}
|
||||
|
||||
public static class TokenData implements Parcelable {
|
||||
private final KbsEnclave enclave;
|
||||
private final String basicAuth;
|
||||
private final TokenResponse tokenResponse;
|
||||
|
||||
TokenData(@NonNull KbsEnclave enclave, @NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
|
||||
this.enclave = enclave;
|
||||
this.basicAuth = basicAuth;
|
||||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
private TokenData(Parcel in) {
|
||||
//noinspection ConstantConditions
|
||||
this.enclave = new KbsEnclave(in.readString(), in.readString(), in.readString());
|
||||
this.basicAuth = in.readString();
|
||||
|
||||
byte[] backupId = new byte[0];
|
||||
byte[] token = new byte[0];
|
||||
|
||||
in.readByteArray(backupId);
|
||||
in.readByteArray(token);
|
||||
|
||||
this.tokenResponse = new TokenResponse(backupId, token, in.readInt());
|
||||
}
|
||||
|
||||
public static @NonNull TokenData withResponse(@NonNull TokenData data, @NonNull TokenResponse response) {
|
||||
return new TokenData(data.getEnclave(), data.getBasicAuth(), response);
|
||||
}
|
||||
|
||||
public int getTriesRemaining() {
|
||||
return tokenResponse.getTries();
|
||||
}
|
||||
|
||||
public @NonNull String getBasicAuth() {
|
||||
return basicAuth;
|
||||
}
|
||||
|
||||
public @NonNull TokenResponse getTokenResponse() {
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
public @NonNull KbsEnclave getEnclave() {
|
||||
return enclave;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(enclave.getEnclaveName());
|
||||
dest.writeString(enclave.getServiceId());
|
||||
dest.writeString(enclave.getMrEnclave());
|
||||
|
||||
dest.writeString(basicAuth);
|
||||
|
||||
dest.writeByteArray(tokenResponse.getBackupId());
|
||||
dest.writeByteArray(tokenResponse.getToken());
|
||||
dest.writeInt(tokenResponse.getTries());
|
||||
}
|
||||
|
||||
public static final Creator<TokenData> CREATOR = new Creator<TokenData>() {
|
||||
@Override
|
||||
public TokenData createFromParcel(Parcel in) {
|
||||
return new TokenData(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenData[] newArray(int size) {
|
||||
return new TokenData[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
static class PinResultData {
|
||||
private final PinResult result;
|
||||
private final TokenData tokenData;
|
||||
|
|
|
@ -15,15 +15,17 @@ public class PinRestoreViewModel extends ViewModel {
|
|||
private final PinRestoreRepository repo;
|
||||
private final DefaultValueLiveData<TriesRemaining> triesRemaining;
|
||||
private final SingleLiveEvent<Event> event;
|
||||
private final KbsRepository kbsRepository;
|
||||
|
||||
private volatile PinRestoreRepository.TokenData tokenData;
|
||||
private volatile TokenData tokenData;
|
||||
|
||||
public PinRestoreViewModel() {
|
||||
this.repo = new PinRestoreRepository();
|
||||
this.kbsRepository = new KbsRepository();
|
||||
this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false));
|
||||
this.event = new SingleLiveEvent<>();
|
||||
|
||||
repo.getToken(token -> {
|
||||
kbsRepository.getToken(token -> {
|
||||
if (token.isPresent()) {
|
||||
updateTokenData(token.get(), false);
|
||||
} else {
|
||||
|
@ -67,7 +69,7 @@ public class PinRestoreViewModel extends ViewModel {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
repo.getToken(token -> {
|
||||
kbsRepository.getToken(token -> {
|
||||
if (token.isPresent()) {
|
||||
updateTokenData(token.get(), false);
|
||||
onPinSubmitted(pin, pinKeyboardType);
|
||||
|
@ -86,7 +88,7 @@ public class PinRestoreViewModel extends ViewModel {
|
|||
return event;
|
||||
}
|
||||
|
||||
private void updateTokenData(@NonNull PinRestoreRepository.TokenData tokenData, boolean incorrectGuess) {
|
||||
private void updateTokenData(@NonNull TokenData tokenData, boolean incorrectGuess) {
|
||||
this.tokenData = tokenData;
|
||||
triesRemaining.postValue(new TriesRemaining(tokenData.getTriesRemaining(), incorrectGuess));
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.lock.PinHashing;
|
|||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -41,61 +40,6 @@ public final class PinState {
|
|||
|
||||
private static final String TAG = Log.tag(PinState.class);
|
||||
|
||||
/**
|
||||
* Invoked during registration to restore the master key based on the server response during
|
||||
* verification.
|
||||
*
|
||||
* Does not affect {@link PinState}.
|
||||
*/
|
||||
public static synchronized @Nullable KbsPinData restoreMasterKey(@Nullable String pin,
|
||||
@NonNull KbsEnclave enclave,
|
||||
@Nullable String basicStorageCredentials,
|
||||
@NonNull TokenResponse tokenResponse)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
Log.i(TAG, "restoreMasterKey()");
|
||||
|
||||
if (pin == null) return null;
|
||||
|
||||
if (basicStorageCredentials == null) {
|
||||
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
|
||||
}
|
||||
|
||||
Log.i(TAG, "Preparing to restore from " + enclave.getEnclaveName());
|
||||
return restoreMasterKeyFromEnclave(enclave, pin, basicStorageCredentials, tokenResponse);
|
||||
}
|
||||
|
||||
private static @NonNull KbsPinData restoreMasterKeyFromEnclave(@NonNull KbsEnclave enclave,
|
||||
@NonNull String pin,
|
||||
@NonNull String basicStorageCredentials,
|
||||
@NonNull TokenResponse tokenResponse)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(enclave);
|
||||
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, tokenResponse);
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Restoring pin from KBS");
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, session);
|
||||
KbsPinData kbsData = session.restorePin(hashedPin);
|
||||
|
||||
if (kbsData != null) {
|
||||
Log.i(TAG, "Found registration lock token on KBS.");
|
||||
} else {
|
||||
throw new AssertionError("Null not expected");
|
||||
}
|
||||
|
||||
return kbsData;
|
||||
} catch (UnauthenticatedResponseException | InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to restore key", e);
|
||||
throw new IOException(e);
|
||||
} catch (KeyBackupServicePinException e) {
|
||||
Log.w(TAG, "Incorrect pin", e);
|
||||
throw new KeyBackupSystemWrongPinException(e.getToken());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a user has successfully registered. Ensures all the necessary state is updated.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
public class TokenData implements Parcelable {
|
||||
private final KbsEnclave enclave;
|
||||
private final String basicAuth;
|
||||
private final TokenResponse tokenResponse;
|
||||
|
||||
TokenData(@NonNull KbsEnclave enclave, @NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
|
||||
this.enclave = enclave;
|
||||
this.basicAuth = basicAuth;
|
||||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
private TokenData(Parcel in) {
|
||||
this.enclave = new KbsEnclave(in.readString(), in.readString(), in.readString());
|
||||
this.basicAuth = in.readString();
|
||||
|
||||
byte[] backupId = in.createByteArray();
|
||||
byte[] token = in.createByteArray();
|
||||
|
||||
this.tokenResponse = new TokenResponse(backupId, token, in.readInt());
|
||||
}
|
||||
|
||||
public static @NonNull TokenData withResponse(@NonNull TokenData data, @NonNull TokenResponse response) {
|
||||
return new TokenData(data.getEnclave(), data.getBasicAuth(), response);
|
||||
}
|
||||
|
||||
public int getTriesRemaining() {
|
||||
return tokenResponse.getTries();
|
||||
}
|
||||
|
||||
public @NonNull String getBasicAuth() {
|
||||
return basicAuth;
|
||||
}
|
||||
|
||||
public @NonNull TokenResponse getTokenResponse() {
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
public @NonNull KbsEnclave getEnclave() {
|
||||
return enclave;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(enclave.getEnclaveName());
|
||||
dest.writeString(enclave.getServiceId());
|
||||
dest.writeString(enclave.getMrEnclave());
|
||||
|
||||
dest.writeString(basicAuth);
|
||||
|
||||
dest.writeByteArray(tokenResponse.getBackupId());
|
||||
dest.writeByteArray(tokenResponse.getToken());
|
||||
dest.writeInt(tokenResponse.getTries());
|
||||
}
|
||||
|
||||
public static final Creator<TokenData> CREATOR = new Creator<TokenData>() {
|
||||
@Override
|
||||
public TokenData createFromParcel(Parcel in) {
|
||||
return new TokenData(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenData[] newArray(int size) {
|
||||
return new TokenData[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey
|
||||
|
||||
data class RegistrationData(
|
||||
val code: String,
|
||||
val e164: String,
|
||||
val password: String,
|
||||
val registrationId: Int,
|
||||
val profileKey: ProfileKey,
|
||||
val fcmToken: String?
|
||||
) {
|
||||
val isFcm: Boolean = fcmToken != null
|
||||
val isNotFcm: Boolean = fcmToken == null
|
||||
}
|
|
@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever;
|
||||
import com.google.android.gms.common.api.CommonStatusCodes;
|
||||
|
@ -18,6 +19,7 @@ import com.google.android.gms.common.api.Status;
|
|||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.service.VerificationCodeParser;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -28,10 +30,9 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
|
|||
|
||||
public static final String RE_REGISTRATION_EXTRA = "re_registration";
|
||||
|
||||
private SmsRetrieverReceiver smsRetrieverReceiver;
|
||||
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);
|
||||
|
@ -58,6 +59,8 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = ViewModelProviders.of(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class);
|
||||
|
||||
setContentView(R.layout.activity_registration_navigation);
|
||||
initializeChallengeListener();
|
||||
|
||||
|
@ -73,6 +76,8 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
|
|||
if (intent.getData() != null) {
|
||||
CommunicationActions.handlePotentialProxyLinkUrl(this, intent.getDataString());
|
||||
}
|
||||
|
||||
viewModel.setIsReregister(isReregister(intent));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -81,6 +86,10 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
|
|||
shutdownChallengeListener();
|
||||
}
|
||||
|
||||
private boolean isReregister(@NonNull Intent intent) {
|
||||
return intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false);
|
||||
}
|
||||
|
||||
private void initializeChallengeListener() {
|
||||
smsRetrieverReceiver = new SmsRetrieverReceiver();
|
||||
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
package org.thoughtcrime.securesms.registration;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.pin.PinState;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.KeyHelper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Operations required for finalizing the registration of an account. This is
|
||||
* to be used after verifying the code and registration lock (if necessary) with
|
||||
* the server and being issued a UUID.
|
||||
*/
|
||||
public final class RegistrationRepository {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationRepository.class);
|
||||
|
||||
private final Application context;
|
||||
|
||||
public RegistrationRepository(@NonNull Application context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
|
||||
if (registrationId == 0) {
|
||||
registrationId = KeyHelper.generateRegistrationId(false);
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
|
||||
}
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public @NonNull ProfileKey getProfileKey(@NonNull String e164) {
|
||||
ProfileKey profileKey = findExistingProfileKey(context, e164);
|
||||
|
||||
if (profileKey == null) {
|
||||
profileKey = ProfileKeyUtil.createNew();
|
||||
Log.i(TAG, "No profile key found, created a new one");
|
||||
}
|
||||
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<VerifyAccountResponse>> registerAccountWithoutRegistrationLock(@NonNull RegistrationData registrationData,
|
||||
@NonNull VerifyAccountResponse response)
|
||||
{
|
||||
return registerAccount(registrationData, response, null, null);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<VerifyAccountResponse>> registerAccountWithRegistrationLock(@NonNull RegistrationData registrationData,
|
||||
@NonNull VerifyAccountWithRegistrationLockResponse response,
|
||||
@NonNull String pin)
|
||||
{
|
||||
return registerAccount(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData());
|
||||
}
|
||||
|
||||
private Single<ServiceResponse<VerifyAccountResponse>> registerAccount(@NonNull RegistrationData registrationData,
|
||||
@NonNull VerifyAccountResponse response,
|
||||
@Nullable String pin,
|
||||
@Nullable KbsPinData kbsData)
|
||||
{
|
||||
return Single.<ServiceResponse<VerifyAccountResponse>>fromCallable(() -> {
|
||||
try {
|
||||
registerAccountInternal(registrationData, response, pin, kbsData);
|
||||
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
jobManager.add(new DirectoryRefreshJob(false));
|
||||
jobManager.add(new RotateCertificateJob());
|
||||
|
||||
DirectoryRefreshListener.schedule(context);
|
||||
RotateSignedPreKeyListener.schedule(context);
|
||||
|
||||
return ServiceResponse.forResult(response, 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void registerAccountInternal(@NonNull RegistrationData registrationData,
|
||||
@NonNull VerifyAccountResponse response,
|
||||
@Nullable String pin,
|
||||
@Nullable KbsPinData kbsData)
|
||||
throws IOException
|
||||
{
|
||||
SessionUtil.archiveAllSessions();
|
||||
SenderKeyUtil.clearAllState(context);
|
||||
|
||||
UUID uuid = UuidUtil.parseOrThrow(response.getUuid());
|
||||
boolean hasPin = response.isStorageCapable();
|
||||
|
||||
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
|
||||
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
|
||||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, uuid, registrationData.getE164(), registrationData.getPassword());
|
||||
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
|
||||
|
||||
if (registrationData.isFcm()) {
|
||||
accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken()));
|
||||
}
|
||||
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientId selfId = Recipient.externalPush(context, uuid, registrationData.getE164(), true).getId();
|
||||
|
||||
recipientDatabase.setProfileSharing(selfId, true);
|
||||
recipientDatabase.markRegisteredOrThrow(selfId, uuid);
|
||||
|
||||
TextSecurePreferences.setLocalNumber(context, registrationData.getE164());
|
||||
TextSecurePreferences.setLocalUuid(context, uuid);
|
||||
recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey());
|
||||
ApplicationDependencies.getRecipientCache().clearSelf();
|
||||
|
||||
TextSecurePreferences.setFcmToken(context, registrationData.getFcmToken());
|
||||
TextSecurePreferences.setFcmDisabled(context, registrationData.isNotFcm());
|
||||
TextSecurePreferences.setWebsocketRegistered(context, true);
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context)
|
||||
.saveIdentity(selfId,
|
||||
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true);
|
||||
|
||||
TextSecurePreferences.setPushRegistered(context, true);
|
||||
TextSecurePreferences.setPushServerPassword(context, registrationData.getPassword());
|
||||
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true);
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false);
|
||||
|
||||
PinState.onRegistration(context, kbsData, pin, hasPin);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
|
||||
|
||||
if (recipient.isPresent()) {
|
||||
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.LocalRateLimitException
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forLocalRateLimit(): RequestVerificationCodeResponseProcessor {
|
||||
val response: ServiceResponse<RequestVerificationCodeResponse> = ServiceResponse.forExecutionError(LocalRateLimitException())
|
||||
return RequestVerificationCodeResponseProcessor(response)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import android.app.Application
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
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.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Request SMS/Phone verification codes to help prove ownership of a phone number.
|
||||
*/
|
||||
class VerifyAccountRepository(private val context: Application) {
|
||||
|
||||
fun requestVerificationCode(
|
||||
e164: String,
|
||||
password: String,
|
||||
mode: Mode,
|
||||
captchaToken: String? = null
|
||||
): Single<ServiceResponse<RequestVerificationCodeResponse>> {
|
||||
Log.d(TAG, "SMS Verification requested")
|
||||
|
||||
return Single.fromCallable {
|
||||
val fcmToken: Optional<String> = FcmUtil.getToken()
|
||||
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, password)
|
||||
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, e164, PUSH_REQUEST_TIMEOUT)
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge, fcmToken)
|
||||
} else {
|
||||
accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported, Optional.fromNullable(captchaToken), pushChallenge, fcmToken)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun verifyAccount(registrationData: RegistrationData): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
|
||||
context,
|
||||
registrationData.e164,
|
||||
registrationData.password
|
||||
)
|
||||
|
||||
return Single.fromCallable {
|
||||
accountManager.verifyAccount(
|
||||
registrationData.code,
|
||||
registrationData.registrationId,
|
||||
registrationData.isNotFcm,
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(true),
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable
|
||||
)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun verifyAccountWithPin(registrationData: RegistrationData, pin: String, tokenData: TokenData): Single<ServiceResponse<VerifyAccountWithRegistrationLockResponse>> {
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
|
||||
context,
|
||||
registrationData.e164,
|
||||
registrationData.password
|
||||
)
|
||||
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
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
|
||||
)
|
||||
VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
enum class Mode(val isSmsRetrieverSupported: Boolean) {
|
||||
SMS_WITH_LISTENER(true),
|
||||
SMS_WITHOUT_LISTENER(false),
|
||||
PHONE_CALL(false);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(VerifyAccountRepository::class.java)
|
||||
private val PUSH_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
|
||||
}
|
||||
|
||||
data class VerifyAccountWithRegistrationLockResponse(val verifyAccountResponse: VerifyAccountResponse, val kbsData: KbsPinData) {
|
||||
companion object {
|
||||
fun from(response: ServiceResponse<VerifyAccountResponse>, kbsData: KbsPinData): ServiceResponse<VerifyAccountWithRegistrationLockResponse> {
|
||||
return if (response.result.isPresent) {
|
||||
ServiceResponse.forResult(VerifyAccountWithRegistrationLockResponse(response.result.get(), kbsData), 200, null)
|
||||
} else {
|
||||
ServiceResponse.coerceError(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.LockedException
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
|
||||
/**
|
||||
* Process responses from attempting to verify an account for use in account registration.
|
||||
*/
|
||||
sealed class VerifyAccountResponseProcessor(response: ServiceResponse<VerifyAccountResponse>) : ServiceResponseProcessor<VerifyAccountResponse>(response) {
|
||||
|
||||
open val tokenData: TokenData? = null
|
||||
|
||||
public override fun authorizationFailed(): Boolean {
|
||||
return super.authorizationFailed()
|
||||
}
|
||||
|
||||
public override fun registrationLock(): Boolean {
|
||||
return super.registrationLock()
|
||||
}
|
||||
|
||||
public override fun rateLimit(): Boolean {
|
||||
return super.rateLimit()
|
||||
}
|
||||
|
||||
public override fun getError(): Throwable? {
|
||||
return super.getError()
|
||||
}
|
||||
|
||||
fun getLockedException(): LockedException {
|
||||
return error as LockedException
|
||||
}
|
||||
|
||||
abstract fun isKbsLocked(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify processor specific to verifying without needing to handle registration lock.
|
||||
*/
|
||||
class VerifyAccountResponseWithoutKbs(response: ServiceResponse<VerifyAccountResponse>) : VerifyAccountResponseProcessor(response) {
|
||||
override fun isKbsLocked(): Boolean {
|
||||
return registrationLock() && getLockedException().basicStorageCredentials == null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify processor specific to verifying and successfully retrieving KBS information to
|
||||
* later attempt to verif with registration lock data (pin).
|
||||
*/
|
||||
class VerifyAccountResponseWithSuccessfulKbs(
|
||||
response: ServiceResponse<VerifyAccountResponse>,
|
||||
override val tokenData: TokenData
|
||||
) : VerifyAccountResponseProcessor(response) {
|
||||
|
||||
override fun isKbsLocked(): Boolean {
|
||||
return registrationLock() && tokenData.triesRemaining == 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify processor specific to verifying and unsuccessfully retrieving KBS information that
|
||||
* is required for attempting to verify a registration locked account.
|
||||
*/
|
||||
class VerifyAccountResponseWithFailedKbs(response: ServiceResponse<TokenData>) : VerifyAccountResponseProcessor(ServiceResponse.coerceError(response)) {
|
||||
override fun isKbsLocked(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package org.thoughtcrime.securesms.registration
|
||||
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
|
||||
/**
|
||||
* Process responses from attempting to verify an account with registration lock for use in
|
||||
* account registration.
|
||||
*/
|
||||
class VerifyCodeWithRegistrationLockResponseProcessor(
|
||||
response: ServiceResponse<VerifyAccountWithRegistrationLockResponse>,
|
||||
val token: TokenData
|
||||
) : ServiceResponseProcessor<VerifyAccountWithRegistrationLockResponse>(response) {
|
||||
|
||||
public override fun rateLimit(): Boolean {
|
||||
return super.rateLimit()
|
||||
}
|
||||
|
||||
public override fun getError(): Throwable? {
|
||||
return super.getError()
|
||||
}
|
||||
|
||||
fun wrongPin(): Boolean {
|
||||
return error is KeyBackupSystemWrongPinException
|
||||
}
|
||||
|
||||
fun getTokenResponse(): TokenResponse {
|
||||
return (error as KeyBackupSystemWrongPinException).tokenResponse
|
||||
}
|
||||
|
||||
fun isKbsLocked(): Boolean {
|
||||
return error is KeyBackupSystemNoDataException
|
||||
}
|
||||
|
||||
fun updatedIfRegistrationFailed(response: ServiceResponse<VerifyAccountResponse>): VerifyCodeWithRegistrationLockResponseProcessor {
|
||||
if (response.result.isPresent) {
|
||||
return this
|
||||
}
|
||||
|
||||
return VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.coerceError(response), token)
|
||||
}
|
||||
}
|
|
@ -11,12 +11,15 @@ import android.widget.TextView;
|
|||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AccountLockedFragment extends BaseRegistrationFragment {
|
||||
public class AccountLockedFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
@ -29,7 +32,8 @@ public class AccountLockedFragment extends BaseRegistrationFragment {
|
|||
|
||||
TextView description = view.findViewById(R.id.account_locked_description);
|
||||
|
||||
getModel().getLockedTimeRemaining().observe(getViewLifecycleOwner(),
|
||||
RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(),
|
||||
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
|
||||
);
|
||||
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.SavedStateViewModelFactory;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.signal.core.util.TranslationDetection;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA;
|
||||
|
||||
abstract class BaseRegistrationFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(BaseRegistrationFragment.class);
|
||||
|
||||
private RegistrationViewModel model;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
this.model = getRegistrationViewModel(requireActivity());
|
||||
}
|
||||
|
||||
protected @NonNull RegistrationViewModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
protected boolean isReregister() {
|
||||
Activity activity = getActivity();
|
||||
|
||||
if (activity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return activity.getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false);
|
||||
}
|
||||
|
||||
protected static RegistrationViewModel getRegistrationViewModel(@NonNull FragmentActivity activity) {
|
||||
SavedStateViewModelFactory savedStateViewModelFactory = new SavedStateViewModelFactory(activity.getApplication(), activity);
|
||||
|
||||
return ViewModelProviders.of(activity, savedStateViewModelFactory).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
protected static void setSpinning(@Nullable CircularProgressButton button) {
|
||||
if (button != null) {
|
||||
button.setClickable(false);
|
||||
button.setIndeterminateProgressMode(true);
|
||||
button.setProgress(50);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void cancelSpinning(@Nullable CircularProgressButton button) {
|
||||
if (button != null) {
|
||||
button.setProgress(0);
|
||||
button.setIndeterminateProgressMode(false);
|
||||
button.setClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void hideKeyboard(@NonNull Context context, @NonNull View view) {
|
||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets view up to allow log submitting after multiple taps.
|
||||
*/
|
||||
protected static void setDebugLogSubmitMultiTapView(@Nullable View view) {
|
||||
if (view == null) return;
|
||||
|
||||
view.setOnClickListener(new View.OnClickListener() {
|
||||
|
||||
private static final int DEBUG_TAP_TARGET = 8;
|
||||
private static final int DEBUG_TAP_ANNOUNCE = 4;
|
||||
|
||||
private int debugTapCounter;
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Context context = v.getContext();
|
||||
|
||||
debugTapCounter++;
|
||||
|
||||
if (debugTapCounter >= DEBUG_TAP_TARGET) {
|
||||
context.startActivity(new Intent(context, SubmitDebugLogActivity.class));
|
||||
} else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) {
|
||||
int remaining = DEBUG_TAP_TARGET - debugTapCounter;
|
||||
|
||||
Toast.makeText(context, context.getResources().getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Presents a prompt for the user to confirm their number as long as it can be shown in one of their device languages.
|
||||
*/
|
||||
protected final void showConfirmNumberDialogIfTranslated(@NonNull Context context,
|
||||
@StringRes int firstMessageLine,
|
||||
@NonNull String e164number,
|
||||
@NonNull Runnable onConfirmed,
|
||||
@NonNull Runnable onEditNumber)
|
||||
{
|
||||
TranslationDetection translationDetection = new TranslationDetection(context);
|
||||
|
||||
if (translationDetection.textExistsInUsersLanguage(firstMessageLine) &&
|
||||
translationDetection.textExistsInUsersLanguage(R.string.RegistrationActivity_is_your_phone_number_above_correct) &&
|
||||
translationDetection.textExistsInUsersLanguage(R.string.RegistrationActivity_edit_number))
|
||||
{
|
||||
CharSequence message = new SpannableStringBuilder().append(context.getString(firstMessageLine))
|
||||
.append("\n\n")
|
||||
.append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(e164number)))
|
||||
.append("\n\n")
|
||||
.append(context.getString(R.string.RegistrationActivity_is_your_phone_number_above_correct));
|
||||
|
||||
Log.i(TAG, "Showing confirm number dialog (" + context.getString(firstMessageLine) + ")");
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok,
|
||||
(a, b) -> {
|
||||
Log.i(TAG, "Number confirmed");
|
||||
onConfirmed.run();
|
||||
})
|
||||
.setNegativeButton(R.string.RegistrationActivity_edit_number,
|
||||
(a, b) -> {
|
||||
Log.i(TAG, "User requested edit number from confirm dialog");
|
||||
onEditNumber.run();
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
Log.i(TAG, "Confirm number dialog not translated in " + Locale.getDefault() + " skipping");
|
||||
onConfirmed.run();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,14 +10,20 @@ import android.webkit.WebViewClient;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
/**
|
||||
* Fragment that displays a Captcha in a WebView.
|
||||
*/
|
||||
public final class CaptchaFragment extends BaseRegistrationFragment {
|
||||
public final class CaptchaFragment extends LoggingFragment {
|
||||
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
@ -46,11 +52,12 @@ public final class CaptchaFragment extends BaseRegistrationFragment {
|
|||
});
|
||||
|
||||
webView.loadUrl(RegistrationConstants.SIGNAL_CAPTCHA_URL);
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
private void handleToken(@NonNull String token) {
|
||||
getModel().onCaptchaResponse(token);
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(CaptchaFragmentDirections.actionCaptchaComplete());
|
||||
viewModel.setCaptchaResponse(token);
|
||||
NavHostFragment.findNavController(this).navigateUp();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,20 +20,18 @@ 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.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
||||
public class ChooseBackupFragment extends BaseRegistrationFragment {
|
||||
public class ChooseBackupFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ChooseBackupFragment.class);
|
||||
|
||||
private static final short OPEN_FILE_REQUEST_CODE = 3862;
|
||||
|
||||
private View chooseBackupButton;
|
||||
private TextView learnMore;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
|
@ -44,10 +42,10 @@ public class ChooseBackupFragment extends BaseRegistrationFragment {
|
|||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
View chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
|
||||
|
||||
learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -14,9 +14,11 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
|
||||
|
@ -39,7 +41,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
|||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
model = BaseRegistrationFragment.getRegistrationViewModel(requireActivity());
|
||||
model = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
countryFilter = view.findViewById(R.id.country_search);
|
||||
|
||||
|
@ -56,7 +58,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
|||
|
||||
model.onCountrySelected(countryName, countryCode);
|
||||
|
||||
Navigation.findNavController(view).navigate(CountryPickerFragmentDirections.actionCountrySelected());
|
||||
NavHostFragment.findNavController(this).navigateUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.PhoneStateListener;
|
||||
|
@ -12,38 +15,42 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
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.CallMeCountDownView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
|
||||
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
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.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class EnterCodeFragment extends BaseRegistrationFragment
|
||||
implements SignalStrengthPhoneStateListener.Callback
|
||||
{
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class EnterCodeFragment extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(EnterCodeFragment.class);
|
||||
|
||||
|
@ -57,7 +64,10 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
private View serviceWarning;
|
||||
private boolean autoCompleting;
|
||||
|
||||
private PhoneStateListener signalStrengthListener;
|
||||
private PhoneStateListener signalStrengthListener;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
@ -82,7 +92,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
signalStrengthListener = new SignalStrengthPhoneStateListener(this, this);
|
||||
|
||||
connectKeyboard(verificationCodeView, keyboard);
|
||||
hideKeyboard(requireContext(), view);
|
||||
ViewUtil.hideKeyboard(requireContext(), view);
|
||||
|
||||
setOnCodeFullyEnteredListener(verificationCodeView);
|
||||
|
||||
|
@ -99,15 +109,16 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
|
||||
noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport());
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
model.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
|
||||
if (attempts >= 3) {
|
||||
noCodeReceivedHelp.setVisibility(View.VISIBLE);
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000);
|
||||
}
|
||||
});
|
||||
|
||||
model.onStartEnterCode();
|
||||
viewModel.onStartEnterCode();
|
||||
}
|
||||
|
||||
private void onWrongNumber() {
|
||||
|
@ -117,115 +128,112 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
|
||||
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
|
||||
verificationCodeView.setOnCompleteListener(code -> {
|
||||
RegistrationViewModel model = getModel();
|
||||
|
||||
model.onVerificationCodeEntered(code);
|
||||
callMeCountDown.setVisibility(View.INVISIBLE);
|
||||
wrongNumber.setVisibility(View.INVISIBLE);
|
||||
keyboard.displayProgress();
|
||||
|
||||
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
|
||||
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithoutRegistrationLock(code)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulRegistration();
|
||||
} else if (processor.rateLimit()) {
|
||||
handleRateLimited();
|
||||
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
LockedException lockedException = processor.getLockedException();
|
||||
handleRegistrationLock(lockedException.getTimeRemaining());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
handleKbsAccountLocked();
|
||||
} else if (processor.authorizationFailed()) {
|
||||
handleIncorrectCodeError();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code", processor.getError());
|
||||
handleGeneralError();
|
||||
}
|
||||
});
|
||||
|
||||
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null,
|
||||
new CodeVerificationRequest.VerifyCallback() {
|
||||
|
||||
@Override
|
||||
public void onSuccessfulRegistration() {
|
||||
SimpleTask.run(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
FeatureFlags.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 -> {
|
||||
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
handleSuccessfulRegistration();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
|
||||
model.setLockedTimeRemaining(timeRemaining);
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull PinRestoreRepository.TokenData tokenData, @NonNull String kbsStorageCredentials) {
|
||||
model.setLockedTimeRemaining(timeRemaining);
|
||||
model.setKeyBackupTokenData(tokenData);
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull PinRestoreRepository.TokenData tokenData) {
|
||||
throw new AssertionError("Unexpected, user has made no pin guesses");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRateLimited() {
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKbsAccountLocked(@Nullable Long timeRemaining) {
|
||||
if (timeRemaining != null) {
|
||||
model.setLockedTimeRemaining(timeRemaining);
|
||||
}
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
disposables.add(verify);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleSuccessfulRegistration() {
|
||||
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
|
||||
public void handleSuccessfulRegistration() {
|
||||
SimpleTask.run(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
FeatureFlags.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 -> {
|
||||
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public 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);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void handleRegistrationLock(long timeRemaining) {
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void handleKbsAccountLocked() {
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
public void handleIncorrectCodeError() {
|
||||
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);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public 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);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -283,46 +291,28 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
private void handlePhoneCallRequest() {
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
|
||||
getModel().getNumber().getE164Number(),
|
||||
viewModel.getNumber().getE164Number(),
|
||||
this::handlePhoneCallRequestAfterConfirm,
|
||||
this::onWrongNumber);
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequestAfterConfirm() {
|
||||
RegistrationViewModel model = getModel();
|
||||
String captcha = model.getCaptchaToken();
|
||||
model.clearCaptchaResponse();
|
||||
Disposable request = viewModel.requestVerificationCode(VerifyAccountRepository.Mode.PHONE_CALL)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_call_requested, Toast.LENGTH_LONG).show();
|
||||
} else if (processor.captchaRequired()) {
|
||||
NavHostFragment.findNavController(this).navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
|
||||
} else if (processor.rateLimit()) {
|
||||
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());
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
model.onCallRequested();
|
||||
|
||||
NavController navController = Navigation.findNavController(callMeCountDown);
|
||||
|
||||
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
|
||||
|
||||
registrationService.requestVerificationCode(requireActivity(), RegistrationCodeRequest.Mode.PHONE_CALL, captcha,
|
||||
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
|
||||
|
||||
@Override
|
||||
public void onNeedCaptcha() {
|
||||
navController.navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestSent(@Nullable String fcmToken) {
|
||||
model.setFcmToken(fcmToken);
|
||||
model.markASuccessfulAttempt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRateLimited() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) {
|
||||
|
@ -341,10 +331,9 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
model.getLiveNumber().observe(getViewLifecycleOwner(), (s) -> header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, s.getFullFormattedNumber())));
|
||||
header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
|
||||
|
||||
model.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
|
||||
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
|
||||
}
|
||||
|
||||
private void sendEmailToSupport() {
|
||||
|
@ -387,8 +376,11 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
|
|||
@Override public void onAnimationEnd(Animator animation) {
|
||||
serviceWarning.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override public void onAnimationRepeat(Animator animation) {}
|
||||
})
|
||||
.start();
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
|
@ -22,11 +27,12 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever;
|
||||
|
@ -34,20 +40,28 @@ 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.i18n.phonenumbers.AsYouTypeFormatter;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class EnterPhoneNumberFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
|
||||
|
||||
|
@ -59,6 +73,9 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
private Spinner countrySpinner;
|
||||
private View cancel;
|
||||
private ScrollView scrollView;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
@ -67,8 +84,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false);
|
||||
}
|
||||
|
||||
|
@ -91,21 +107,23 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
|
||||
register.setOnClickListener(v -> handleRegister(requireContext()));
|
||||
|
||||
if (isReregister()) {
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
if (viewModel.isReregister()) {
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
cancel.setOnClickListener(v -> Navigation.findNavController(v).navigateUp());
|
||||
} else {
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
NumberViewState number = model.getNumber();
|
||||
NumberViewState number = viewModel.getNumber();
|
||||
|
||||
initNumber(number);
|
||||
|
||||
countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener());
|
||||
|
||||
if (model.hasCaptchaToken()) {
|
||||
if (viewModel.hasCaptchaToken()) {
|
||||
handleRegister(requireContext());
|
||||
}
|
||||
|
||||
|
@ -145,7 +163,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
numberInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
hideKeyboard(requireContext(), v);
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
handleRegister(requireContext());
|
||||
return true;
|
||||
}
|
||||
|
@ -164,31 +182,32 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
final NumberViewState number = getModel().getNumber();
|
||||
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));
|
||||
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, () -> handleRequestVerification(context, e164number, true));
|
||||
confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, true));
|
||||
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
|
||||
confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context, e164number));
|
||||
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));
|
||||
Dialogs.showAlertDialog(context,
|
||||
getString(R.string.RegistrationActivity_play_services_error),
|
||||
getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRequestVerification(@NonNull Context context, @NonNull String e164number, boolean fcmSupported) {
|
||||
private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) {
|
||||
setSpinning(register);
|
||||
disableAllEntries();
|
||||
|
||||
|
@ -198,16 +217,16 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
|
||||
task.addOnSuccessListener(none -> {
|
||||
Log.i(TAG, "Successfully registered SMS listener.");
|
||||
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITH_LISTENER);
|
||||
requestVerificationCode(Mode.SMS_WITH_LISTENER);
|
||||
});
|
||||
|
||||
task.addOnFailureListener(e -> {
|
||||
Log.w(TAG, "Failed to register SMS listener.", e);
|
||||
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER);
|
||||
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
|
||||
});
|
||||
} else {
|
||||
Log.i(TAG, "FCM is not supported, using no SMS listener");
|
||||
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER);
|
||||
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,77 +241,39 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
countryCode.setEnabled(true);
|
||||
number.setEnabled(true);
|
||||
countrySpinner.setEnabled(true);
|
||||
if (isReregister()) {
|
||||
if (viewModel.isReregister()) {
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestVerificationCode(String e164number, @NonNull RegistrationCodeRequest.Mode mode) {
|
||||
RegistrationViewModel model = getModel();
|
||||
String captcha = model.getCaptchaToken();
|
||||
model.clearCaptchaResponse();
|
||||
private void requestVerificationCode(@NonNull Mode mode) {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
|
||||
NavController navController = Navigation.findNavController(register);
|
||||
Disposable request = viewModel.requestVerificationCode(mode)
|
||||
.doOnSubscribe(unused -> TextSecurePreferences.setPushRegistered(ApplicationDependencies.getApplication(), false))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit");
|
||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required");
|
||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit");
|
||||
Toast.makeText(register.getContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.getError());
|
||||
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) {
|
||||
Log.i(TAG, "Local rate limited");
|
||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
cancelSpinning(register);
|
||||
enableAllEntries();
|
||||
return;
|
||||
}
|
||||
cancelSpinning(register);
|
||||
enableAllEntries();
|
||||
});
|
||||
|
||||
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
|
||||
|
||||
registrationService.requestVerificationCode(requireActivity(), mode, captcha,
|
||||
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
|
||||
|
||||
@Override
|
||||
public void onNeedCaptcha() {
|
||||
if (getContext() == null) {
|
||||
Log.i(TAG, "Got onNeedCaptcha response, but fragment is no longer attached.");
|
||||
return;
|
||||
}
|
||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
|
||||
cancelSpinning(register);
|
||||
enableAllEntries();
|
||||
model.getRequestLimiter().onUnsuccessfulRequest();
|
||||
model.updateLimiter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestSent(@Nullable String fcmToken) {
|
||||
if (getContext() == null) {
|
||||
Log.i(TAG, "Got requestSent response, but fragment is no longer attached.");
|
||||
return;
|
||||
}
|
||||
model.setFcmToken(fcmToken);
|
||||
model.markASuccessfulAttempt();
|
||||
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
cancelSpinning(register);
|
||||
enableAllEntries();
|
||||
model.getRequestLimiter().onSuccessfulRequest(mode, e164number, System.currentTimeMillis());
|
||||
model.updateLimiter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRateLimited() {
|
||||
Toast.makeText(register.getContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
|
||||
cancelSpinning(register);
|
||||
enableAllEntries();
|
||||
model.getRequestLimiter().onUnsuccessfulRequest();
|
||||
model.updateLimiter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
cancelSpinning(register);
|
||||
enableAllEntries();
|
||||
model.getRequestLimiter().onUnsuccessfulRequest();
|
||||
model.updateLimiter();
|
||||
}
|
||||
});
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
private void initializeSpinner(Spinner countrySpinner) {
|
||||
|
@ -368,10 +349,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
number.getInput().setSelection(numberLength, numberLength);
|
||||
}
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
|
||||
model.onCountrySelected(null, countryCode);
|
||||
setCountryDisplay(model.getNumber().getCountryDisplayName());
|
||||
viewModel.onCountrySelected(null, countryCode);
|
||||
setCountryDisplay(viewModel.getNumber().getCountryDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -391,11 +370,9 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
|
||||
if (number == null) return;
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
viewModel.setNationalNumber(number);
|
||||
|
||||
model.setNationalNumber(number);
|
||||
|
||||
setCountryDisplay(model.getNumber().getCountryDisplayName());
|
||||
setCountryDisplay(viewModel.getNumber().getCountryDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -449,13 +426,13 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
reformatText(number.getText());
|
||||
}
|
||||
|
||||
private void handlePromptForNoPlayServices(@NonNull Context context, @NonNull String e164number) {
|
||||
new AlertDialog.Builder(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) -> handleRequestVerification(context, e164number, false))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
private void handlePromptForNoPlayServices(@NonNull Context context) {
|
||||
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) -> handleRequestVerification(context, false))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
protected final void confirmNumberPrompt(@NonNull Context context,
|
||||
|
@ -466,7 +443,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
|
|||
R.string.RegistrationActivity_a_verification_code_will_be_sent_to,
|
||||
e164number,
|
||||
() -> {
|
||||
hideKeyboard(context, number.getInput());
|
||||
ViewUtil.hideKeyboard(context, number.getInput());
|
||||
onConfirmed.run();
|
||||
},
|
||||
() -> number.focusAndMoveCursorToEndAndOpenKeyboard());
|
||||
|
|
|
@ -9,16 +9,19 @@ import android.view.ViewGroup;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
|
||||
public final class RegistrationCompleteFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
|
@ -30,11 +33,12 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
|
|||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
FragmentActivity activity = requireActivity();
|
||||
FragmentActivity activity = requireActivity();
|
||||
RegistrationViewModel viewModel = ViewModelProviders.of(activity).get(RegistrationViewModel.class);
|
||||
|
||||
if (SignalStore.storageService().needsAccountRestore()) {
|
||||
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
|
||||
} else if (!isReregister()) {
|
||||
} else if (!viewModel.isReregister()) {
|
||||
final Intent main = MainActivity.clearTop(activity);
|
||||
final Intent profile = EditProfileActivity.getIntentForUserProfile(activity);
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
|
@ -14,37 +18,44 @@ import android.widget.Toast;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
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.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.pin.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class RegistrationLockFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationLockFragment.class);
|
||||
|
||||
/** Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1. */
|
||||
/**
|
||||
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
|
||||
*/
|
||||
private static final int MINIMUM_PIN_LENGTH = 4;
|
||||
|
||||
private EditText pinEntry;
|
||||
|
@ -54,6 +65,9 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
private TextView keyboardToggle;
|
||||
private long timeRemaining;
|
||||
private boolean isV1RegistrationLock;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
@ -87,7 +101,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
hideKeyboard(requireContext(), v);
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
handlePinEntry();
|
||||
return true;
|
||||
}
|
||||
|
@ -97,7 +111,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
enableAndFocusPinEntry();
|
||||
|
||||
pinButton.setOnClickListener((v) -> {
|
||||
hideKeyboard(requireContext(), pinEntry);
|
||||
ViewUtil.hideKeyboard(requireContext(), pinEntry);
|
||||
handlePinEntry();
|
||||
});
|
||||
|
||||
|
@ -111,22 +125,25 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
|
||||
getModel().getLockedTimeRemaining()
|
||||
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
TokenData keyBackupCurrentToken = getModel().getKeyBackupCurrentToken();
|
||||
viewModel.getLockedTimeRemaining()
|
||||
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
|
||||
|
||||
TokenData keyBackupCurrentToken = viewModel.getKeyBackupCurrentToken();
|
||||
|
||||
if (keyBackupCurrentToken != null) {
|
||||
int triesRemaining = keyBackupCurrentToken.getTriesRemaining();
|
||||
if (triesRemaining <= 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new AlertDialog.Builder(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();
|
||||
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) {
|
||||
|
@ -137,8 +154,8 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
@ -167,114 +184,94 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
|
||||
TokenData tokenData = model.getKeyBackupCurrentToken();
|
||||
|
||||
setSpinning(pinButton);
|
||||
|
||||
registrationService.verifyAccount(requireActivity(),
|
||||
model.getFcmToken(),
|
||||
model.getTextCodeEntered(),
|
||||
pin,
|
||||
tokenData,
|
||||
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulPinEntry();
|
||||
} else if (processor.wrongPin()) {
|
||||
onIncorrectKbsRegistrationLockPin(processor.getToken());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
onKbsAccountLocked();
|
||||
} else if (processor.rateLimit()) {
|
||||
onRateLimited();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", processor.getError());
|
||||
onError();
|
||||
}
|
||||
});
|
||||
|
||||
new CodeVerificationRequest.VerifyCallback() {
|
||||
disposables.add(verify);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccessfulRegistration() {
|
||||
handleSuccessfulPinEntry();
|
||||
}
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
@Override
|
||||
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
|
||||
getModel().setLockedTimeRemaining(timeRemaining);
|
||||
viewModel.setKeyBackupTokenData(tokenData);
|
||||
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
int triesRemaining = tokenData.getTriesRemaining();
|
||||
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin);
|
||||
}
|
||||
if (triesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials) {
|
||||
throw new AssertionError("Not expected after a pin guess");
|
||||
}
|
||||
if (triesRemaining == 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
@Override
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
model.setKeyBackupTokenData(tokenData);
|
||||
if (triesRemaining > 5) {
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
forgotPin.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
int triesRemaining = tokenData.getTriesRemaining();
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
return;
|
||||
}
|
||||
public void onRateLimited() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
if (triesRemaining == 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
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();
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
forgotPin.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
public void onKbsAccountLocked() {
|
||||
onAccountLocked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRateLimited() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
new AlertDialog.Builder(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 onError() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
@Override
|
||||
public void onKbsAccountLocked(@Nullable Long timeRemaining) {
|
||||
if (timeRemaining != null) {
|
||||
model.setLockedTimeRemaining(timeRemaining);
|
||||
}
|
||||
|
||||
onAccountLocked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void handleForgottenPin(long timeRemainingMs) {
|
||||
int lockoutDays = getLockoutDays(timeRemainingMs);
|
||||
new AlertDialog.Builder(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();
|
||||
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) {
|
||||
|
@ -288,7 +285,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
|
||||
pinEntry.getText().clear();
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.TranslationDetection
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
|
||||
object RegistrationViewDelegate {
|
||||
|
||||
@JvmStatic
|
||||
fun setDebugLogSubmitMultiTapView(view: View?) {
|
||||
view?.setOnClickListener(object : View.OnClickListener {
|
||||
private val DEBUG_TAP_TARGET = 8
|
||||
private val DEBUG_TAP_ANNOUNCE = 4
|
||||
private var debugTapCounter = 0
|
||||
|
||||
override fun onClick(view: View) {
|
||||
debugTapCounter++
|
||||
|
||||
if (debugTapCounter >= DEBUG_TAP_TARGET) {
|
||||
view.context.startActivity(Intent(view.context, SubmitDebugLogActivity::class.java))
|
||||
} else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) {
|
||||
val remaining = DEBUG_TAP_TARGET - debugTapCounter
|
||||
Toast.makeText(view.context, view.context.resources.getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showConfirmNumberDialogIfTranslated(
|
||||
context: Context,
|
||||
@StringRes firstMessageLine: Int,
|
||||
e164number: String,
|
||||
onConfirmed: Runnable,
|
||||
onEditNumber: Runnable
|
||||
) {
|
||||
val translationDetection = TranslationDetection(context)
|
||||
|
||||
if (translationDetection.textExistsInUsersLanguage(
|
||||
firstMessageLine,
|
||||
R.string.RegistrationActivity_is_your_phone_number_above_correct,
|
||||
R.string.RegistrationActivity_edit_number
|
||||
)
|
||||
) {
|
||||
val message: CharSequence = SpannableStringBuilder()
|
||||
.append(context.getString(firstMessageLine))
|
||||
.append("\n\n")
|
||||
.append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(e164number)))
|
||||
.append("\n\n")
|
||||
.append(context.getString(R.string.RegistrationActivity_is_your_phone_number_above_correct))
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> onConfirmed.run() }
|
||||
.setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onEditNumber.run() }
|
||||
.show()
|
||||
} else {
|
||||
onConfirmed.run()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
@ -27,6 +31,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
@ -38,6 +43,7 @@ import org.greenrobot.eventbus.Subscribe;
|
|||
import org.greenrobot.eventbus.ThreadMode;
|
||||
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.BackupPassphrase;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupBase;
|
||||
|
@ -47,6 +53,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
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;
|
||||
|
@ -57,7 +64,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
|||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
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;
|
||||
|
@ -94,7 +101,8 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
|||
.navigate(RestoreBackupFragmentDirections.actionSkip());
|
||||
});
|
||||
|
||||
if (isReregister()) {
|
||||
RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
if (viewModel.isReregister()) {
|
||||
Log.i(TAG, "Skipping backup restore during re-register.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
@ -16,8 +20,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
@ -30,6 +34,7 @@ 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;
|
||||
|
@ -40,52 +45,49 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
public final class WelcomeFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(WelcomeFragment.class);
|
||||
|
||||
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE };
|
||||
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE };
|
||||
@RequiresApi(26)
|
||||
private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
Manifest.permission.READ_PHONE_NUMBERS };
|
||||
private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
Manifest.permission.READ_PHONE_NUMBERS };
|
||||
@RequiresApi(26)
|
||||
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
Manifest.permission.READ_PHONE_NUMBERS };
|
||||
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
|
||||
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
|
||||
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
|
||||
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
|
||||
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
Manifest.permission.READ_PHONE_NUMBERS };
|
||||
|
||||
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
|
||||
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
|
||||
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
|
||||
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
|
||||
|
||||
private CircularProgressButton continueButton;
|
||||
private Button restoreFromBackup;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(isReregister() ? R.layout.fragment_registration_blank
|
||||
: R.layout.fragment_registration_welcome,
|
||||
container,
|
||||
false);
|
||||
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);
|
||||
|
||||
if (isReregister()) {
|
||||
RegistrationViewModel model = getModel();
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
if (model.hasRestoreFlowBeenShown()) {
|
||||
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();
|
||||
|
@ -98,10 +100,9 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
initializeNumber();
|
||||
|
||||
Log.i(TAG, "Skipping restore because this is a reregistration.");
|
||||
model.setWelcomeSkippedOnRestore();
|
||||
viewModel.setWelcomeSkippedOnRestore();
|
||||
Navigation.findNavController(view)
|
||||
.navigate(WelcomeFragmentDirections.actionSkipRestore());
|
||||
|
||||
} else {
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.image));
|
||||
|
@ -110,7 +111,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
continueButton = view.findViewById(R.id.welcome_continue_button);
|
||||
continueButton.setOnClickListener(this::continueClicked);
|
||||
|
||||
restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
|
||||
Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
|
||||
restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked);
|
||||
|
||||
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
|
||||
|
@ -211,13 +212,13 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
Phonenumber.PhoneNumber phoneNumber = localNumber.get();
|
||||
String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
|
||||
|
||||
getModel().onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
|
||||
viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
|
||||
} else {
|
||||
Log.i(TAG, "No number detected");
|
||||
Optional<String> simCountryIso = Util.getSimCountryIso(requireContext());
|
||||
|
||||
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
|
||||
getModel().onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
|
||||
viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,7 +229,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
|
||||
private boolean canUserSelectBackup() {
|
||||
return BackupUtil.isUserSelectionRequired(requireContext()) &&
|
||||
!isReregister() &&
|
||||
!viewModel.isReregister() &&
|
||||
!SignalStore.settings().isBackupEnabled();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,316 +0,0 @@
|
|||
package org.thoughtcrime.securesms.registration.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.AppCapabilities;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
|
||||
import org.thoughtcrime.securesms.pin.PinState;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.KeyHelper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class CodeVerificationRequest {
|
||||
|
||||
private static final String TAG = Log.tag(CodeVerificationRequest.class);
|
||||
|
||||
private enum Result {
|
||||
SUCCESS,
|
||||
PIN_LOCKED,
|
||||
KBS_WRONG_PIN,
|
||||
RATE_LIMITED,
|
||||
KBS_ACCOUNT_LOCKED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously verify the account via the code.
|
||||
*
|
||||
* @param fcmToken The FCM token for the device.
|
||||
* @param code The code that was delivered to the user.
|
||||
* @param pin The users registration pin.
|
||||
* @param callback Exactly one method on this callback will be called.
|
||||
* @param kbsTokenData By keeping the token, on failure, a newly returned token will be reused in subsequent pin
|
||||
* attempts, preventing certain attacks, we can also track the attempts making missing replies easier to spot.
|
||||
*/
|
||||
static void verifyAccount(@NonNull Context context,
|
||||
@NonNull Credentials credentials,
|
||||
@Nullable String fcmToken,
|
||||
@NonNull String code,
|
||||
@Nullable String pin,
|
||||
@Nullable TokenData kbsTokenData,
|
||||
@NonNull VerifyCallback callback)
|
||||
{
|
||||
new AsyncTask<Void, Void, Result>() {
|
||||
|
||||
private volatile LockedException lockedException;
|
||||
private volatile TokenData tokenData;
|
||||
|
||||
@Override
|
||||
protected Result doInBackground(Void... voids) {
|
||||
final boolean pinSupplied = pin != null;
|
||||
final boolean tryKbs = tokenData != null;
|
||||
|
||||
try {
|
||||
this.tokenData = kbsTokenData;
|
||||
verifyAccount(context, credentials, code, pin, tokenData, fcmToken);
|
||||
return Result.SUCCESS;
|
||||
} catch (KeyBackupSystemNoDataException e) {
|
||||
Log.w(TAG, "No data found on KBS");
|
||||
return Result.KBS_ACCOUNT_LOCKED;
|
||||
} catch (KeyBackupSystemWrongPinException e) {
|
||||
tokenData = TokenData.withResponse(tokenData, e.getTokenResponse());
|
||||
return Result.KBS_WRONG_PIN;
|
||||
} catch (LockedException e) {
|
||||
if (pinSupplied && tryKbs) {
|
||||
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
|
||||
}
|
||||
|
||||
Log.w(TAG, e);
|
||||
lockedException = e;
|
||||
if (e.getBasicStorageCredentials() != null) {
|
||||
try {
|
||||
tokenData = getToken(e.getBasicStorageCredentials());
|
||||
if (tokenData == null || tokenData.getTriesRemaining() == 0) {
|
||||
return Result.KBS_ACCOUNT_LOCKED;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Log.w(TAG, e);
|
||||
return Result.ERROR;
|
||||
}
|
||||
}
|
||||
return Result.PIN_LOCKED;
|
||||
} catch (RateLimitException e) {
|
||||
Log.w(TAG, e);
|
||||
return Result.RATE_LIMITED;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return Result.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
handleSuccessfulRegistration(context);
|
||||
callback.onSuccessfulRegistration();
|
||||
break;
|
||||
case PIN_LOCKED:
|
||||
if (tokenData != null) {
|
||||
if (lockedException.getBasicStorageCredentials() == null) {
|
||||
throw new AssertionError("KBS Token set, but no storage credentials supplied.");
|
||||
}
|
||||
Log.w(TAG, "Reg Locked: V2 pin needed for registration");
|
||||
callback.onKbsRegistrationLockPinRequired(lockedException.getTimeRemaining(), tokenData, lockedException.getBasicStorageCredentials());
|
||||
} else {
|
||||
Log.w(TAG, "Reg Locked: V1 pin needed for registration");
|
||||
callback.onV1RegistrationLockPinRequiredOrIncorrect(lockedException.getTimeRemaining());
|
||||
}
|
||||
break;
|
||||
case RATE_LIMITED:
|
||||
callback.onRateLimited();
|
||||
break;
|
||||
case ERROR:
|
||||
callback.onError();
|
||||
break;
|
||||
case KBS_WRONG_PIN:
|
||||
Log.w(TAG, "KBS Pin was wrong");
|
||||
callback.onIncorrectKbsRegistrationLockPin(tokenData);
|
||||
break;
|
||||
case KBS_ACCOUNT_LOCKED:
|
||||
Log.w(TAG, "KBS Account is locked");
|
||||
callback.onKbsAccountLocked(lockedException != null ? lockedException.getTimeRemaining() : null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.UNBOUNDED);
|
||||
}
|
||||
|
||||
private static TokenData getToken(@Nullable String basicStorageCredentials) throws IOException {
|
||||
if (basicStorageCredentials == null) return null;
|
||||
return new PinRestoreRepository().getTokenSync(basicStorageCredentials);
|
||||
}
|
||||
|
||||
private static void handleSuccessfulRegistration(@NonNull Context context) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
jobManager.add(new DirectoryRefreshJob(false));
|
||||
jobManager.add(new RotateCertificateJob());
|
||||
|
||||
DirectoryRefreshListener.schedule(context);
|
||||
RotateSignedPreKeyListener.schedule(context);
|
||||
}
|
||||
|
||||
private static void verifyAccount(@NonNull Context context,
|
||||
@NonNull Credentials credentials,
|
||||
@NonNull String code,
|
||||
@Nullable String pin,
|
||||
@Nullable TokenData kbsTokenData,
|
||||
@Nullable String fcmToken)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
boolean isV2RegistrationLock = kbsTokenData != null;
|
||||
int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number());
|
||||
|
||||
if (profileKey == null) {
|
||||
profileKey = ProfileKeyUtil.createNew();
|
||||
Log.i(TAG, "No profile key found, created a new one");
|
||||
}
|
||||
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
|
||||
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
|
||||
SessionUtil.archiveAllSessions();
|
||||
SenderKeyUtil.clearAllState(context);
|
||||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
|
||||
KbsPinData kbsData = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse()) : null;
|
||||
String registrationLockV2 = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null;
|
||||
String registrationLockV1 = isV2RegistrationLock ? null : pin;
|
||||
boolean hasFcm = fcmToken != null;
|
||||
|
||||
Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2));
|
||||
|
||||
VerifyAccountResponse response = accountManager.verifyAccountWithCode(code,
|
||||
null,
|
||||
registrationId,
|
||||
!hasFcm,
|
||||
registrationLockV1,
|
||||
registrationLockV2,
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(true),
|
||||
SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable());
|
||||
|
||||
UUID uuid = UuidUtil.parseOrThrow(response.getUuid());
|
||||
boolean hasPin = response.isStorageCapable();
|
||||
|
||||
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
|
||||
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
|
||||
|
||||
accountManager = AccountManagerFactory.createAuthenticated(context, uuid, credentials.getE164number(), credentials.getPassword());
|
||||
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
|
||||
|
||||
if (hasFcm) {
|
||||
accountManager.setGcmId(Optional.fromNullable(fcmToken));
|
||||
}
|
||||
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientId selfId = Recipient.externalPush(context, uuid, credentials.getE164number(), true).getId();
|
||||
|
||||
recipientDatabase.setProfileSharing(selfId, true);
|
||||
recipientDatabase.markRegisteredOrThrow(selfId, uuid);
|
||||
|
||||
TextSecurePreferences.setLocalNumber(context, credentials.getE164number());
|
||||
TextSecurePreferences.setLocalUuid(context, uuid);
|
||||
recipientDatabase.setProfileKey(selfId, profileKey);
|
||||
ApplicationDependencies.getRecipientCache().clearSelf();
|
||||
|
||||
TextSecurePreferences.setFcmToken(context, fcmToken);
|
||||
TextSecurePreferences.setFcmDisabled(context, !hasFcm);
|
||||
TextSecurePreferences.setWebsocketRegistered(context, true);
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context)
|
||||
.saveIdentity(selfId,
|
||||
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true);
|
||||
|
||||
TextSecurePreferences.setPushRegistered(context, true);
|
||||
TextSecurePreferences.setPushServerPassword(context, credentials.getPassword());
|
||||
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true);
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false);
|
||||
|
||||
PinState.onRegistration(context, kbsData, pin, hasPin);
|
||||
}
|
||||
|
||||
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
|
||||
|
||||
if (recipient.isPresent()) {
|
||||
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public interface VerifyCallback {
|
||||
|
||||
void onSuccessfulRegistration();
|
||||
|
||||
/**
|
||||
* The account is locked with a V1 (non-KBS) pin.
|
||||
*
|
||||
* @param timeRemaining Time until pin expires and number can be reused.
|
||||
*/
|
||||
void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining);
|
||||
|
||||
/**
|
||||
* The account is locked with a V2 (KBS) pin. Called before any user pin guesses.
|
||||
*/
|
||||
void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials);
|
||||
|
||||
/**
|
||||
* The account is locked with a V2 (KBS) pin. Called after a user pin guess.
|
||||
* <p>
|
||||
* i.e. an attempt has likely been used.
|
||||
*/
|
||||
void onIncorrectKbsRegistrationLockPin(@NonNull TokenData kbsTokenResponse);
|
||||
|
||||
/**
|
||||
* V2 (KBS) pin is set, but there is no data on KBS.
|
||||
*
|
||||
* @param timeRemaining Non-null if known.
|
||||
*/
|
||||
void onKbsAccountLocked(@Nullable Long timeRemaining);
|
||||
|
||||
void onRateLimited();
|
||||
|
||||
void onError();
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package org.thoughtcrime.securesms.registration.service;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class Credentials {
|
||||
|
||||
private final String e164number;
|
||||
private final String password;
|
||||
|
||||
public Credentials(@NonNull String e164number, @NonNull String password) {
|
||||
this.e164number = e164number;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public @NonNull String getE164number() {
|
||||
return e164number;
|
||||
}
|
||||
|
||||
public @NonNull String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package org.thoughtcrime.securesms.registration.service;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.gcm.FcmUtil;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class RegistrationCodeRequest {
|
||||
|
||||
private static final long PUSH_REQUEST_TIMEOUT_MS = 5000L;
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationCodeRequest.class);
|
||||
|
||||
/**
|
||||
* Request a verification code to be sent according to the specified {@param mode}.
|
||||
*
|
||||
* The request will fire asynchronously, and exactly one of the methods on the {@param callback}
|
||||
* will be called.
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
static void requestSmsVerificationCode(@NonNull Context context, @NonNull Credentials credentials, @Nullable String captchaToken, @NonNull Mode mode, @NonNull SmsVerificationCodeCallback callback) {
|
||||
Log.d(TAG, "SMS Verification requested");
|
||||
|
||||
new AsyncTask<Void, Void, VerificationRequestResult>() {
|
||||
@Override
|
||||
protected @NonNull
|
||||
VerificationRequestResult doInBackground(Void... voids) {
|
||||
try {
|
||||
markAsVerifying(context);
|
||||
|
||||
Optional<String> fcmToken = FcmUtil.getToken();
|
||||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
|
||||
|
||||
Optional<String> pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, credentials.getE164number(), PUSH_REQUEST_TIMEOUT_MS);
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge);
|
||||
} else {
|
||||
accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported(), Optional.fromNullable(captchaToken), pushChallenge);
|
||||
}
|
||||
|
||||
return new VerificationRequestResult(fcmToken.orNull(), Optional.absent());
|
||||
} catch (IOException e) {
|
||||
org.signal.core.util.logging.Log.w(TAG, "Error during account registration", e);
|
||||
return new VerificationRequestResult(null, Optional.of(e));
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPostExecute(@NonNull VerificationRequestResult result) {
|
||||
if (isCaptchaRequired(result)) {
|
||||
callback.onNeedCaptcha();
|
||||
} else if (isRateLimited(result)) {
|
||||
callback.onRateLimited();
|
||||
} else if (result.exception.isPresent()) {
|
||||
callback.onError();
|
||||
} else {
|
||||
callback.requestSent(result.fcmToken);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private static void markAsVerifying(Context context) {
|
||||
TextSecurePreferences.setPushRegistered(context, false);
|
||||
}
|
||||
|
||||
private static boolean isCaptchaRequired(@NonNull VerificationRequestResult result) {
|
||||
return result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException;
|
||||
}
|
||||
|
||||
private static boolean isRateLimited(@NonNull VerificationRequestResult result) {
|
||||
return result.exception.isPresent() && result.exception.get() instanceof RateLimitException;
|
||||
}
|
||||
|
||||
private static class VerificationRequestResult {
|
||||
private final @Nullable String fcmToken;
|
||||
private final Optional<IOException> exception;
|
||||
|
||||
private VerificationRequestResult(@Nullable String fcmToken, Optional<IOException> exception) {
|
||||
this.fcmToken = fcmToken;
|
||||
this.exception = exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The mode by which a code is being requested.
|
||||
*/
|
||||
public enum Mode {
|
||||
|
||||
/**
|
||||
* Device is requesting an SMS and supports SMS retrieval.
|
||||
*
|
||||
* The SMS sent will be formatted for automatic SMS retrieval.
|
||||
*/
|
||||
SMS_WITH_LISTENER(true),
|
||||
|
||||
/**
|
||||
* Device is requesting an SMS and does not support SMS retrieval.
|
||||
*
|
||||
* The SMS sent will be not be specially formatted for automatic SMS retrieval.
|
||||
*/
|
||||
SMS_WITHOUT_LISTENER(false),
|
||||
|
||||
/**
|
||||
* Device is requesting a phone call.
|
||||
*/
|
||||
PHONE_CALL(false);
|
||||
|
||||
private final boolean smsRetrieverSupported;
|
||||
|
||||
Mode(boolean smsRetrieverSupported) {
|
||||
this.smsRetrieverSupported = smsRetrieverSupported;
|
||||
}
|
||||
|
||||
public boolean isSmsRetrieverSupported() {
|
||||
return smsRetrieverSupported;
|
||||
}
|
||||
}
|
||||
|
||||
public interface SmsVerificationCodeCallback {
|
||||
|
||||
void onNeedCaptcha();
|
||||
|
||||
void requestSent(@Nullable String fcmToken);
|
||||
|
||||
void onRateLimited();
|
||||
|
||||
void onError();
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package org.thoughtcrime.securesms.registration.service;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
|
||||
|
||||
public final class RegistrationService {
|
||||
|
||||
private final Credentials credentials;
|
||||
|
||||
private RegistrationService(@NonNull Credentials credentials) {
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
public static RegistrationService getInstance(@NonNull String e164number, @NonNull String password) {
|
||||
return new RegistrationService(new Credentials(e164number, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link RegistrationCodeRequest}.
|
||||
*/
|
||||
public void requestVerificationCode(@NonNull Activity activity,
|
||||
@NonNull RegistrationCodeRequest.Mode mode,
|
||||
@Nullable String captchaToken,
|
||||
@NonNull RegistrationCodeRequest.SmsVerificationCodeCallback callback)
|
||||
{
|
||||
RegistrationCodeRequest.requestSmsVerificationCode(activity, credentials, captchaToken, mode, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link CodeVerificationRequest}.
|
||||
*/
|
||||
public void verifyAccount(@NonNull Activity activity,
|
||||
@Nullable String fcmToken,
|
||||
@NonNull String code,
|
||||
@Nullable String pin,
|
||||
@Nullable PinRestoreRepository.TokenData tokenData,
|
||||
@NonNull CodeVerificationRequest.VerifyCallback callback)
|
||||
{
|
||||
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, tokenData, callback);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import android.os.Parcelable;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
@ -14,8 +14,8 @@ import java.util.Objects;
|
|||
|
||||
public final class LocalCodeRequestRateLimiter implements Parcelable {
|
||||
|
||||
private final long timePeriod;
|
||||
private final Map<RegistrationCodeRequest.Mode, Data> dataMap;
|
||||
private final long timePeriod;
|
||||
private final Map<Mode, Data> dataMap;
|
||||
|
||||
public LocalCodeRequestRateLimiter(long timePeriod) {
|
||||
this.timePeriod = timePeriod;
|
||||
|
@ -23,7 +23,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
|
|||
}
|
||||
|
||||
@MainThread
|
||||
public boolean canRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
|
||||
public boolean canRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
|
||||
Data data = dataMap.get(mode);
|
||||
|
||||
return data == null || !data.limited(e164Number, currentTime);
|
||||
|
@ -33,7 +33,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
|
|||
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
|
||||
*/
|
||||
@MainThread
|
||||
public void onSuccessfulRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
|
||||
public void onSuccessfulRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
|
||||
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
|
||||
}
|
||||
|
||||
|
@ -69,9 +69,9 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
|
|||
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
|
||||
|
||||
for (int i = 0; i < numberOfMapEntries; i++) {
|
||||
RegistrationCodeRequest.Mode mode = RegistrationCodeRequest.Mode.values()[in.readInt()];
|
||||
String e164Number = in.readString();
|
||||
long limitedUntil = in.readLong();
|
||||
Mode mode = Mode.values()[in.readInt()];
|
||||
String e164Number = in.readString();
|
||||
long limitedUntil = in.readLong();
|
||||
|
||||
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
|
|||
dest.writeLong(timePeriod);
|
||||
dest.writeInt(dataMap.size());
|
||||
|
||||
for (Map.Entry<RegistrationCodeRequest.Mode, Data> a : dataMap.entrySet()) {
|
||||
for (Map.Entry<Mode, Data> a : dataMap.entrySet()) {
|
||||
dest.writeInt(a.getKey().ordinal());
|
||||
dest.writeString(a.getValue().e164Number);
|
||||
dest.writeLong(a.getValue().limitedUntil);
|
||||
|
|
|
@ -3,90 +3,117 @@ package org.thoughtcrime.securesms.registration.viewmodel;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.savedstate.SavedStateRegistryOwner;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
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.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithFailedKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithSuccessfulKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RegistrationViewModel extends ViewModel {
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationViewModel.class);
|
||||
public final class RegistrationViewModel 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 final String secret;
|
||||
private final MutableLiveData<NumberViewState> number;
|
||||
private final MutableLiveData<String> textCodeEntered;
|
||||
private final MutableLiveData<String> captchaToken;
|
||||
private final MutableLiveData<String> fcmToken;
|
||||
private final MutableLiveData<Boolean> restoreFlowShown;
|
||||
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
|
||||
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
|
||||
private final MutableLiveData<TokenData> kbsTokenData;
|
||||
private final MutableLiveData<Long> lockedTimeRemaining;
|
||||
private final MutableLiveData<Long> canCallAtTime;
|
||||
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
|
||||
private static final String STATE_NUMBER = "NUMBER";
|
||||
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
|
||||
private static final String STATE_CAPTCHA = "CAPTCHA";
|
||||
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_SUCCESSFUL_CODE_REQUEST_ATTEMPTS = "SUCCESSFUL_CODE_REQUEST_ATTEMPTS";
|
||||
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
|
||||
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_IS_REREGISTER = "IS_REREGISTER";
|
||||
|
||||
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
|
||||
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
|
||||
private final SavedStateHandle registrationState;
|
||||
private final VerifyAccountRepository verifyAccountRepository;
|
||||
private final KbsRepository kbsRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
|
||||
number = savedStateHandle.getLiveData("NUMBER", NumberViewState.INITIAL);
|
||||
textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", "");
|
||||
captchaToken = savedStateHandle.getLiveData("CAPTCHA");
|
||||
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
|
||||
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
|
||||
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
|
||||
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
|
||||
kbsTokenData = savedStateHandle.getLiveData("KBS_TOKEN");
|
||||
lockedTimeRemaining = savedStateHandle.getLiveData("TIME_REMAINING", 0L);
|
||||
canCallAtTime = savedStateHandle.getLiveData("CAN_CALL_AT_TIME", 0L);
|
||||
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
|
||||
boolean isReregister,
|
||||
@NonNull VerifyAccountRepository verifyAccountRepository,
|
||||
@NonNull KbsRepository kbsRepository,
|
||||
@NonNull RegistrationRepository registrationRepository)
|
||||
{
|
||||
this.registrationState = savedStateHandle;
|
||||
this.verifyAccountRepository = verifyAccountRepository;
|
||||
this.kbsRepository = kbsRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
|
||||
setInitialDefaultValue(this.registrationState, STATE_REGISTRATION_SECRET, Util.getSecret(18));
|
||||
setInitialDefaultValue(this.registrationState, STATE_NUMBER, NumberViewState.INITIAL);
|
||||
setInitialDefaultValue(this.registrationState, STATE_VERIFICATION_CODE, "");
|
||||
setInitialDefaultValue(this.registrationState, STATE_RESTORE_FLOW_SHOWN, false);
|
||||
setInitialDefaultValue(this.registrationState, STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
setInitialDefaultValue(this.registrationState, STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
|
||||
|
||||
this.registrationState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
|
||||
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
|
||||
if (!savedStateHandle.contains(key)) {
|
||||
private static <T> void setInitialDefaultValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
|
||||
if (!savedStateHandle.contains(key) || savedStateHandle.get(key) == null) {
|
||||
savedStateHandle.set(key, initialValue);
|
||||
}
|
||||
return savedStateHandle.get(key);
|
||||
}
|
||||
|
||||
public boolean isReregister() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_IS_REREGISTER);
|
||||
}
|
||||
|
||||
public @NonNull NumberViewState getNumber() {
|
||||
//noinspection ConstantConditions Live data was given an initial value
|
||||
return number.getValue();
|
||||
}
|
||||
|
||||
public @NonNull LiveData<NumberViewState> getLiveNumber() {
|
||||
return number;
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_NUMBER);
|
||||
}
|
||||
|
||||
public @NonNull String getTextCodeEntered() {
|
||||
//noinspection ConstantConditions Live data was given an initial value
|
||||
return textCodeEntered.getValue();
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_VERIFICATION_CODE);
|
||||
}
|
||||
|
||||
public String getCaptchaToken() {
|
||||
return captchaToken.getValue();
|
||||
private @Nullable String getCaptchaToken() {
|
||||
return registrationState.get(STATE_CAPTCHA);
|
||||
}
|
||||
|
||||
public boolean hasCaptchaToken() {
|
||||
return getCaptchaToken() != null;
|
||||
}
|
||||
|
||||
public String getRegistrationSecret() {
|
||||
return secret;
|
||||
private @NonNull String getRegistrationSecret() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_REGISTRATION_SECRET);
|
||||
}
|
||||
|
||||
public void onCaptchaResponse(String captchaToken) {
|
||||
this.captchaToken.setValue(captchaToken);
|
||||
public void setCaptchaResponse(@Nullable String captchaToken) {
|
||||
registrationState.set(STATE_CAPTCHA, captchaToken);
|
||||
}
|
||||
|
||||
public void clearCaptchaResponse() {
|
||||
captchaToken.setValue(null);
|
||||
private void clearCaptchaResponse() {
|
||||
setCaptchaResponse(null);
|
||||
}
|
||||
|
||||
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
|
||||
|
@ -102,13 +129,13 @@ public final class RegistrationViewModel extends ViewModel {
|
|||
|
||||
private void setViewState(NumberViewState numberViewState) {
|
||||
if (!numberViewState.equals(getNumber())) {
|
||||
number.setValue(numberViewState);
|
||||
registrationState.set(STATE_NUMBER, numberViewState);
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onVerificationCodeEntered(String code) {
|
||||
textCodeEntered.setValue(code);
|
||||
registrationState.set(STATE_VERIFICATION_CODE, code);
|
||||
}
|
||||
|
||||
public void onNumberDetected(int countryCode, String nationalNumber) {
|
||||
|
@ -118,67 +145,183 @@ public final class RegistrationViewModel extends ViewModel {
|
|||
.build());
|
||||
}
|
||||
|
||||
public String getFcmToken() {
|
||||
return fcmToken.getValue();
|
||||
private @Nullable String getFcmToken() {
|
||||
return registrationState.get(STATE_FCM_TOKEN);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setFcmToken(@Nullable String fcmToken) {
|
||||
this.fcmToken.setValue(fcmToken);
|
||||
registrationState.set(STATE_FCM_TOKEN, fcmToken);
|
||||
}
|
||||
|
||||
public void setWelcomeSkippedOnRestore() {
|
||||
restoreFlowShown.setValue(true);
|
||||
registrationState.set(STATE_RESTORE_FLOW_SHOWN, true);
|
||||
}
|
||||
|
||||
public boolean hasRestoreFlowBeenShown() {
|
||||
//noinspection ConstantConditions Live data was given an initial value
|
||||
return restoreFlowShown.getValue();
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_RESTORE_FLOW_SHOWN);
|
||||
}
|
||||
|
||||
public void markASuccessfulAttempt() {
|
||||
//noinspection ConstantConditions Live data was given an initial value
|
||||
successfulCodeRequestAttempts.setValue(successfulCodeRequestAttempts.getValue() + 1);
|
||||
private void markASuccessfulAttempt() {
|
||||
//noinspection ConstantConditions
|
||||
registrationState.set(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, (Integer) registrationState.get(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS) + 1);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
|
||||
return successfulCodeRequestAttempts;
|
||||
return registrationState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
}
|
||||
|
||||
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
|
||||
//noinspection ConstantConditions Live data was given an initial value
|
||||
return requestLimiter.getValue();
|
||||
private @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_REQUEST_RATE_LIMITER);
|
||||
}
|
||||
|
||||
public void updateLimiter() {
|
||||
requestLimiter.setValue(requestLimiter.getValue());
|
||||
private void updateLimiter() {
|
||||
registrationState.set(STATE_REQUEST_RATE_LIMITER, registrationState.get(STATE_REQUEST_RATE_LIMITER));
|
||||
}
|
||||
|
||||
public @Nullable TokenData getKeyBackupCurrentToken() {
|
||||
return kbsTokenData.getValue();
|
||||
return registrationState.get(STATE_KBS_TOKEN);
|
||||
}
|
||||
|
||||
public void setKeyBackupTokenData(TokenData tokenData) {
|
||||
kbsTokenData.setValue(tokenData);
|
||||
public void setKeyBackupTokenData(@Nullable TokenData tokenData) {
|
||||
registrationState.set(STATE_KBS_TOKEN, tokenData);
|
||||
}
|
||||
|
||||
public LiveData<Long> getLockedTimeRemaining() {
|
||||
return lockedTimeRemaining;
|
||||
return registrationState.getLiveData(STATE_TIME_REMAINING, 0L);
|
||||
}
|
||||
|
||||
public LiveData<Long> getCanCallAtTime() {
|
||||
return canCallAtTime;
|
||||
return registrationState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L);
|
||||
}
|
||||
|
||||
public void setLockedTimeRemaining(long lockedTimeRemaining) {
|
||||
this.lockedTimeRemaining.setValue(lockedTimeRemaining);
|
||||
registrationState.set(STATE_TIME_REMAINING, lockedTimeRemaining);
|
||||
}
|
||||
|
||||
public void onStartEnterCode() {
|
||||
canCallAtTime.setValue(System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
|
||||
registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
|
||||
}
|
||||
|
||||
public void onCallRequested() {
|
||||
canCallAtTime.setValue(System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
|
||||
private void onCallRequested() {
|
||||
registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
|
||||
}
|
||||
|
||||
public void setIsReregister(boolean isReregister) {
|
||||
registrationState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
|
||||
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull Mode mode) {
|
||||
String captcha = getCaptchaToken();
|
||||
clearCaptchaResponse();
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
onCallRequested();
|
||||
} else if (!getRequestLimiter().canRequest(mode, getNumber().getE164Number(), System.currentTimeMillis())) {
|
||||
return Single.just(RequestVerificationCodeResponseProcessor.forLocalRateLimit());
|
||||
}
|
||||
|
||||
return verifyAccountRepository.requestVerificationCode(getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
mode,
|
||||
captcha)
|
||||
.map(RequestVerificationCodeResponseProcessor::new)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
markASuccessfulAttempt();
|
||||
setFcmToken(processor.getResult().getFcmToken().orNull());
|
||||
getRequestLimiter().onSuccessfulRequest(mode, getNumber().getE164Number(), System.currentTimeMillis());
|
||||
} else {
|
||||
getRequestLimiter().onUnsuccessfulRequest();
|
||||
}
|
||||
updateLimiter();
|
||||
});
|
||||
}
|
||||
|
||||
public Single<VerifyAccountResponseProcessor> verifyCodeAndRegisterAccountWithoutRegistrationLock(@NonNull String code) {
|
||||
onVerificationCodeEntered(code);
|
||||
|
||||
RegistrationData registrationData = new RegistrationData(getTextCodeEntered(),
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken());
|
||||
|
||||
return verifyAccountRepository.verifyAccount(registrationData)
|
||||
.map(VerifyAccountResponseWithoutKbs::new)
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return registrationRepository.registerAccountWithoutRegistrationLock(registrationData, processor.getResult())
|
||||
.map(VerifyAccountResponseWithoutKbs::new);
|
||||
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
return kbsRepository.getToken(processor.getLockedException().getBasicStorageCredentials())
|
||||
.map(r -> r.getResult().isPresent() ? new VerifyAccountResponseWithSuccessfulKbs(processor.getResponse(), r.getResult().get())
|
||||
: new VerifyAccountResponseWithFailedKbs(r));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
setKeyBackupTokenData(processor.getTokenData());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public Single<VerifyCodeWithRegistrationLockResponseProcessor> verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) {
|
||||
RegistrationData registrationData = new RegistrationData(getTextCodeEntered(),
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken());
|
||||
|
||||
TokenData kbsTokenData = Objects.requireNonNull(getKeyBackupCurrentToken());
|
||||
|
||||
return verifyAccountRepository.verifyAccountWithPin(registrationData, pin, kbsTokenData)
|
||||
.map(r -> new VerifyCodeWithRegistrationLockResponseProcessor(r, kbsTokenData))
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return registrationRepository.registerAccountWithRegistrationLock(registrationData, processor.getResult(), pin)
|
||||
.map(processor::updatedIfRegistrationFailed);
|
||||
} else if (processor.wrongPin()) {
|
||||
TokenData newToken = TokenData.withResponse(kbsTokenData, processor.getTokenResponse());
|
||||
return Single.just(new VerifyCodeWithRegistrationLockResponseProcessor(processor.getResponse(), newToken));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.wrongPin()) {
|
||||
setKeyBackupTokenData(processor.getToken());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(ApplicationDependencies.getApplication()),
|
||||
new KbsRepository(),
|
||||
new RegistrationRepository(ApplicationDependencies.getApplication())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import com.dd.CircularProgressButton
|
||||
|
||||
object CircularProgressButtonUtil {
|
||||
|
||||
@JvmStatic
|
||||
fun setSpinning(button: CircularProgressButton?) {
|
||||
button?.apply {
|
||||
isClickable = false
|
||||
isIndeterminateProgressMode = true
|
||||
progress = 50
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun cancelSpinning(button: CircularProgressButton?) {
|
||||
button?.apply {
|
||||
progress = 0
|
||||
isIndeterminateProgressMode = false
|
||||
isClickable = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ class LifecycleDisposable : DefaultLifecycleObserver {
|
|||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
owner.lifecycle.removeObserver(this)
|
||||
disposables.clear()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
|
||||
public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private static final String TAG = Log.tag(SignalUncaughtExceptionHandler.class);
|
||||
|
@ -20,6 +22,10 @@ public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionH
|
|||
|
||||
@Override
|
||||
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
|
||||
if (e instanceof OnErrorNotImplementedException) {
|
||||
e = e.getCause();
|
||||
}
|
||||
|
||||
Log.e(TAG, "", e, true);
|
||||
SignalStore.blockUntilAllWritesFinished();
|
||||
Log.blockUntilAllWritesFinished();
|
||||
|
|
|
@ -116,14 +116,7 @@
|
|||
android:id="@+id/countryPickerFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
|
||||
android:label="fragment_country_picker"
|
||||
tools:layout="@layout/fragment_registration_country_picker">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_countrySelected"
|
||||
app:popUpTo="@id/countryPickerFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
tools:layout="@layout/fragment_registration_country_picker"/>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/enterCodeFragment"
|
||||
|
@ -217,14 +210,7 @@
|
|||
android:id="@+id/captchaFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
|
||||
android:label="fragment_captcha"
|
||||
tools:layout="@layout/fragment_registration_captcha">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_captchaComplete"
|
||||
app:popUpTo="@id/captchaFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
tools:layout="@layout/fragment_registration_captcha"/>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/restoreBackupFragment"
|
||||
|
|
|
@ -1513,6 +1513,7 @@
|
|||
<string name="RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends">Signal needs access to your contacts in order to connect with friends, exchange messages, and make secure calls</string>
|
||||
<string name="RegistrationActivity_rate_limited_to_service">You\'ve made too many attempts to register this number. Please try again later.</string>
|
||||
<string name="RegistrationActivity_unable_to_connect_to_service">Unable to connect to service. Please check network connection and try again.</string>
|
||||
<string name="RegistrationActivity_call_requested">Call requested</string>
|
||||
<plurals name="RegistrationActivity_debug_log_hint">
|
||||
<item quantity="one">You are now %d step away from submitting a debug log.</item>
|
||||
<item quantity="other">You are now %d steps away from submitting a debug log.</item>
|
||||
|
@ -3010,6 +3011,7 @@
|
|||
<string name="RegistrationActivity_call_me_instead_available_in">Call me instead \n (Available in %1$02d:%2$02d)</string>
|
||||
<string name="RegistrationActivity_contact_signal_support">Contact Signal Support</string>
|
||||
<string name="RegistrationActivity_code_support_subject">Signal Registration - Verification Code for Android</string>
|
||||
<string name="RegistrationActivity_incorrect_code">Incorrect code</string>
|
||||
<string name="BackupUtil_never">Never</string>
|
||||
<string name="BackupUtil_unknown">Unknown</string>
|
||||
<string name="preferences_app_protection__see_my_phone_number">See my phone number</string>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class LocalCodeRequestRateLimiterTest {
|
||||
|
||||
|
@ -12,63 +12,63 @@ public final class LocalCodeRequestRateLimiterTest {
|
|||
public void initially_can_request() {
|
||||
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cant_request_within_same_time_period() {
|
||||
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
|
||||
|
||||
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
|
||||
limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
|
||||
|
||||
assertFalse(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
|
||||
assertFalse(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_request_within_same_time_period_if_different_number() {
|
||||
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
|
||||
|
||||
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
|
||||
limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_request_within_same_time_period_if_different_mode() {
|
||||
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
|
||||
|
||||
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
|
||||
limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_request_after_time_period() {
|
||||
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
|
||||
|
||||
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
|
||||
limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_request_within_same_time_period_if_an_unsuccessful_request_is_seen() {
|
||||
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
|
||||
|
||||
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
|
||||
limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
|
||||
|
||||
limiter.onUnsuccessfulRequest();
|
||||
|
||||
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000));
|
||||
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000));
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ import org.whispersystems.signalservice.api.storage.StorageKey;
|
|||
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
|
@ -61,6 +62,7 @@ import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
|||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||
|
@ -209,10 +211,14 @@ public class SignalServiceAccountManager {
|
|||
* @param androidSmsRetrieverSupported
|
||||
* @param captchaToken If the user has done a CAPTCHA, include this.
|
||||
* @param challenge If present, it can bypass the CAPTCHA.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
|
||||
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
|
||||
public ServiceResponse<RequestVerificationCodeResponse> requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge, Optional<String> fcmToken) {
|
||||
try {
|
||||
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
|
||||
return ServiceResponse.forResult(new RequestVerificationCodeResponse(fcmToken), 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -222,10 +228,14 @@ public class SignalServiceAccountManager {
|
|||
* @param locale
|
||||
* @param captchaToken If the user has done a CAPTCHA, include this.
|
||||
* @param challenge If present, it can bypass the CAPTCHA.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
|
||||
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
|
||||
public ServiceResponse<RequestVerificationCodeResponse> requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge, Optional<String> fcmToken) {
|
||||
try {
|
||||
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
|
||||
return ServiceResponse.forResult(new RequestVerificationCodeResponse(fcmToken), 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -234,32 +244,76 @@ public class SignalServiceAccountManager {
|
|||
* @param verificationCode The verification code received via SMS or Voice
|
||||
* (see {@link #requestSmsVerificationCode} and
|
||||
* {@link #requestVoiceVerificationCode}).
|
||||
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
|
||||
* concatenated.
|
||||
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
|
||||
* This value should remain consistent across registrations for the
|
||||
* same install, but probabilistically differ across registrations
|
||||
* for separate installs.
|
||||
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
|
||||
* @return The UUID of the user that was registered.
|
||||
* @throws IOException for various HTTP and networking errors
|
||||
*/
|
||||
public ServiceResponse<VerifyAccountResponse> verifyAccount(String verificationCode,
|
||||
int signalProtocolRegistrationId,
|
||||
boolean fetchesMessages,
|
||||
byte[] unidentifiedAccessKey,
|
||||
boolean unrestrictedUnidentifiedAccess,
|
||||
AccountAttributes.Capabilities capabilities,
|
||||
boolean discoverableByPhoneNumber)
|
||||
{
|
||||
try {
|
||||
VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode,
|
||||
null,
|
||||
signalProtocolRegistrationId,
|
||||
fetchesMessages,
|
||||
null,
|
||||
null,
|
||||
unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess,
|
||||
capabilities,
|
||||
discoverableByPhoneNumber);
|
||||
return ServiceResponse.forResult(response, 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a Signal Service account with a received SMS or voice verification code with
|
||||
* registration lock.
|
||||
*
|
||||
* @param verificationCode The verification code received via SMS or Voice
|
||||
* (see {@link #requestSmsVerificationCode} and
|
||||
* {@link #requestVoiceVerificationCode}).
|
||||
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
|
||||
* This value should remain consistent across registrations for the
|
||||
* same install, but probabilistically differ across registrations
|
||||
* for separate installs.
|
||||
* @param registrationLock Only supply if found on KBS.
|
||||
* @return The UUID of the user that was registered.
|
||||
* @throws IOException
|
||||
*/
|
||||
public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||
AccountAttributes.Capabilities capabilities,
|
||||
boolean discoverableByPhoneNumber)
|
||||
throws IOException
|
||||
public ServiceResponse<VerifyAccountResponse> verifyAccountWithRegistrationLockPin(String verificationCode,
|
||||
int signalProtocolRegistrationId,
|
||||
boolean fetchesMessages,
|
||||
String registrationLock,
|
||||
byte[] unidentifiedAccessKey,
|
||||
boolean unrestrictedUnidentifiedAccess,
|
||||
AccountAttributes.Capabilities capabilities,
|
||||
boolean discoverableByPhoneNumber)
|
||||
{
|
||||
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
|
||||
signalProtocolRegistrationId,
|
||||
fetchesMessages,
|
||||
pin, registrationLock,
|
||||
unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess,
|
||||
capabilities,
|
||||
discoverableByPhoneNumber);
|
||||
try {
|
||||
VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode,
|
||||
null,
|
||||
signalProtocolRegistrationId,
|
||||
fetchesMessages,
|
||||
null,
|
||||
registrationLock,
|
||||
unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess,
|
||||
capabilities,
|
||||
discoverableByPhoneNumber);
|
||||
return ServiceResponse.forResult(response, 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package org.whispersystems.signalservice.api.push.exceptions;
|
||||
|
||||
/**
|
||||
* Thrown when self limiting networking.
|
||||
*/
|
||||
public final class LocalRateLimitException extends Exception {
|
||||
public LocalRateLimitException() { }
|
||||
}
|
|
@ -3,6 +3,7 @@ package org.whispersystems.signalservice.internal;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -92,8 +93,17 @@ public final class ServiceResponse<Result> {
|
|||
return forUnknownError(throwable.getCause());
|
||||
} else if (throwable instanceof NonSuccessfulResponseCodeException) {
|
||||
return forApplicationError(throwable, ((NonSuccessfulResponseCodeException) throwable).getCode(), null);
|
||||
} else if (throwable instanceof PushNetworkException && throwable.getCause() != null) {
|
||||
return forUnknownError(throwable.getCause());
|
||||
} else {
|
||||
return forExecutionError(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T, I> ServiceResponse<T> coerceError(ServiceResponse<I> response) {
|
||||
if (response.applicationError.isPresent()) {
|
||||
return ServiceResponse.forApplicationError(response.applicationError.get(), response.status, response.body.orNull());
|
||||
}
|
||||
return ServiceResponse.forExecutionError(response.executionError.orNull());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,10 @@ public abstract class ServiceResponseProcessor<T> {
|
|||
return response.getStatus() == 401 || response.getStatus() == 403;
|
||||
}
|
||||
|
||||
protected boolean captchaRequired() {
|
||||
return response.getStatus() == 402;
|
||||
}
|
||||
|
||||
protected boolean notFound() {
|
||||
return response.getStatus() == 404;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public final class RequestVerificationCodeResponse {
|
||||
private final Optional<String> fcmToken;
|
||||
|
||||
public RequestVerificationCodeResponse(Optional<String> fcmToken) {
|
||||
this.fcmToken = fcmToken;
|
||||
}
|
||||
|
||||
public Optional<String> getFcmToken() {
|
||||
return fcmToken;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package org.whispersystems.signalservice.internal.websocket;
|
|||
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
|
||||
|
@ -72,6 +73,8 @@ public final class DefaultErrorMapper implements ErrorMapper {
|
|||
case 401:
|
||||
case 403:
|
||||
return new AuthorizationFailedException(status, "Authorization failed!");
|
||||
case 402:
|
||||
return new CaptchaRequiredException();
|
||||
case 404:
|
||||
return new NotFoundException("Not found");
|
||||
case 409:
|
||||
|
|
Ładowanie…
Reference in New Issue