diff --git a/Android.bp b/Android.bp index 04c3cc12..d61e977c 100644 --- a/Android.bp +++ b/Android.bp @@ -30,6 +30,7 @@ android_app { "androidx.activity_activity-ktx", "androidx.preference_preference", "androidx.documentfile_documentfile", + "androidx.work_work-runtime-ktx", "androidx.lifecycle_lifecycle-viewmodel-ktx", "androidx.lifecycle_lifecycle-livedata-ktx", "androidx-constraintlayout_constraintlayout", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 52a0e183..ec43fa21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.documentfile) + implementation(libs.androidx.work.runtime.ktx) implementation(libs.google.material) implementation(libs.google.tink.android) diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt new file mode 100644 index 00000000..d0352e46 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault + +import android.content.Context +import android.util.Log +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.stevesoltys.seedvault.transport.requestBackup +import java.util.Date +import java.util.concurrent.TimeUnit + +class BackupWorker( + appContext: Context, + workerParams: WorkerParameters, +) : Worker(appContext, workerParams) { + + companion object { + private const val UNIQUE_WORK_NAME = "APP_BACKUP" + + fun schedule(appContext: Context) { + val backupConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresCharging(true) + .build() + val backupWorkRequest = PeriodicWorkRequestBuilder( + repeatInterval = 24, + repeatIntervalTimeUnit = TimeUnit.HOURS, + flexTimeInterval = 2, + flexTimeIntervalUnit = TimeUnit.HOURS, + ).setConstraints(backupConstraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS) + .build() + val workManager = WorkManager.getInstance(appContext) + workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, UPDATE, backupWorkRequest) + } + + fun unschedule(appContext: Context) { + val workManager = WorkManager.getInstance(appContext) + workManager.cancelUniqueWork(UNIQUE_WORK_NAME) + } + + fun logWorkInfo(appContext: Context) { + val workManager = WorkManager.getInstance(appContext) + workManager.getWorkInfosForUniqueWork(UNIQUE_WORK_NAME).get().forEach { + Log.e( + "BackupWorker", " ${it.state.name} - ${Date(it.nextScheduleTimeMillis)} - " + + "runAttempts: ${it.runAttemptCount}" + ) + } + } + } + + override fun doWork(): Result { + // TODO once we make this the default, we should do storage backup here as well + // or have two workers and ensure they never run at the same time + return if (requestBackup(applicationContext)) Result.success() + else Result.retry() + } +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt index c7e7d378..b1281325 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt @@ -44,7 +44,8 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { val d2dPreference = findPreference(PREF_KEY_D2D_BACKUPS) d2dPreference?.setOnPreferenceChangeListener { _, newValue -> - d2dPreference.isChecked = newValue as Boolean + viewModel.onD2dChanged(newValue as Boolean) + d2dPreference.isChecked = newValue // automatically enable unlimited quota when enabling D2D backups if (d2dPreference.isChecked) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 3c190651..e220462c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -12,6 +12,7 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri import android.os.Process.myUid +import android.os.UserHandle import android.provider.Settings import android.util.Log import android.widget.Toast @@ -24,6 +25,7 @@ import androidx.lifecycle.Transformations.switchMap import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.DiffUtil.calculateDiff +import com.stevesoltys.seedvault.BackupWorker import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.metadata.MetadataManager @@ -261,4 +263,13 @@ internal class SettingsViewModel( Toast.makeText(app, str, LENGTH_LONG).show() } + fun onD2dChanged(enabled: Boolean) { + backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), !enabled) + if (enabled) { + BackupWorker.schedule(app) + } else { + BackupWorker.unschedule(app) + } + } + } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 1d67988d..443badd4 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -60,10 +60,15 @@ class ConfigurableBackupTransportService : Service(), KoinComponent { } +/** + * Requests the system to initiate a backup. + * + * @return true iff backups was requested successfully (backup itself can still fail). + */ @WorkerThread -fun requestBackup(context: Context) { +fun requestBackup(context: Context): Boolean { val backupManager: IBackupManager = get().get() - if (backupManager.isBackupEnabled) { + return if (backupManager.isBackupEnabled) { val packageService: PackageService = get().get() val packages = packageService.eligiblePackages val appTotals = packageService.expectedAppTotals @@ -78,11 +83,14 @@ fun requestBackup(context: Context) { nm.onBackupError() } if (result == BackupManager.SUCCESS) { - Log.i(TAG, "Backup succeeded ") + Log.i(TAG, "Backup request succeeded ") + true } else { - Log.e(TAG, "Backup failed: $result") + Log.e(TAG, "Backup request failed: $result") + false } } else { Log.i(TAG, "Backup is not enabled") + true // this counts as success } } diff --git a/build.libs.toml b/build.libs.toml index b723baab..03af200d 100644 --- a/build.libs.toml +++ b/build.libs.toml @@ -38,7 +38,7 @@ coroutines = { strictly = "1.6.4" } # AndroidX versions # https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-14.0.0_r1/current/androidx/Android.bp -room = { strictly = "2.4.0-alpha05" } +room = { strictly = "2.5.0" } androidx-core = { strictly = "1.9.0-alpha05" } androidx-fragment = { strictly = "1.5.0-alpha03" } androidx-activity = { strictly = "1.5.0-alpha03" } @@ -47,6 +47,7 @@ androidx-lifecycle-viewmodel-ktx = { strictly = "2.5.0-alpha03" } androidx-lifecycle-livedata-ktx = { strictly = "2.5.0-alpha03" } androidx-constraintlayout = { strictly = "2.2.0-alpha05" } androidx-documentfile = { strictly = "1.1.0-alpha01" } +androidx-work-runtime = { strictly = "2.9.0-alpha01" } [libraries] # Kotlin standard dependencies @@ -76,6 +77,7 @@ androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-view androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidx-lifecycle-livedata-ktx" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidx-documentfile" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work-runtime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } [bundles]