mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-21 03:28:47 +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.DeletionState
|
||||||
import org.thoughtcrime.securesms.backup.RestoreState
|
import org.thoughtcrime.securesms.backup.RestoreState
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.copyAttachmentToArchive
|
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.importer.ChatItemArchiveImporter
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataArchiveProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallArchiveProcessor
|
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.MediaRootBackupKey
|
||||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
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.AttachmentTransferProgress
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
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].
|
* Imports a backup that was exported via [exportForDebugging].
|
||||||
*/
|
*/
|
||||||
@@ -2078,6 +2104,82 @@ object BackupRepository {
|
|||||||
return RemoteRestoreResult.Success
|
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 {
|
private fun buildDebugInfo(): ByteString {
|
||||||
if (!RemoteConfig.internalUser) {
|
if (!RemoteConfig.internalUser) {
|
||||||
return ByteString.EMPTY
|
return ByteString.EMPTY
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ class ArchiveBackupIdReservationJob private constructor(parameters: Parameters)
|
|||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SignalStore.account.isLinkedDevice) {
|
||||||
|
Log.i(TAG, "Linked device. Skipping.")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
return when (val result = BackupRepository.triggerBackupIdReservation()) {
|
return when (val result = BackupRepository.triggerBackupIdReservation()) {
|
||||||
is NetworkResult.Success -> Result.success()
|
is NetworkResult.Success -> Result.success()
|
||||||
is NetworkResult.NetworkError -> Result.retry(defaultBackoff())
|
is NetworkResult.NetworkError -> Result.retry(defaultBackoff())
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ class BackupMessagesJob private constructor(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignalStore.account.isLinkedDevice -> {
|
||||||
|
Log.i(TAG, "Backup not allowed: linked device.")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ class PostRegistrationBackupRedemptionJob : CoroutineJob {
|
|||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SignalStore.account.isLinkedDevice) {
|
||||||
|
info("Linked device. Exiting.")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
if (SignalStore.backup.deletionState != DeletionState.NONE) {
|
if (SignalStore.backup.deletionState != DeletionState.NONE) {
|
||||||
info("User is in the process of or has delete their backup. Exiting.")
|
info("User is in the process of or has delete their backup. Exiting.")
|
||||||
return Result.success()
|
return Result.success()
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import androidx.annotation.WorkerThread
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
@@ -40,6 +42,7 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
|||||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||||
import org.thoughtcrime.securesms.pin.Svr3Migration
|
import org.thoughtcrime.securesms.pin.Svr3Migration
|
||||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
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.crypto.UnidentifiedAccess
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
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
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
@@ -93,6 +97,8 @@ import java.util.Locale
|
|||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
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
|
// TODO [linked-device] May want to have a different opt out mechanism for linked devices
|
||||||
SvrRepository.optOutOfPin()
|
SvrRepository.optOutOfPin()
|
||||||
|
|
||||||
|
SignalStore.account.isMultiDevice = true
|
||||||
SignalStore.registration.hasUploadedProfile = true
|
SignalStore.registration.hasUploadedProfile = true
|
||||||
jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds)
|
jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds)
|
||||||
|
|
||||||
@@ -676,6 +683,51 @@ object RegistrationRepository {
|
|||||||
return Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(AppDependencies.application, Recipient.self().id)
|
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 {
|
fun interface MasterKeyProducer {
|
||||||
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
||||||
fun produceMasterKey(): MasterKey
|
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.AccountEntropyPool
|
||||||
import org.whispersystems.signalservice.api.NetworkResult
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
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.kbs.MasterKey
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage
|
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||||
@@ -1134,6 +1135,19 @@ class RegistrationViewModel : ViewModel() {
|
|||||||
|
|
||||||
refreshRemoteConfig()
|
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) {
|
for (type in SyncMessage.Request.Type.entries) {
|
||||||
if (type == SyncMessage.Request.Type.UNKNOWN) {
|
if (type == SyncMessage.Request.Type.UNKNOWN) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
package org.whispersystems.signalservice.api;
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
import org.signal.core.util.StreamUtil;
|
import org.signal.core.util.StreamUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||||
@@ -17,6 +16,7 @@ import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
|||||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||||
@@ -38,6 +38,8 @@ import java.util.Map;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import kotlin.Unit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The primary interface for receiving Signal Service messages.
|
* The primary interface for receiving Signal Service messages.
|
||||||
*
|
*
|
||||||
@@ -200,6 +202,16 @@ public class SignalServiceMessageReceiver {
|
|||||||
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
|
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a link+sync backup file. The data is written to @{code destination}.
|
||||||
|
*/
|
||||||
|
public @Nonnull NetworkResult<Unit> retrieveLinkAndSyncBackup(int cdn, @Nonnull String key, @Nonnull File destination, @Nullable ProgressListener listener) {
|
||||||
|
return NetworkResult.fromFetch(() -> {
|
||||||
|
socket.retrieveAttachment(cdn, Collections.emptyMap(), new SignalServiceAttachmentRemoteId.V4(key), destination, 1_000_000_000L, listener);
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String cdnPath) throws MissingConfigurationException, IOException {
|
public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String cdnPath) throws MissingConfigurationException, IOException {
|
||||||
return socket.getCdnLastModifiedTime(cdnNumber, headers, cdnPath);
|
return socket.getCdnLastModifiedTime(cdnNumber, headers, cdnPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,4 +212,27 @@ class LinkDeviceApi(
|
|||||||
val request = WebSocketRequestMessage.put("/v1/accounts/name?deviceId=$deviceId", SetDeviceNameRequest(encryptedDeviceName))
|
val request = WebSocketRequestMessage.put("/v1/accounts/name?deviceId=$deviceId", SetDeviceNameRequest(encryptedDeviceName))
|
||||||
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
|
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "long-polling" endpoint that will return once the primary device has successfully sent sync data.
|
||||||
|
*
|
||||||
|
* @param timeout The max amount of time to wait. Capped at 30 seconds.
|
||||||
|
*
|
||||||
|
* GET /v1/devices/transfer_archive?timeout=[timeout]
|
||||||
|
*
|
||||||
|
* - 200: Success, the primary device was sent backup sync data.
|
||||||
|
* - 204: The primary didn't provide data before the max waiting time elapsed.
|
||||||
|
* - 400: Invalid timeout.
|
||||||
|
* - 429: Rate-limited.
|
||||||
|
*/
|
||||||
|
fun waitForPrimaryDevice(timeout: Duration = 30.seconds): NetworkResult<TransferArchiveResponse> {
|
||||||
|
val request = WebSocketRequestMessage.get("/v1/devices/transfer_archive?timeout=${timeout.inWholeSeconds}")
|
||||||
|
return NetworkResult
|
||||||
|
.fromWebSocketRequest(
|
||||||
|
signalWebSocket = authWebSocket,
|
||||||
|
request = request,
|
||||||
|
timeout = timeout,
|
||||||
|
webSocketResponseConverter = NetworkResult.LongPollingWebSocketConverter(TransferArchiveResponse::class)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.link
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data from primary on where to find link+sync backup file.
|
||||||
|
*/
|
||||||
|
data class TransferArchiveResponse @JsonCreator constructor(
|
||||||
|
@JsonProperty val cdn: Int,
|
||||||
|
@JsonProperty val key: String
|
||||||
|
)
|
||||||
@@ -249,7 +249,7 @@ class ProvisioningSocket<T> private constructor(
|
|||||||
private fun generateProvisioningUrl(deviceAddress: String): String {
|
private fun generateProvisioningUrl(deviceAddress: String): String {
|
||||||
val encodedDeviceId = URLEncoder.encode(deviceAddress, "UTF-8")
|
val encodedDeviceId = URLEncoder.encode(deviceAddress, "UTF-8")
|
||||||
val encodedPubKey: String = URLEncoder.encode(Base64.encodeWithoutPadding(cipher.secondaryDevicePublicKey.serialize()), "UTF-8")
|
val encodedPubKey: String = URLEncoder.encode(Base64.encodeWithoutPadding(cipher.secondaryDevicePublicKey.serialize()), "UTF-8")
|
||||||
return "sgnl://${mode.host}?uuid=$encodedDeviceId&pub_key=$encodedPubKey"
|
return "sgnl://${mode.host}?uuid=$encodedDeviceId&pub_key=$encodedPubKey${mode.params}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun keepAlive(webSocket: WebSocket) {
|
private suspend fun keepAlive(webSocket: WebSocket) {
|
||||||
@@ -288,9 +288,9 @@ class ProvisioningSocket<T> private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Mode(val host: String) {
|
enum class Mode(val host: String, val params: String) {
|
||||||
REREG("rereg"),
|
REREG("rereg", ""),
|
||||||
LINK("linkdevice")
|
LINK("linkdevice", "&capabilities=backup4")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun interface ProvisioningSocketExceptionHandler {
|
fun interface ProvisioningSocketExceptionHandler {
|
||||||
|
|||||||
Reference in New Issue
Block a user