mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add support for restoring usernames post-registration.
This commit is contained in:
@@ -157,7 +157,6 @@ public final class JobManagerFactories {
|
||||
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
|
||||
put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory());
|
||||
put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory());
|
||||
put(NewRegistrationUsernameSyncJob.KEY, new NewRegistrationUsernameSyncJob.Factory());
|
||||
put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory());
|
||||
put(OptimizeMessageSearchIndexJob.KEY, new OptimizeMessageSearchIndexJob.Factory());
|
||||
put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory());
|
||||
@@ -179,6 +178,7 @@ public final class JobManagerFactories {
|
||||
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
|
||||
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
|
||||
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
|
||||
put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory());
|
||||
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||
put(RefreshCallLinkDetailsJob.KEY, new RefreshCallLinkDetailsJob.Factory());
|
||||
put(RefreshSvrCredentialsJob.KEY, new RefreshSvrCredentialsJob.Factory());
|
||||
@@ -309,6 +309,7 @@ public final class JobManagerFactories {
|
||||
put("SmsReceiveJob", new FailingJob.Factory());
|
||||
put("StoryReadStateMigrationJob", new PassingMigrationJob.Factory());
|
||||
put("GroupV1MigrationJob", new FailingJob.Factory());
|
||||
put("NewRegistrationUsernameSyncJob", new FailingJob.Factory());
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* If a user registers and the storage sync service doesn't contain a username,
|
||||
* then we should delete our username from the server.
|
||||
*/
|
||||
class NewRegistrationUsernameSyncJob private constructor(parameters: Parameters) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(NewRegistrationUsernameSyncJob::class.java)
|
||||
|
||||
const val KEY = "NewRegistrationUsernameSyncJob"
|
||||
}
|
||||
|
||||
constructor() : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.build()
|
||||
)
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
override fun onRun() {
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean {
|
||||
return e is IOException
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<NewRegistrationUsernameSyncJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): NewRegistrationUsernameSyncJob {
|
||||
return NewRegistrationUsernameSyncJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
class ReclaimUsernameAndLinkJob private constructor(parameters: Job.Parameters) : Job(parameters) {
|
||||
companion object {
|
||||
const val KEY = "UsernameAndLinkRestoreJob"
|
||||
|
||||
private val TAG = Log.tag(ReclaimUsernameAndLinkJob::class.java)
|
||||
}
|
||||
|
||||
constructor() : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setLifespan(30.days.inWholeMilliseconds)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.build()
|
||||
)
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
return when (UsernameRepository.reclaimUsernameIfNecessary()) {
|
||||
UsernameRepository.UsernameReclaimResult.SUCCESS -> Result.success()
|
||||
UsernameRepository.UsernameReclaimResult.PERMANENT_ERROR -> Result.success()
|
||||
UsernameRepository.UsernameReclaimResult.NETWORK_ERROR -> Result.retry(BackoffUtil.exponentialBackoff(runAttempt + 1, FeatureFlags.getDefaultMaxBackoff()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<ReclaimUsernameAndLinkJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): ReclaimUsernameAndLinkJob {
|
||||
return ReclaimUsernameAndLinkJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,7 @@ public class RefreshOwnProfileJob extends BaseJob {
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
static void checkUsernameIsInSync() {
|
||||
private static void checkUsernameIsInSync() {
|
||||
boolean validated = false;
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,13 +4,17 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.AccountValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
@@ -111,6 +115,16 @@ public class StorageAccountRestoreJob extends BaseJob {
|
||||
SignalDatabase.getRawDatabase().endTransaction();
|
||||
}
|
||||
|
||||
// We will try to reclaim the username here, as early as possible, but the registration flow also enqueues a username restore job,
|
||||
// so failing here isn't a huge deal
|
||||
if (SignalStore.account().getUsername() != null) {
|
||||
Log.i(TAG, "Attempting to reclaim username...");
|
||||
UsernameRepository.UsernameReclaimResult result = UsernameRepository.reclaimUsernameIfNecessary();
|
||||
Log.i(TAG, "Username reclaim result: " + result.name());
|
||||
} else {
|
||||
Log.i(TAG, "No username to reclaim.");
|
||||
}
|
||||
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
|
||||
if (accountRecord.getAvatarUrlPath().isPresent()) {
|
||||
|
||||
@@ -39,6 +39,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
private static final String LAST_CONSISTENCY_CHECK_TIME = "misc.last_consistency_check_time";
|
||||
private static final String SERVER_TIME_OFFSET = "misc.server_time_offset";
|
||||
private static final String LAST_SERVER_TIME_OFFSET_UPDATE = "misc.last_server_time_offset_update";
|
||||
private static final String NEEDS_USERNAME_RESTORE = "misc.needs_username_restore";
|
||||
|
||||
MiscellaneousValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -47,6 +48,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
@Override
|
||||
void onFirstEverAppLaunch() {
|
||||
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
|
||||
putBoolean(NEEDS_USERNAME_RESTORE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -331,4 +333,15 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
public long getLastKnownServerTimeOffsetUpdateTime() {
|
||||
return getLong(LAST_SERVER_TIME_OFFSET_UPDATE, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should attempt to restore the user's username and link.
|
||||
*/
|
||||
public boolean needsUsernameRestore() {
|
||||
return getBoolean(NEEDS_USERNAME_RESTORE, false);
|
||||
}
|
||||
|
||||
public void setNeedsUsernameRestore(boolean value) {
|
||||
putBoolean(NEEDS_USERNAME_RESTORE, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
|
||||
@@ -152,7 +152,7 @@ object SvrRepository {
|
||||
ApplicationDependencies
|
||||
.getJobManager()
|
||||
.startChain(StorageSyncJob())
|
||||
.then(NewRegistrationUsernameSyncJob())
|
||||
.then(ReclaimUsernameAndLinkJob())
|
||||
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10))
|
||||
stopwatch.split("contact-restore")
|
||||
|
||||
|
||||
@@ -121,7 +121,52 @@ object UsernameRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the username and username link from the local user's account
|
||||
* Attempts to reclaim the username that is currently stored on disk if necessary.
|
||||
* This is intended to be used after registration.
|
||||
*
|
||||
* This method call may result in mutating [SignalStore] state.
|
||||
*/
|
||||
@WorkerThread
|
||||
@JvmStatic
|
||||
fun reclaimUsernameIfNecessary(): UsernameReclaimResult {
|
||||
if (!SignalStore.misc().needsUsernameRestore()) {
|
||||
Log.d(TAG, "[reclaimUsernameIfNecessary] No need to restore username. Skipping.")
|
||||
return UsernameReclaimResult.SUCCESS
|
||||
}
|
||||
|
||||
val username = SignalStore.account().username
|
||||
val link = SignalStore.account().usernameLink
|
||||
|
||||
if (username == null || link == null) {
|
||||
Log.d(TAG, "[reclaimUsernameIfNecessary] No username or link to restore. Skipping.")
|
||||
SignalStore.misc().setNeedsUsernameRestore(false)
|
||||
return UsernameReclaimResult.SUCCESS
|
||||
}
|
||||
|
||||
val result = reclaimUsernameIfNecessaryInternal(Username(username), link)
|
||||
|
||||
when (result) {
|
||||
UsernameReclaimResult.SUCCESS -> {
|
||||
Log.i(TAG, "[reclaimUsernameIfNecessary] Successfully reclaimed username and link.")
|
||||
SignalStore.misc().setNeedsUsernameRestore(false)
|
||||
}
|
||||
|
||||
UsernameReclaimResult.PERMANENT_ERROR -> {
|
||||
Log.w(TAG, "[reclaimUsernameIfNecessary] Permanently failed to reclaim username and link. User will see an error.")
|
||||
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED
|
||||
SignalStore.misc().setNeedsUsernameRestore(false)
|
||||
}
|
||||
|
||||
UsernameReclaimResult.NETWORK_ERROR -> {
|
||||
Log.w(TAG, "[reclaimUsernameIfNecessary] Hit a transient network error while trying to reclaim username and link.")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the username from the local user's account
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deleteUsernameAndLink(): Single<UsernameDeleteResult> {
|
||||
@@ -418,10 +463,36 @@ object UsernameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@JvmStatic
|
||||
private fun reclaimUsernameIfNecessaryInternal(username: Username, usernameLinkComponents: UsernameLinkComponents): UsernameReclaimResult {
|
||||
try {
|
||||
accountManager.reclaimUsernameAndLink(username, usernameLinkComponents)
|
||||
} catch (e: UsernameTakenException) {
|
||||
Log.w(TAG, "[reclaimUsername] Username gone.")
|
||||
return UsernameReclaimResult.PERMANENT_ERROR
|
||||
} catch (e: UsernameIsNotReservedException) {
|
||||
Log.w(TAG, "[reclaimUsername] Username was not reserved.")
|
||||
return UsernameReclaimResult.PERMANENT_ERROR
|
||||
} catch (e: BaseUsernameException) {
|
||||
Log.w(TAG, "[reclaimUsername] Invalid username.")
|
||||
return UsernameReclaimResult.PERMANENT_ERROR
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[reclaimUsername] Network error.", e)
|
||||
return UsernameReclaimResult.NETWORK_ERROR
|
||||
}
|
||||
|
||||
return UsernameReclaimResult.SUCCESS
|
||||
}
|
||||
|
||||
enum class UsernameSetResult {
|
||||
SUCCESS, USERNAME_UNAVAILABLE, USERNAME_INVALID, NETWORK_ERROR, CANDIDATE_GENERATION_ERROR
|
||||
}
|
||||
|
||||
enum class UsernameReclaimResult {
|
||||
SUCCESS, PERMANENT_ERROR, NETWORK_ERROR
|
||||
}
|
||||
|
||||
enum class UsernameDeleteResult {
|
||||
SUCCESS, NETWORK_ERROR
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -56,7 +56,7 @@ public final class RegistrationLockFragment extends BaseRegistrationLockFragment
|
||||
ApplicationDependencies
|
||||
.getJobManager()
|
||||
.startChain(new StorageSyncJob())
|
||||
.then(new NewRegistrationUsernameSyncJob())
|
||||
.then(new ReclaimUsernameAndLinkJob())
|
||||
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
|
||||
stopwatch.split("ContactRestore");
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.savedstate.SavedStateRegistryOwner;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -402,7 +402,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
|
||||
ApplicationDependencies
|
||||
.getJobManager()
|
||||
.startChain(new StorageSyncJob())
|
||||
.then(new NewRegistrationUsernameSyncJob())
|
||||
.then(new ReclaimUsernameAndLinkJob())
|
||||
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
|
||||
stopwatch.split("ContactRestore");
|
||||
|
||||
|
||||
@@ -794,6 +794,17 @@ public class SignalServiceAccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
public UsernameLinkComponents reclaimUsernameAndLink(Username username, UsernameLinkComponents linkComponents) throws IOException {
|
||||
try {
|
||||
UsernameLink link = username.generateLink(linkComponents.getEntropy());
|
||||
UUID serverId = this.pushServiceSocket.confirmUsernameAndCreateNewLink(username, link);
|
||||
|
||||
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||
} catch (BaseUsernameException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public UsernameLinkComponents updateUsernameLink(UsernameLink newUsernameLink) throws IOException {
|
||||
UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithoutPadding(newUsernameLink.getEncryptedUsername()), true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user