Use our own scheduling when doing d2d backups (experimental)
diff --git a/Android.bp b/Android.bp
index 04c3cc1..d61e977 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,6 +30,7 @@
"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 52a0e18..ec43fa2 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -135,6 +135,7 @@
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 0000000..d0352e4
--- /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<BackupWorker>(
+ 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 c7e7d37..b128132 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 @@
val d2dPreference = findPreference<SwitchPreferenceCompat>(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 3c19065..e220462 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.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.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 @@
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 1d67988..443badd 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 @@
}
+/**
+ * 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 @@
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 b723baa..03af200 100644
--- a/build.libs.toml
+++ b/build.libs.toml
@@ -38,7 +38,7 @@
# 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-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-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]