Skip to content

Commit

Permalink
Implement checkin job for backups.
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-signal authored and greyson-signal committed Nov 21, 2024
1 parent ae37001 commit 6ff31b9
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
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;
Expand Down Expand Up @@ -247,6 +248,7 @@ public void onForeground() {
startAnrDetector();

SignalExecutors.BOUNDED.execute(() -> {
BackupRefreshJob.enqueueIfNecessary();
InAppPaymentAuthCheckJob.enqueueIfNeeded();
RemoteConfig.refreshIfNecessary();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
105 changes: 105 additions & 0 deletions app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRefreshJob.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ public static Map<String, Job.Factory> getJobFactories(@NonNull Application appl
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,15 @@ public ArchiveGetBackupInfoResponse getArchiveBackupInfo(ArchiveCredentialPresen
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<>();

Expand Down

0 comments on commit 6ff31b9

Please sign in to comment.