Add device transfer flow to regV5.

This commit is contained in:
Greyson Parrelli
2026-06-09 17:21:47 -04:00
committed by Cody Henthorne
parent 566c2d5838
commit 754dd15c94
48 changed files with 2820 additions and 66 deletions
@@ -10,6 +10,7 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.NewDeviceRestoreStatus;
import org.signal.devicetransfer.ServerTask;
import org.thoughtcrime.securesms.AppInitialization;
import org.thoughtcrime.securesms.backup.BackupEvent;
@@ -29,7 +30,10 @@ import java.io.InputStream;
* Performs the restore with the backup data coming in over the input stream. Used in
* conjunction with {@link org.signal.devicetransfer.DeviceToDeviceTransferService}.
*/
final class NewDeviceServerTask implements ServerTask {
public final class NewDeviceServerTask implements ServerTask {
public NewDeviceServerTask() {}
private static final String TAG = Log.tag(NewDeviceServerTask.class);
@@ -62,13 +66,13 @@ final class NewDeviceServerTask implements ServerTask {
Log.i(TAG, "Backup restore complete.");
} catch (FullBackupImporter.DatabaseDowngradeException e) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_VERSION_DOWNGRADE));
EventBus.getDefault().post(new NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.FAILURE_VERSION_DOWNGRADE));
} catch (FullBackupImporter.ForeignKeyViolationException e) {
Log.w(TAG, "Failed due to foreign key constraint violations.", e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_FOREIGN_KEY));
EventBus.getDefault().post(new NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.FAILURE_FOREIGN_KEY));
} catch (IOException e) {
Log.w(TAG, e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_UNKNOWN));
EventBus.getDefault().post(new NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.FAILURE_UNKNOWN));
} finally {
EventBus.getDefault().unregister(this);
DataRestoreConstraint.setRestoringData(false);
@@ -77,42 +81,16 @@ final class NewDeviceServerTask implements ServerTask {
long end = System.currentTimeMillis();
Log.i(TAG, "Receive took: " + (end - start));
EventBus.getDefault().post(new Status(0, Status.State.RESTORE_COMPLETE));
EventBus.getDefault().post(new NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.RESTORE_COMPLETE));
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onEvent(BackupEvent event) {
if (event.getType() == BackupEvent.Type.PROGRESS) {
EventBus.getDefault().post(new Status(event.getCount(), Status.State.IN_PROGRESS));
EventBus.getDefault().post(new NewDeviceRestoreStatus(event.getCount(), NewDeviceRestoreStatus.State.IN_PROGRESS));
} else if (event.getType() == BackupEvent.Type.FINISHED) {
EventBus.getDefault().post(new Status(event.getCount(), Status.State.TRANSFER_COMPLETE));
EventBus.getDefault().post(new NewDeviceRestoreStatus(event.getCount(), NewDeviceRestoreStatus.State.TRANSFER_COMPLETE));
}
}
public static final class Status {
private final long messageCount;
private final State state;
public Status(long messageCount, State state) {
this.messageCount = messageCount;
this.state = state;
}
public long getMessageCount() {
return messageCount;
}
public @NonNull State getState() {
return state;
}
public enum State {
IN_PROGRESS,
TRANSFER_COMPLETE,
RESTORE_COMPLETE,
FAILURE_VERSION_DOWNGRADE,
FAILURE_FOREIGN_KEY,
FAILURE_UNKNOWN
}
}
}
@@ -8,6 +8,7 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.devicetransfer.DeviceToDeviceTransferService
import org.signal.devicetransfer.NewDeviceRestoreStatus
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.restore.RestoreActivity
@@ -62,17 +63,17 @@ class NewDeviceTransferFragment : DeviceTransferFragment() {
private inner class ServerTaskListener {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: NewDeviceServerTask.Status) {
fun onEventMainThread(event: NewDeviceRestoreStatus) {
status.text = getString(R.string.DeviceTransfer__d_messages_so_far, event.messageCount)
when (event.state) {
NewDeviceServerTask.Status.State.IN_PROGRESS,
NewDeviceServerTask.Status.State.TRANSFER_COMPLETE -> Unit
NewDeviceRestoreStatus.State.IN_PROGRESS,
NewDeviceRestoreStatus.State.TRANSFER_COMPLETE -> Unit
NewDeviceServerTask.Status.State.RESTORE_COMPLETE -> onRestoreComplete()
NewDeviceServerTask.Status.State.FAILURE_VERSION_DOWNGRADE -> abort(R.string.NewDeviceTransfer__cannot_transfer_from_a_newer_version_of_signal)
NewDeviceServerTask.Status.State.FAILURE_FOREIGN_KEY -> abort(R.string.NewDeviceTransfer__failure_foreign_key)
NewDeviceServerTask.Status.State.FAILURE_UNKNOWN -> abort()
NewDeviceRestoreStatus.State.RESTORE_COMPLETE -> onRestoreComplete()
NewDeviceRestoreStatus.State.FAILURE_VERSION_DOWNGRADE -> abort(R.string.NewDeviceTransfer__cannot_transfer_from_a_newer_version_of_signal)
NewDeviceRestoreStatus.State.FAILURE_FOREIGN_KEY -> abort(R.string.NewDeviceTransfer__failure_foreign_key)
NewDeviceRestoreStatus.State.FAILURE_UNKNOWN -> abort()
}
}
}
@@ -41,6 +41,7 @@ import org.signal.registration.NetworkController.RestoreMasterKeyError
import org.signal.registration.NetworkController.SessionMetadata
import org.signal.registration.NetworkController.SetAccountAttributesError
import org.signal.registration.NetworkController.SetRegistrationLockError
import org.signal.registration.NetworkController.SetRestoreMethodError
import org.signal.registration.NetworkController.SubmitVerificationCodeError
import org.signal.registration.NetworkController.SvrCredentials
import org.signal.registration.NetworkController.ThirdPartyServiceErrorResponse
@@ -72,6 +73,7 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import org.whispersystems.signalservice.api.account.AccountAttributes as ServiceAccountAttributes
import org.whispersystems.signalservice.api.account.PreKeyCollection as ServicePreKeyCollection
import org.whispersystems.signalservice.api.provisioning.RestoreMethod as ServiceRestoreMethod
/**
* Implementation of [NetworkController] that bridges to the app's existing network infrastructure.
@@ -443,8 +445,13 @@ class AppRegistrationNetworkController(
}
}
override suspend fun enqueueSvrGuessResetJob() {
override suspend fun enqueueSvrGuessResetJobIfPossible(): Boolean {
if (SignalStore.svr.pin == null) {
return false
}
AppDependencies.jobManager.add(ResetSvrGuessCountJob())
return true
}
override suspend fun enableRegistrationLock(): RequestResult<Unit, SetRegistrationLockError> = withContext(Dispatchers.IO) {
@@ -595,6 +602,26 @@ class AppRegistrationNetworkController(
AppDependencies.jobManager.add(RefreshAttributesJob())
}
override suspend fun setRestoreMethod(token: String, method: NetworkController.RestoreMethod): RequestResult<Unit, SetRestoreMethodError> = withContext(Dispatchers.IO) {
val serviceMethod = when (method) {
NetworkController.RestoreMethod.REMOTE_BACKUP -> ServiceRestoreMethod.REMOTE_BACKUP
NetworkController.RestoreMethod.LOCAL_BACKUP -> ServiceRestoreMethod.LOCAL_BACKUP
NetworkController.RestoreMethod.DEVICE_TRANSFER -> ServiceRestoreMethod.DEVICE_TRANSFER
NetworkController.RestoreMethod.DECLINE -> ServiceRestoreMethod.DECLINE
}
when (val result = AppDependencies.registrationApi.setRestoreMethod(token, serviceMethod)) {
is NetworkResult.Success -> RequestResult.Success(Unit)
is NetworkResult.StatusCodeError -> {
when (result.code) {
429 -> RequestResult.NonSuccess(SetRestoreMethodError.RateLimited(0.seconds))
else -> RequestResult.NonSuccess(SetRestoreMethodError.InvalidRequest("HTTP ${result.code}"))
}
}
is NetworkResult.NetworkError -> RequestResult.RetryableNetworkError(result.exception)
is NetworkResult.ApplicationError -> RequestResult.ApplicationError(result.throwable)
}
}
override suspend fun getBackupFileLastModified(
aep: AccountEntropyPool,
backupInfo: NetworkController.GetBackupInfoResponse
@@ -627,6 +654,26 @@ class AppRegistrationNetworkController(
}
}
override fun startNewDeviceTransferServer(context: Context, aep: AccountEntropyPool) {
val pendingIntent = android.app.PendingIntent.getActivity(
context,
0,
org.thoughtcrime.securesms.MainActivity.clearTop(context),
org.signal.core.util.PendingIntentFlags.mutable()
)
val notificationData = org.signal.devicetransfer.DeviceToDeviceTransferService.TransferNotificationData(
org.thoughtcrime.securesms.notifications.NotificationIds.DEVICE_TRANSFER,
org.thoughtcrime.securesms.notifications.NotificationChannels.getInstance().BACKUPS,
org.thoughtcrime.securesms.R.drawable.ic_signal_backup
)
org.signal.devicetransfer.DeviceToDeviceTransferService.startServer(
context,
org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceServerTask(),
notificationData,
pendingIntent
)
}
override fun startProvisioning(): Flow<ProvisioningEvent> = callbackFlow {
val socketHandles = mutableListOf<java.io.Closeable>()
val configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration()