Add relink support and fix sad paths in all link flows.

Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
Cody Henthorne
2026-07-07 15:17:35 -04:00
committed by GitHub
parent 384344c91b
commit ecbdde592e
33 changed files with 356 additions and 87 deletions
@@ -13,9 +13,11 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.AppForegroundObserver;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.devicetransfer.TransferStatus;
import org.signal.registration.RegistrationRoute;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
@@ -30,11 +32,10 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
import org.thoughtcrime.securesms.util.Environment;
import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.signal.core.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.Environment;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@@ -57,6 +58,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_TRANSFER_OR_RESTORE = 11;
private static final int STATE_RESUME_LINKING_REG = 12;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -155,6 +157,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
case STATE_RESUME_LINKING_REG: return getResumeLinkedRegistrationIntent();
default: return null;
}
}
@@ -168,6 +171,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (shouldResumeLinkingRegistration()) {
return STATE_RESUME_LINKING_REG;
} else if (userCanTransferOrRestore()) {
return STATE_TRANSFER_OR_RESTORE;
} else if (SignalStore.storageService().getNeedsAccountRestore()) {
@@ -192,6 +197,14 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
RestoreDecisionStateUtil.isDecisionPending(SignalStore.registration().getRestoreDecisionState());
}
private boolean shouldResumeLinkingRegistration() {
return Environment.USE_NEW_REGISTRATION &&
SignalStore.account().isRegistered() &&
!SignalStore.account().isPrimaryDevice() &&
!SignalStore.registration().isRegistrationComplete() &&
RestoreDecisionStateUtil.isDecisionPending(SignalStore.registration().getRestoreDecisionState());
}
private boolean userMustCreateSignalPin() {
return !SignalStore.registration().isRegistrationComplete() &&
!SignalStore.svr().hasPin() &&
@@ -246,6 +259,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return getRoutedIntent(intent, MainActivity.clearTop(this));
}
private Intent getResumeLinkedRegistrationIntent() {
return org.signal.registration.RegistrationActivity.createIntent(this, MainActivity.clearTop(this), RegistrationRoute.MessageSync.INSTANCE);
}
private Intent getCreateProfileNameIntent() {
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.net.SignalNetwork;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.signal.core.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
@@ -72,6 +73,11 @@ public class FcmRefreshJob extends BaseJob {
@Override
public void onRun() throws Exception {
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.i(TAG, "No longer authorized. Ignoring.");
return;
}
Log.i(TAG, "Reregistering FCM...");
boolean playServicesMissing = PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.MISSING ;
@@ -91,6 +91,11 @@ public class RefreshAttributesJob extends BaseJob {
return;
}
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.i(TAG, "No longer authorized. Ignoring.");
return;
}
if (!forced && hasRefreshedThisAppCycle) {
Log.d(TAG, "Already refreshed this app cycle. Skipping.");
return;
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.io.IOException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
@@ -24,7 +25,7 @@ class RefreshSvrCredentialsJob private constructor(parameters: Parameters) : Bas
@JvmStatic
fun enqueueIfNecessary() {
if (SignalStore.svr.hasPin() && SignalStore.account.isRegistered) {
if (SignalStore.svr.hasPin() && SignalStore.account.isRegistered && !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)) {
val lastTimestamp = SignalStore.svr.lastRefreshAuthTimestamp
if (lastTimestamp + FREQUENCY.inWholeMilliseconds < System.currentTimeMillis() || lastTimestamp > System.currentTimeMillis()) {
AppDependencies.jobManager.add(RefreshSvrCredentialsJob())
@@ -55,6 +56,11 @@ class RefreshSvrCredentialsJob private constructor(parameters: Parameters) : Bas
return
}
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.i(TAG, "No longer authorized. Ignoring.")
return
}
SvrRepository.refreshAndStoreAuthorization()
}
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import kotlin.time.Duration.Companion.days
@@ -44,6 +45,11 @@ class RemoteConfigRefreshJob private constructor(parameters: Parameters) : Job(p
return Result.success()
}
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.i(TAG, "No longer authorized. Ignoring.")
return Result.success()
}
return when (val result = SignalNetwork.remoteConfig.getRemoteConfig(SignalStore.remoteConfig.eTag)) {
is NetworkResult.Success -> {
RemoteConfig.update(result.result.config)
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.keyvalue.CertificateType;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.SignalNetwork;
import org.thoughtcrime.securesms.util.ExceptionHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import java.io.IOException;
@@ -56,6 +57,11 @@ public final class RotateCertificateJob extends BaseJob {
return;
}
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.i(TAG, "No longer authorized. Ignoring.");
return;
}
synchronized (RotateCertificateJob.class) {
Collection<CertificateType> certificateTypes = SignalStore.phoneNumberPrivacy()
.getAllCertificateTypes();
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncValidations
import org.thoughtcrime.securesms.storage.StoryDistributionListRecordProcessor
import org.thoughtcrime.securesms.transport.RetryLaterException
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
@@ -185,6 +186,11 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
return
}
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
Log.i(TAG, "No longer authorized. Ignoring.")
return
}
if (!Recipient.self().hasE164 || !Recipient.self().hasServiceId) {
Log.w(TAG, "Missing E164 or ACI!")
return
@@ -12,6 +12,7 @@ import androidx.activity.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.ActivityNavigator
import org.signal.registration.RegistrationRoute
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
@@ -84,7 +85,7 @@ class RegistrationActivity : BaseActivity() {
@JvmStatic
fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent {
return if (Environment.USE_NEW_REGISTRATION) {
org.signal.registration.RegistrationActivity.createIntent(context, MainActivity.clearTop(context))
org.signal.registration.RegistrationActivity.createIntent(context, nextIntent = MainActivity.clearTop(context))
} else {
Intent(context, RegistrationActivity::class.java).apply {
putExtra(RE_REGISTRATION_EXTRA, false)
@@ -96,7 +97,7 @@ class RegistrationActivity : BaseActivity() {
@JvmStatic
fun newIntentForReRegistration(context: Context): Intent {
return if (Environment.USE_NEW_REGISTRATION) {
org.signal.registration.RegistrationActivity.createIntent(context, MainActivity.clearTop(context))
org.signal.registration.RegistrationActivity.createIntent(context, nextIntent = MainActivity.clearTop(context))
} else {
Intent(context, RegistrationActivity::class.java).apply {
putExtra(RE_REGISTRATION_EXTRA, true)
@@ -107,7 +108,11 @@ class RegistrationActivity : BaseActivity() {
@JvmStatic
fun newIntentForReLinkDevice(context: Context): Intent {
return if (Environment.USE_NEW_REGISTRATION) {
org.signal.registration.RegistrationActivity.createIntent(context, MainActivity.clearTop(context))
org.signal.registration.RegistrationActivity.createIntent(
context = context,
nextIntent = MainActivity.clearTop(context),
startDestination = RegistrationRoute.LinkAccount(showCreateAccount = false)
)
} else {
Intent(context, RegistrationActivity::class.java)
}
@@ -87,7 +87,7 @@ class RegisterLinkDeviceQrViewModel : ViewModel() {
}
return ProvisioningSocket.start<ProvisionMessage>(
mode = ProvisioningSocket.Mode.LINK,
mode = ProvisioningSocket.Mode.Link(linkAndSyncCapable = true),
identityKeyPair = IdentityKeyPair.generate(),
configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration(),
handler = { id, t ->
@@ -122,7 +122,7 @@ class RestoreViaQrViewModel : ViewModel() {
}
return ProvisioningSocket.start<RegistrationProvisionMessage>(
mode = ProvisioningSocket.Mode.REREG,
mode = ProvisioningSocket.Mode.Rereg,
identityKeyPair = IdentityKeyPair.generate(),
configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration(),
handler = { id, t ->
@@ -771,7 +771,7 @@ class AppRegistrationNetworkController(
fun startSocket() {
val handle = ProvisioningSocket.start<RegistrationProvisionMessage>(
mode = ProvisioningSocket.Mode.REREG,
mode = ProvisioningSocket.Mode.Rereg,
identityKeyPair = IdentityKeyPair.generate(),
configuration = configuration,
handler = { id, t ->
@@ -848,13 +848,13 @@ class AppRegistrationNetworkController(
}
}
override fun startLinkDeviceProvisioning(): Flow<LinkDeviceProvisioningEvent> = callbackFlow {
override fun startLinkDeviceProvisioning(allowLinkAndSync: Boolean): Flow<LinkDeviceProvisioningEvent> = callbackFlow {
val socketHandles = mutableListOf<Closeable>()
val configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration()
fun startSocket() {
val handle = ProvisioningSocket.start<ProvisionMessage>(
mode = ProvisioningSocket.Mode.LINK,
mode = ProvisioningSocket.Mode.Link(linkAndSyncCapable = allowLinkAndSync),
identityKeyPair = IdentityKeyPair.generate(),
configuration = configuration,
handler = { id, t ->
@@ -91,7 +91,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
val pni = SignalStore.account.pni ?: return@withContext null
val e164 = SignalStore.account.e164 ?: return@withContext null
val servicePassword = SignalStore.account.servicePassword ?: return@withContext null
val aep = SignalStore.account.accountEntropyPool ?: return@withContext null
val aep = SignalStore.account.accountEntropyPool
val aciIdentityKeyPair = SignalStore.account.aciIdentityKey
val pniIdentityKeyPair = SignalStore.account.pniIdentityKey