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]