diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bc9213ab..c3bd194cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -156,6 +156,11 @@ + + { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory { AppListRetriever(this@App, get(), get(), get()) } - viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) } + viewModel { + SettingsViewModel( + app = this@App, + settingsManager = get(), + keyManager = get(), + notificationManager = get(), + metadataManager = get(), + appListRetriever = get(), + storageBackup = get(), + backupManager = get(), + workScheduler = get(), + ) + } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } @@ -95,6 +108,7 @@ open class App : Application() { restoreModule, installModule, storageModule, + workerModule, appModule ) diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt deleted file mode 100644 index 024461622..000000000 --- a/app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 The Calyx Institute - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.stevesoltys.seedvault - -import android.content.Context -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.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) - } - } - - 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/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index e220462c3..53e134cd2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -25,7 +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.worker.AppDataBackupWorker import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.metadata.MetadataManager @@ -36,6 +36,7 @@ import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_ST import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.worker.WorkScheduler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,6 +58,7 @@ internal class SettingsViewModel( private val appListRetriever: AppListRetriever, private val storageBackup: StorageBackup, private val backupManager: IBackupManager, + private val workScheduler: WorkScheduler, ) : RequireProvisioningViewModel(app, settingsManager, keyManager) { private val contentResolver = app.contentResolver @@ -266,9 +268,9 @@ internal class SettingsViewModel( fun onD2dChanged(enabled: Boolean) { backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), !enabled) if (enabled) { - BackupWorker.schedule(app) + AppDataBackupWorker.schedule(workScheduler) } else { - BackupWorker.unschedule(app) + AppDataBackupWorker.unschedule(workScheduler) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupWorker.kt new file mode 100644 index 000000000..13e7270c6 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupWorker.kt @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.worker + +import android.content.Context +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_APK +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class ApkBackupWorker( + appContext: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(appContext, workerParams), KoinComponent { + + companion object { + private const val UNIQUE_WORK_NAME = "APK_BACKUP" + + fun schedule(workScheduler: WorkScheduler) { + workScheduler.schedule(UNIQUE_WORK_NAME) + } + + fun scheduleNow(workScheduler: WorkScheduler) { + workScheduler.scheduleNow(UNIQUE_WORK_NAME) + } + + fun unschedule(workScheduler: WorkScheduler) { + workScheduler.unschedule(UNIQUE_WORK_NAME) + } + } + + private val apkBackupManager by inject() + private val nm by inject() + + override suspend fun doWork(): Result { + try { + setForeground(createForegroundInfo()) + } catch (e: Exception) { + Log.e("ApkBackupWorker", "Error while running setForeground: ", e) + } + return try { + apkBackupManager.backup() + Result.success() + } catch (e: Exception) { + Log.e(ApkBackupWorker::class.simpleName, "Error backing up APKs: ", e) + Result.retry() + } + } + + private fun createForegroundInfo() = ForegroundInfo( + NOTIFICATION_ID_APK, + nm.getApkBackupNotification(null), + FOREGROUND_SERVICE_TYPE_DATA_SYNC, + ) +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppDataBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppDataBackupWorker.kt new file mode 100644 index 000000000..efe5dd80b --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppDataBackupWorker.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.worker + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.stevesoltys.seedvault.transport.requestBackup + +class AppDataBackupWorker( + appContext: Context, + workerParams: WorkerParameters, +) : Worker(appContext, workerParams) { + + companion object { + private const val UNIQUE_WORK_NAME = "APP_BACKUP" + + fun schedule(workScheduler: WorkScheduler) { + workScheduler.schedule(UNIQUE_WORK_NAME) + } + + fun unschedule(workScheduler: WorkScheduler) { + workScheduler.unschedule(UNIQUE_WORK_NAME) + } + } + + 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/worker/WorkScheduler.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkScheduler.kt new file mode 100644 index 000000000..65d19f953 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkScheduler.kt @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.worker + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE +import androidx.work.ExistingWorkPolicy.REPLACE +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit + +class WorkScheduler(appContext: Context) { + + val workManager = WorkManager.getInstance(appContext) + + inline fun schedule( + uniqueWorkName: String, + ) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresCharging(true) + .build() + val workRequest = PeriodicWorkRequestBuilder( + repeatInterval = 24, + repeatIntervalTimeUnit = TimeUnit.HOURS, + flexTimeInterval = 2, + flexTimeIntervalUnit = TimeUnit.HOURS, + ).setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS) + .build() + workManager.enqueueUniquePeriodicWork(uniqueWorkName, UPDATE, workRequest) + } + + inline fun scheduleNow( + uniqueWorkName: String, + ) { + val workRequest = OneTimeWorkRequestBuilder() + .setExpedited(RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + // TODO still replaces scheduled request + workManager.enqueueUniqueWork(uniqueWorkName, REPLACE, workRequest) + } + + fun unschedule(uniqueWorkName: String) { + workManager.cancelUniqueWork(uniqueWorkName) + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt new file mode 100644 index 000000000..d7a41a3f0 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.worker + +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val workerModule = module { + single { WorkScheduler(androidContext()) } + single { ApkBackupManager(androidContext(), get(), get(), get(), get(), get(), get()) } +}