mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-14 12:47:36 +01:00
Fix linked android device improperly reading/writing backup level in storage service.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import arrow.core.Either
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.network.service.ArchiveError
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.CoroutineJob
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.protos.BackupTierDowngradeCheckJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Asks the service what tier we're actually on after a storage service account record told us our backup tier went down.
|
||||
*
|
||||
* [remoteBackupTier] is the zkgroup backup level from that record, which we fall back to when the service can't give us a usable answer.
|
||||
*/
|
||||
class BackupTierDowngradeCheckJob private constructor(
|
||||
private val remoteBackupTier: Long?,
|
||||
parameters: Parameters
|
||||
) : CoroutineJob(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BackupTierDowngradeCheckJob::class)
|
||||
|
||||
const val KEY = "BackupTierDowngradeCheckJob"
|
||||
|
||||
fun enqueue(remoteBackupTier: Long?) {
|
||||
AppDependencies.jobManager.add(create(remoteBackupTier))
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun create(remoteBackupTier: Long?): BackupTierDowngradeCheckJob {
|
||||
return BackupTierDowngradeCheckJob(
|
||||
remoteBackupTier = remoteBackupTier,
|
||||
parameters = Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setLifespan(3.days.inWholeMilliseconds)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun doRun(): Result {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
Log.i(TAG, "Not registered. Nothing to confirm.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (SignalStore.account.isPrimaryDevice) {
|
||||
Log.i(TAG, "Primary device owns its own tier. Nothing to confirm.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (SignalStore.backup.backupTier == null) {
|
||||
Log.i(TAG, "We already have no tier. Nothing to confirm.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
return when (val result = BackupRepository.getBackupTierWithoutDowngrade()) {
|
||||
is Either.Right -> {
|
||||
Log.i(TAG, "Service says we're on ${result.value}. Applying it over our local tier of ${SignalStore.backup.backupTier}.", true)
|
||||
SignalStore.backup.backupTier = result.value
|
||||
Result.success()
|
||||
}
|
||||
|
||||
is Either.Left -> handleError(result.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: ArchiveError.CredentialError): Result {
|
||||
return when (error) {
|
||||
is ArchiveError.NetworkError -> {
|
||||
Log.w(TAG, "Network error. Retrying later.")
|
||||
Result.retry(defaultBackoff())
|
||||
}
|
||||
|
||||
is ArchiveError.CredentialError.RateLimited -> {
|
||||
Log.w(TAG, "Rate limited. Retrying later.")
|
||||
Result.retry(error.retryAfter?.inWholeMilliseconds ?: defaultBackoff())
|
||||
}
|
||||
|
||||
is ArchiveError.CredentialError.NotFound,
|
||||
is ArchiveError.CredentialError.Unauthorized,
|
||||
is ArchiveError.CredentialError.InvalidRequest,
|
||||
is ArchiveError.CredentialError.ZkVerificationFailed,
|
||||
is ArchiveError.ApplicationError -> {
|
||||
val tier = MessageBackupTier.fromBackupLevel(remoteBackupTier)
|
||||
Log.w(TAG, "Service could not tell us our tier ($error). Deferring to the account record and applying $tier over ${SignalStore.backup.backupTier}.", true)
|
||||
SignalStore.backup.backupTier = tier
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray {
|
||||
return BackupTierDowngradeCheckJobData(remoteBackupTier = remoteBackupTier).encode()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<BackupTierDowngradeCheckJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): BackupTierDowngradeCheckJob {
|
||||
val data = BackupTierDowngradeCheckJobData.ADAPTER.decode(serializedData!!)
|
||||
return BackupTierDowngradeCheckJob(remoteBackupTier = data.remoteBackupTier, parameters = parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,7 @@ public final class JobManagerFactories {
|
||||
put(BackupMessagesJob.KEY, new BackupMessagesJob.Factory());
|
||||
put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory());
|
||||
put(BackupSubscriptionCheckJob.KEY, new BackupSubscriptionCheckJob.Factory());
|
||||
put(BackupTierDowngradeCheckJob.KEY, new BackupTierDowngradeCheckJob.Factory());
|
||||
put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory());
|
||||
put(CallingAssetsDownloadJob.KEY, new CallingAssetsDownloadJob.Factory());
|
||||
put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory());
|
||||
|
||||
@@ -141,7 +141,7 @@ class AccountRecordProcessor(
|
||||
username = remote.proto.username
|
||||
usernameLink = remote.proto.usernameLink
|
||||
notificationProfileManualOverride = remote.proto.notificationProfileManualOverride
|
||||
backupTier = local.proto.backupTier ?: remote.proto.backupTier
|
||||
backupTier = if (SignalStore.account.isPrimaryDevice) local.proto.backupTier ?: remote.proto.backupTier else remote.proto.backupTier
|
||||
avatarColor = if (SignalStore.account.isPrimaryDevice) local.proto.avatarColor else remote.proto.avatarColor
|
||||
automaticKeyVerificationDisabled = remote.proto.automaticKeyVerificationDisabled
|
||||
hasSeenAdminDeleteEducationDialog = remote.proto.hasSeenAdminDeleteEducationDialog
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.database.model.KeyTransparencyStore
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.KeyTransparencyApi
|
||||
import org.thoughtcrime.securesms.jobs.BackupTierDowngradeCheckJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob
|
||||
@@ -196,7 +197,6 @@ object StorageSyncHelper {
|
||||
}
|
||||
|
||||
backupTier = when {
|
||||
SignalStore.account.isLinkedDevice -> null
|
||||
SignalStore.backup.areBackupsEnabled && SignalStore.backup.backupTier != null -> SignalStore.backup.backupTier!!.toBackupLevel()
|
||||
SignalStore.backup.backupTierInternalOverride != null -> SignalStore.backup.backupTierInternalOverride!!.toBackupLevel()
|
||||
else -> null
|
||||
@@ -310,8 +310,15 @@ object StorageSyncHelper {
|
||||
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
val remoteBackupTier = MessageBackupTier.fromBackupLevel(update.new.proto.backupTier)
|
||||
if (remoteBackupTier != SignalStore.backup.backupTier) {
|
||||
SignalStore.backup.backupTier = remoteBackupTier
|
||||
val localBackupTier = SignalStore.backup.backupTier
|
||||
|
||||
if (remoteBackupTier != localBackupTier) {
|
||||
if (isBackupTierDowngrade(from = localBackupTier, to = remoteBackupTier)) {
|
||||
Log.w(TAG, "Remote account record downgrades our backup tier ($localBackupTier -> $remoteBackupTier). Confirming with the service before applying it.")
|
||||
BackupTierDowngradeCheckJob.enqueue(update.new.proto.backupTier)
|
||||
} else {
|
||||
SignalStore.backup.backupTier = remoteBackupTier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,6 +415,14 @@ object StorageSyncHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBackupTierDowngrade(from: MessageBackupTier?, to: MessageBackupTier?): Boolean {
|
||||
return when (from) {
|
||||
null -> false
|
||||
MessageBackupTier.FREE -> to == null
|
||||
MessageBackupTier.PAID -> to == null || to == MessageBackupTier.FREE
|
||||
}
|
||||
}
|
||||
|
||||
class IdDifferenceResult(
|
||||
@JvmField val remoteOnlyIds: List<StorageId>,
|
||||
@JvmField val localOnlyIds: List<StorageId>,
|
||||
|
||||
@@ -292,3 +292,7 @@ message IndividualSendJobV2Data {
|
||||
message MultiDeviceAttachmentBackfillRequestJobData {
|
||||
uint64 messageId = 1;
|
||||
}
|
||||
|
||||
message BackupTierDowngradeCheckJobData {
|
||||
optional uint64 remoteBackupTier = 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.verify
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException
|
||||
import org.signal.network.service.ArchiveError
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.testutil.MockSignalStoreRule
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class BackupTierDowngradeCheckJobTest {
|
||||
|
||||
companion object {
|
||||
private val FREE_TIER = MessageBackupTier.FREE.toBackupLevel()
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val signalStore = MockSignalStoreRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Log.initialize(SystemOutLogger())
|
||||
|
||||
every { signalStore.account.isRegistered } returns true
|
||||
every { signalStore.account.isPrimaryDevice } returns false
|
||||
every { signalStore.backup.backupTier } returns MessageBackupTier.PAID
|
||||
|
||||
mockkObject(BackupRepository)
|
||||
mockkObject(RemoteConfig)
|
||||
every { RemoteConfig.defaultMaxBackoff } returns 1.minutes.inWholeMilliseconds
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkObject(BackupRepository)
|
||||
unmockkObject(RemoteConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the service says paid, when I run, then I keep the paid tier`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns MessageBackupTier.PAID.right()
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(FREE_TIER).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { signalStore.backup.backupTier = MessageBackupTier.PAID }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the service says free, when I run, then I downgrade to free`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns MessageBackupTier.FREE.right()
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(FREE_TIER).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { signalStore.backup.backupTier = MessageBackupTier.FREE }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a network error, when I run, then I retry and leave our tier alone`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns ArchiveError.NetworkError(IOException()).left()
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(FREE_TIER).run()
|
||||
|
||||
assertTrue(result.isRetry)
|
||||
verify(exactly = 0) { signalStore.backup.backupTier = any() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the service rejects our credential, when I run, then I take the tier from the record`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns ArchiveError.CredentialError.Unauthorized().left()
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(FREE_TIER).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { signalStore.backup.backupTier = MessageBackupTier.FREE }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the service has no backup for us and the record had no tier, when I run, then I clear our tier`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns ArchiveError.CredentialError.NotFound().left()
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(null).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { signalStore.backup.backupTier = null }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given we cannot verify our own credential, when I run, then I take the tier from the record`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns ArchiveError.CredentialError.ZkVerificationFailed(VerificationFailedException()).left()
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(null).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { signalStore.backup.backupTier = null }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a job carrying a tier, when I serialize and restore it, then I keep that tier`() {
|
||||
every { BackupRepository.getBackupTierWithoutDowngrade() } returns ArchiveError.CredentialError.NotFound().left()
|
||||
|
||||
val job = BackupTierDowngradeCheckJob.create(FREE_TIER)
|
||||
val restored = BackupTierDowngradeCheckJob.Factory().create(job.parameters, job.serialize())
|
||||
|
||||
val result = restored.run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { signalStore.backup.backupTier = MessageBackupTier.FREE }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given we are not registered, when I run, then I leave our tier alone`() {
|
||||
every { signalStore.account.isRegistered } returns false
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(FREE_TIER).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify(exactly = 0) { signalStore.backup.backupTier = any() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given we are the primary, when I run, then I leave our tier alone`() {
|
||||
every { signalStore.account.isPrimaryDevice } returns true
|
||||
|
||||
val result = BackupTierDowngradeCheckJob.create(FREE_TIER).run()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify(exactly = 0) { signalStore.backup.backupTier = any() }
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.database.IssueReporter
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -54,6 +55,7 @@ import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord
|
||||
@@ -375,6 +377,81 @@ class StorageSyncJobTest {
|
||||
assertTrue(remoteStorage.manifest!!.storageIds.contains(StorageId.forAccount(selfStorageId!!)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a linked device with a tier, when I write our account record, then I include our tier`() {
|
||||
stubLinkedDevice()
|
||||
every { recipients.signalStore.backup.areBackupsEnabled } returns true
|
||||
every { recipients.signalStore.backup.backupTier } returns MessageBackupTier.PAID
|
||||
|
||||
markSelfNeedsSync()
|
||||
|
||||
val result = runJob(StorageSyncJob.forLocalChange())
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(MessageBackupTier.PAID.toBackupLevel(), remoteAccountRecord().backupTier)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a linked device and a remote record with a higher tier, when I run, then I apply it`() {
|
||||
stubLinkedDevice()
|
||||
|
||||
remoteStorage.setRemoteState(listOf(accountRecordWithBackupTier(MessageBackupTier.PAID.toBackupLevel())), version = BASE_MANIFEST_VERSION + 1)
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val result = runJob(StorageSyncJob.forRemoteChange())
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify { recipients.signalStore.backup.backupTier = MessageBackupTier.PAID }
|
||||
verify(exactly = 0) { jobManager.add(ofType<BackupTierDowngradeCheckJob>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a linked device and a remote record without a tier, when I run, then I keep our tier and confirm with the service`() {
|
||||
stubLinkedDevice()
|
||||
every { recipients.signalStore.backup.areBackupsEnabled } returns true
|
||||
every { recipients.signalStore.backup.backupTier } returns MessageBackupTier.PAID
|
||||
|
||||
remoteStorage.setRemoteState(listOf(accountRecordWithBackupTier(null)), version = BASE_MANIFEST_VERSION + 1)
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val result = runJob(StorageSyncJob.forRemoteChange())
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify(exactly = 0) { recipients.signalStore.backup.backupTier = null }
|
||||
verify { jobManager.add(ofType<BackupTierDowngradeCheckJob>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a linked device on paid and a remote record on free, when I run, then I keep our tier and confirm with the service`() {
|
||||
stubLinkedDevice()
|
||||
every { recipients.signalStore.backup.areBackupsEnabled } returns true
|
||||
every { recipients.signalStore.backup.backupTier } returns MessageBackupTier.PAID
|
||||
|
||||
remoteStorage.setRemoteState(listOf(accountRecordWithBackupTier(MessageBackupTier.FREE.toBackupLevel())), version = BASE_MANIFEST_VERSION + 1)
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val result = runJob(StorageSyncJob.forRemoteChange())
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
verify(exactly = 0) { recipients.signalStore.backup.backupTier = MessageBackupTier.FREE }
|
||||
verify { jobManager.add(ofType<BackupTierDowngradeCheckJob>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a primary with a tier and a remote record without one, when I run, then I write our tier back and confirm nothing`() {
|
||||
every { recipients.signalStore.backup.areBackupsEnabled } returns true
|
||||
every { recipients.signalStore.backup.backupTier } returns MessageBackupTier.PAID
|
||||
|
||||
remoteStorage.setRemoteState(listOf(accountRecordWithBackupTier(null)), version = BASE_MANIFEST_VERSION + 1)
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val result = runJob(StorageSyncJob.forRemoteChange())
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(MessageBackupTier.PAID.toBackupLevel(), remoteAccountRecord().backupTier)
|
||||
verify(exactly = 0) { jobManager.add(ofType<BackupTierDowngradeCheckJob>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a contact was unregistered long ago, when I run, then I remove their storage id`() {
|
||||
val contact = recipients.createRecipient("Local Contact")
|
||||
@@ -669,4 +746,29 @@ class StorageSyncJobTest {
|
||||
private fun bumpRemoteManifestWithoutTouchingAccountRecord() {
|
||||
remoteStorage.addRemoteRecords(listOf(contactRecord(ACI.from(UUID.randomUUID()), ProfileName.fromParts("Remote", "Contact"))))
|
||||
}
|
||||
|
||||
private fun markSelfNeedsSync() {
|
||||
SignalDatabase.recipients.markNeedsSync(recipients.self)
|
||||
Recipient.self().live().refresh()
|
||||
}
|
||||
|
||||
private fun remoteAccountRecord(): AccountRecord {
|
||||
return remoteStorage.records.mapNotNull { it.proto.account }.single()
|
||||
}
|
||||
|
||||
private fun stubLinkedDevice() {
|
||||
every { recipients.signalStore.account.isLinkedDevice } returns true
|
||||
every { recipients.signalStore.account.isPrimaryDevice } returns false
|
||||
every { recipients.signalStore.account.restoredAccountEntropyPoolFromPrimary } returns true
|
||||
}
|
||||
|
||||
/** Our account record as another device would have rewritten it: same contents, [backupTier] swapped in, under a fresh storage id so that we see it as remote-only. */
|
||||
private fun accountRecordWithBackupTier(backupTier: Long?): SignalStorageRecord {
|
||||
val record = StorageSyncHelper.buildAccountRecord(ApplicationProvider.getApplicationContext(), Recipient.self())
|
||||
|
||||
return record.copy(
|
||||
id = StorageId.forAccount(StorageSyncHelper.generateKey()),
|
||||
proto = record.proto.copy(account = record.proto.account!!.copy(backupTier = backupTier))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +237,6 @@ message AccountRecord {
|
||||
}
|
||||
}
|
||||
|
||||
message BackupTierHistory {
|
||||
// See zkgroup for integer particular values. Unset if backups are not enabled.
|
||||
optional uint64 backupTier = 1;
|
||||
optional uint64 endedAtTimestamp = 2;
|
||||
}
|
||||
|
||||
message NotificationProfileManualOverride {
|
||||
message ManuallyEnabled {
|
||||
bytes id = 1;
|
||||
@@ -299,7 +293,7 @@ message AccountRecord {
|
||||
optional uint64 backupTier = 40; // See zkgroup for integer particular values. Unset if backups are not enabled.
|
||||
IAPSubscriberData backupSubscriberData = 41;
|
||||
optional AvatarColor avatarColor = 42;
|
||||
BackupTierHistory backupTierHistory = 43;
|
||||
reserved /* backupTierHistory */ 43;
|
||||
NotificationProfileManualOverride notificationProfileManualOverride = 44;
|
||||
bool notificationProfileSyncDisabled = 45;
|
||||
bool automaticKeyVerificationDisabled = 46;
|
||||
|
||||
Reference in New Issue
Block a user