mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Add rudimentary link+sync support.
This commit is contained in:
@@ -59,6 +59,7 @@ import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.copyAttachmentToArchive
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.exportForDebugging
|
||||
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataArchiveProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallArchiveProcessor
|
||||
@@ -141,6 +142,7 @@ import org.whispersystems.signalservice.api.backup.MediaName
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
@@ -1092,6 +1094,30 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a link and sync backup stored on the transit CDN.
|
||||
*
|
||||
* @param backupKey The key used to encrypt the backup. If `null`, we assume that the file is plaintext.
|
||||
*/
|
||||
fun importLinkAndSyncSignalBackup(
|
||||
length: Long,
|
||||
inputStreamFactory: () -> InputStream,
|
||||
selfData: SelfData,
|
||||
backupKey: MessageBackupKey,
|
||||
cancellationSignal: () -> Boolean = { false }
|
||||
): ImportResult {
|
||||
val frameReader = EncryptedBackupReader.createForLocalOrLinking(
|
||||
key = backupKey,
|
||||
aci = selfData.aci,
|
||||
length = length,
|
||||
dataStream = inputStreamFactory
|
||||
)
|
||||
|
||||
return frameReader.use { reader ->
|
||||
import(reader, selfData, cancellationSignal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a backup that was exported via [exportForDebugging].
|
||||
*/
|
||||
@@ -2078,6 +2104,82 @@ object BackupRepository {
|
||||
return RemoteRestoreResult.Success
|
||||
}
|
||||
|
||||
suspend fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey) {
|
||||
val context = AppDependencies.application
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
|
||||
try {
|
||||
DataRestoreConstraint.isRestoringData = true
|
||||
return withContext(Dispatchers.IO) {
|
||||
return@withContext BackupProgressService.start(context, context.getString(R.string.BackupProgressService_title)).use {
|
||||
restoreLinkAndSyncBackup(response, ephemeralBackupKey, controller = it, cancellationSignal = { !isActive })
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
DataRestoreConstraint.isRestoringData = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey, controller: BackupProgressService.Controller, cancellationSignal: () -> Boolean): RemoteRestoreResult {
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
|
||||
val progressListener = object : ProgressListener {
|
||||
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
|
||||
controller.update(
|
||||
title = AppDependencies.application.getString(R.string.BackupProgressService_title_downloading),
|
||||
progress = progress.value,
|
||||
indeterminate = false
|
||||
)
|
||||
EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_DOWNLOAD, progress.transmitted, progress.total))
|
||||
}
|
||||
|
||||
override fun shouldCancel() = cancellationSignal()
|
||||
}
|
||||
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Downloading backup")
|
||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
when (val result = AppDependencies.signalServiceMessageReceiver.retrieveLinkAndSyncBackup(response.cdn, response.key, tempBackupFile, progressListener)) {
|
||||
is NetworkResult.Success -> Log.i(TAG, "[restoreLinkAndSyncBackup] Download successful")
|
||||
else -> {
|
||||
Log.w(TAG, "[restoreLinkAndSyncBackup] Failed to download backup file", result.getCause())
|
||||
return RemoteRestoreResult.NetworkError
|
||||
}
|
||||
}
|
||||
|
||||
if (cancellationSignal()) {
|
||||
return RemoteRestoreResult.Canceled
|
||||
}
|
||||
|
||||
controller.update(
|
||||
title = AppDependencies.application.getString(R.string.BackupProgressService_title),
|
||||
progress = 0f,
|
||||
indeterminate = true
|
||||
)
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Importing backup")
|
||||
val result = importLinkAndSyncSignalBackup(
|
||||
length = tempBackupFile.length(),
|
||||
inputStreamFactory = tempBackupFile::inputStream,
|
||||
selfData = selfData,
|
||||
backupKey = ephemeralBackupKey,
|
||||
cancellationSignal = cancellationSignal
|
||||
)
|
||||
|
||||
if (result == ImportResult.Failure) {
|
||||
Log.w(TAG, "[restoreLinkAndSyncBackup] Failed to import backup")
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Restore successful")
|
||||
return RemoteRestoreResult.Success
|
||||
}
|
||||
|
||||
private fun buildDebugInfo(): ByteString {
|
||||
if (!RemoteConfig.internalUser) {
|
||||
return ByteString.EMPTY
|
||||
|
||||
@@ -52,6 +52,11 @@ class ArchiveBackupIdReservationJob private constructor(parameters: Parameters)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
Log.i(TAG, "Linked device. Skipping.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
return when (val result = BackupRepository.triggerBackupIdReservation()) {
|
||||
is NetworkResult.Success -> Result.success()
|
||||
is NetworkResult.NetworkError -> Result.retry(defaultBackoff())
|
||||
|
||||
@@ -80,6 +80,11 @@ class BackupMessagesJob private constructor(
|
||||
false
|
||||
}
|
||||
|
||||
SignalStore.account.isLinkedDevice -> {
|
||||
Log.i(TAG, "Backup not allowed: linked device.")
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ class PostRegistrationBackupRedemptionJob : CoroutineJob {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
info("Linked device. Exiting.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (SignalStore.backup.deletionState != DeletionState.NONE) {
|
||||
info("User is in the process of or has delete their backup. Exiting.")
|
||||
return Result.success()
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
@@ -40,6 +42,7 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.pin.Svr3Migration
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
@@ -76,6 +79,7 @@ import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
@@ -93,6 +97,8 @@ import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -253,6 +259,7 @@ object RegistrationRepository {
|
||||
// TODO [linked-device] May want to have a different opt out mechanism for linked devices
|
||||
SvrRepository.optOutOfPin()
|
||||
|
||||
SignalStore.account.isMultiDevice = true
|
||||
SignalStore.registration.hasUploadedProfile = true
|
||||
jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds)
|
||||
|
||||
@@ -676,6 +683,51 @@ object RegistrationRepository {
|
||||
return Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(AppDependencies.application, Recipient.self().id)
|
||||
}
|
||||
|
||||
suspend fun waitForLinkAndSyncBackupDetails(maxWaitTime: Duration = 60.seconds): TransferArchiveResponse? {
|
||||
val startTime = System.currentTimeMillis()
|
||||
var timeRemaining = maxWaitTime.inWholeMilliseconds
|
||||
|
||||
while (timeRemaining > 0 && coroutineContext.isActive) {
|
||||
Log.d(TAG, "[waitForLinkAndSyncBackupDetails] Willing to wait for $timeRemaining ms...")
|
||||
|
||||
when (val result = SignalNetwork.linkDevice.waitForPrimaryDevice(timeout = 60.seconds)) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.i(TAG, "[waitForLinkAndSyncBackupDetails] Transfer archive data provided by primary")
|
||||
return result.result
|
||||
}
|
||||
is NetworkResult.ApplicationError -> {
|
||||
Log.e(TAG, "[waitForLinkAndSyncBackupDetails] Application error!", result.throwable)
|
||||
throw result.throwable
|
||||
}
|
||||
is NetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "[waitForLinkAndSyncBackupDetails] Hit a network error while waiting for linking. Will try to wait again.", result.exception)
|
||||
}
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (result.code) {
|
||||
400 -> {
|
||||
Log.w(TAG, "[waitForLinkAndSyncBackupDetails] Invalid timeout!")
|
||||
return null
|
||||
}
|
||||
429 -> {
|
||||
Log.w(TAG, "[waitForLinkAndSyncBackupDetails] Hit a rate-limit. Will try to wait again after delay: ${result.retryAfter()}.")
|
||||
result.retryAfter()?.let { retryAfter ->
|
||||
delay(retryAfter)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "[waitForLinkAndSyncBackupDetails] Hit an unknown status code of ${result.code}. Will try to wait again.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeRemaining = maxWaitTime.inWholeMilliseconds - (System.currentTimeMillis() - startTime)
|
||||
}
|
||||
|
||||
Log.w(TAG, "[waitForLinkAndSyncBackupDetails] Failed to get transfer archive data from primary")
|
||||
return null
|
||||
}
|
||||
|
||||
fun interface MasterKeyProducer {
|
||||
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
||||
fun produceMasterKey(): MasterKey
|
||||
|
||||
@@ -85,6 +85,7 @@ import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||
@@ -1134,6 +1135,19 @@ class RegistrationViewModel : ViewModel() {
|
||||
|
||||
refreshRemoteConfig()
|
||||
|
||||
if (message.ephemeralBackupKey != null) {
|
||||
Log.i(TAG, "Primary has given Linked device an ephemeral backup key, waiting for backup...")
|
||||
val result = RegistrationRepository.waitForLinkAndSyncBackupDetails()
|
||||
if (result != null) {
|
||||
BackupRepository.restoreLinkAndSyncBackup(result, MessageBackupKey(message.ephemeralBackupKey!!.toByteArray()))
|
||||
} else {
|
||||
Log.w(TAG, "Unable to get transfer archive data, continuing with linking process")
|
||||
}
|
||||
|
||||
// TODO [linked-device] Reapply opt-out, backup restore sets pin, may want to have a different opt out mechanism for linked devices
|
||||
SvrRepository.optOutOfPin()
|
||||
}
|
||||
|
||||
for (type in SyncMessage.Request.Type.entries) {
|
||||
if (type == SyncMessage.Request.Type.UNKNOWN) {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user