Implement checkin job for backups.

This commit is contained in:
Alex Hart 2024-11-18 15:00:17 -04:00 committed by Greyson Parrelli
parent ae37001949
commit 6ff31b950d
8 changed files with 146 additions and 0 deletions

View file

@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
import org.thoughtcrime.securesms.jobs.BackupRefreshJob;
import org.thoughtcrime.securesms.jobs.BackupSubscriptionCheckJob;
import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
@ -247,6 +248,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
startAnrDetector();
SignalExecutors.BOUNDED.execute(() -> {
BackupRefreshJob.enqueueIfNecessary();
InAppPaymentAuthCheckJob.enqueueIfNeeded();
RemoteConfig.refreshIfNecessary();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();

View file

@ -132,6 +132,19 @@ object BackupRepository {
}
}
/**
* Refreshes backup via server
*/
fun refreshBackup(): NetworkResult<Unit> {
return initBackupAndFetchAuth()
.then { accessPair ->
AppDependencies.archiveApi.refreshBackup(
aci = SignalStore.account.requireAci(),
archiveServiceAccess = accessPair.messageBackupAccess
)
}
}
/**
* Gets the free storage space in the device's data partition.
*/

View file

@ -0,0 +1,105 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.NetworkResult
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/**
* Notifies the server that the backup for the local user is still being used.
*/
class BackupRefreshJob private constructor(
parameters: Parameters
) : Job(parameters) {
companion object {
private val TAG = Log.tag(BackupRefreshJob::class)
const val KEY = "BackupRefreshJob"
private val TIME_BETWEEN_CHECKINS = 3.days
@JvmStatic
fun enqueueIfNecessary() {
if (!canExecuteJob()) {
return
}
val now = System.currentTimeMillis().milliseconds
val lastCheckIn = SignalStore.backup.lastCheckInMillis.milliseconds
if ((now - lastCheckIn) >= TIME_BETWEEN_CHECKINS) {
AppDependencies.jobManager.add(
BackupRefreshJob(
parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(Parameters.UNLIMITED)
.setLifespan(3.days.inWholeMilliseconds)
.setMaxInstancesForFactory(1)
.build()
)
)
}
}
private fun canExecuteJob(): Boolean {
if (!SignalStore.account.isRegistered) {
Log.i(TAG, "Account not registered. Exiting.")
return false
}
if (!RemoteConfig.messageBackups) {
Log.i(TAG, "Backups are not enabled in remote config. Exiting.")
return false
}
if (!SignalStore.backup.areBackupsEnabled) {
Log.i(TAG, "Backups have not been enabled on this device. Exiting.")
return false
}
return true
}
}
override fun run(): Result {
if (!canExecuteJob()) {
return Result.success()
}
val result = BackupRepository.refreshBackup()
return when (result) {
is NetworkResult.Success -> {
SignalStore.backup.lastCheckInMillis = System.currentTimeMillis()
Result.success()
}
else -> {
Log.w(TAG, "Failed to refresh backup with server.", result.getCause())
Result.failure()
}
}
}
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
class Factory : Job.Factory<BackupRefreshJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): BackupRefreshJob {
return BackupRefreshJob(parameters)
}
}
}

View file

@ -258,6 +258,7 @@ class InAppPaymentRedemptionJob private constructor(
if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) {
Log.i(TAG, "Setting backup tier to PAID", true)
SignalStore.backup.backupTier = MessageBackupTier.PAID
SignalStore.backup.lastCheckInMillis = System.currentTimeMillis()
}
}

View file

@ -271,6 +271,7 @@ public final class JobManagerFactories {
put(BackfillDigestsForDuplicatesMigrationJob.KEY, new BackfillDigestsForDuplicatesMigrationJob.Factory());
put(BackupJitterMigrationJob.KEY, new BackupJitterMigrationJob.Factory());
put(BackupNotificationMigrationJob.KEY, new BackupNotificationMigrationJob.Factory());
put(BackupRefreshJob.KEY, new BackupRefreshJob.Factory());
put(BlobStorageLocationMigrationJob.KEY, new BlobStorageLocationMigrationJob.Factory());
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
put(ClearGlideCacheMigrationJob.KEY, new ClearGlideCacheMigrationJob.Factory());

View file

@ -35,6 +35,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_BACKUP_LAST_PROTO_SIZE = "backup.lastProtoSize"
private const val KEY_BACKUP_TIER = "backup.backupTier"
private const val KEY_LATEST_BACKUP_TIER = "backup.latestBackupTier"
private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds"
private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime"
private const val KEY_LAST_BACKUP_TIME = "backup.lastBackupTime"
@ -94,6 +95,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var userManuallySkippedMediaRestore: Boolean by booleanValue(KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE, false)
var lastCheckInMillis: Long by longValue(KEY_LAST_CHECK_IN_MILLIS, 0L)
/**
* Key used to backup messages.
*/

View file

@ -150,6 +150,18 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
}
}
/**
* Backup keep-alive that informs the server that the backup is still in use. If a backup is not refreshed, it may be deleted
* after 30 days.
*/
fun refreshBackup(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MessageBackupKey>): NetworkResult<Unit> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.refreshBackup(presentationData.toArchiveCredentialPresentation())
}
}
/**
* Lists the media objects in the backup
*/

View file

@ -563,6 +563,15 @@ public class PushServiceSocket {
return JsonUtil.fromJson(response, ArchiveGetBackupInfoResponse.class);
}
/**
* POST credential presentation to the server to keep backup alive.
*/
public void refreshBackup(ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
makeServiceRequestWithoutAuthentication(ARCHIVE_INFO, "POST", null, headers, NO_HANDLER);
}
public List<ArchiveGetMediaItemsResponse.StoredMediaObject> debugGetAllArchiveMediaItems(ArchiveCredentialPresentation credentialPresentation) throws IOException {
List<ArchiveGetMediaItemsResponse.StoredMediaObject> mediaObjects = new ArrayList<>();