Make 'Backup now' action use AppBackupWorker

This commit is contained in:
Torsten Grote 2024-02-20 11:52:49 -03:00
parent 49066be31b
commit 8da73ad8d1
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
16 changed files with 172 additions and 168 deletions

View file

@ -47,7 +47,7 @@ class KoinInstrumentationTestApp : App() {
viewModel {
currentBackupStorageViewModel =
spyk(BackupStorageViewModel(context, get(), get(), get(), get()))
spyk(BackupStorageViewModel(context, get(), get(), get()))
currentBackupStorageViewModel!!
}

View file

@ -156,6 +156,11 @@
</intent-filter>
</receiver>
<!-- Used by Workmanager to schedule our workers -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<!-- Used to start actual BackupService depending on scheduling criteria -->
<service
android:name=".storage.StorageBackupJobService"

View file

@ -28,6 +28,7 @@ import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
import com.stevesoltys.seedvault.worker.workerModule
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@ -51,7 +52,7 @@ open class App : Application() {
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { FileSelectionViewModel(this@App, get()) }
@ -95,6 +96,7 @@ open class App : Application() {
restoreModule,
installModule,
storageModule,
workerModule,
appModule
)

View file

@ -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<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)
}
}
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()
}
}

View file

@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.koin.core.context.GlobalContext.get
import java.util.concurrent.TimeUnit.HOURS
@ -63,9 +63,7 @@ class UsbIntentReceiver : UsbMonitor() {
i.putExtra(EXTRA_START_APP_BACKUP, true)
startForegroundService(context, i)
} else {
Thread {
requestBackup(context)
}.start()
AppBackupWorker.scheduleNow(context)
}
}

View file

@ -39,7 +39,7 @@ import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.NUM_PACKAGES_PER_TRANSACTION
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED

View file

@ -25,7 +25,6 @@ import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
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
@ -33,9 +32,9 @@ import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.storage.StorageBackupJobService
import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.worker.AppBackupWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -175,7 +174,7 @@ internal class SettingsViewModel(
i.putExtra(EXTRA_START_APP_BACKUP, true)
startForegroundService(app, i)
} else {
requestBackup(app)
AppBackupWorker.scheduleNow(app)
}
}
}
@ -267,9 +266,9 @@ internal class SettingsViewModel(
fun onD2dChanged(enabled: Boolean) {
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), !enabled)
if (enabled) {
BackupWorker.schedule(app)
AppBackupWorker.schedule(app)
} else {
BackupWorker.unschedule(app)
AppBackupWorker.unschedule(app)
}
}

View file

@ -1,7 +1,7 @@
package com.stevesoltys.seedvault.storage
import android.content.Intent
import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.calyxos.backup.storage.api.BackupObserver
import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.api.StorageBackup
@ -40,7 +40,7 @@ internal class StorageBackupService : BackupService() {
override fun onBackupFinished(intent: Intent, success: Boolean) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
requestBackup(applicationContext)
AppBackupWorker.scheduleNow(applicationContext)
}
}
}

View file

@ -2,18 +2,13 @@ package com.stevesoltys.seedvault.transport
import android.app.Service
import android.app.backup.IBackupManager
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.transport.backup.BackupRequester
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.get
private val TAG = ConfigurableBackupTransportService::class.java.simpleName
@ -56,23 +51,3 @@ 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): Boolean {
val backupManager: IBackupManager = get().get()
return if (backupManager.isBackupEnabled) {
val packageService: PackageService = get().get()
Log.d(TAG, "Backup is enabled, request backup...")
val backupRequester = BackupRequester(context, backupManager, packageService)
return backupRequester.requestBackup()
} else {
Log.i(TAG, "Backup is not enabled")
true // this counts as success
}
}

View file

@ -30,17 +30,15 @@ import com.stevesoltys.seedvault.settings.SettingsActivity
import kotlin.math.min
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
private const val CHANNEL_ID_APK = "NotificationApkBackup"
private const val CHANNEL_ID_SUCCESS = "NotificationBackupSuccess"
private const val CHANNEL_ID_ERROR = "NotificationError"
private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError"
private const val NOTIFICATION_ID_OBSERVER = 1
internal const val NOTIFICATION_ID_APK = 2
private const val NOTIFICATION_ID_SUCCESS = 3
private const val NOTIFICATION_ID_ERROR = 4
private const val NOTIFICATION_ID_RESTORE_ERROR = 5
private const val NOTIFICATION_ID_BACKGROUND = 6
private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 7
internal const val NOTIFICATION_ID_OBSERVER = 1
private const val NOTIFICATION_ID_SUCCESS = 2
private const val NOTIFICATION_ID_ERROR = 3
private const val NOTIFICATION_ID_RESTORE_ERROR = 4
private const val NOTIFICATION_ID_BACKGROUND = 5
private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 6
private val TAG = BackupNotificationManager::class.java.simpleName
@ -48,7 +46,6 @@ internal class BackupNotificationManager(private val context: Context) {
private val nm = context.getSystemService(NotificationManager::class.java)!!.apply {
createNotificationChannel(getObserverChannel())
createNotificationChannel(getApkChannel())
createNotificationChannel(getSuccessChannel())
createNotificationChannel(getErrorChannel())
createNotificationChannel(getRestoreErrorChannel())
@ -61,13 +58,6 @@ internal class BackupNotificationManager(private val context: Context) {
}
}
private fun getApkChannel(): NotificationChannel {
val title = context.getString(R.string.notification_apk_channel_title)
return NotificationChannel(CHANNEL_ID_APK, title, IMPORTANCE_LOW).apply {
enableVibration(false)
}
}
private fun getSuccessChannel(): NotificationChannel {
val title = context.getString(R.string.notification_success_channel_title)
return NotificationChannel(CHANNEL_ID_SUCCESS, title, IMPORTANCE_LOW).apply {
@ -91,8 +81,7 @@ internal class BackupNotificationManager(private val context: Context) {
fun onApkBackup(packageName: String, name: CharSequence, transferred: Int, expected: Int) {
Log.i(TAG, "$transferred/$expected - $name ($packageName)")
val text = context.getString(R.string.notification_apk_text, name)
val notification = getApkBackupNotification(text, transferred, expected)
nm.notify(NOTIFICATION_ID_APK, notification)
updateBackupNotification(text, transferred, expected)
}
/**
@ -100,32 +89,15 @@ internal class BackupNotificationManager(private val context: Context) {
*/
fun onAppsNotBackedUp() {
Log.i(TAG, "onAppsNotBackedUp")
val notification =
getApkBackupNotification(context.getString(R.string.notification_apk_not_backed_up))
nm.notify(NOTIFICATION_ID_APK, notification)
val text = context.getString(R.string.notification_apk_not_backed_up)
updateBackupNotification(text)
}
fun getApkBackupNotification(
text: String?,
expected: Int = 0,
transferred: Int = 0,
): Notification = Builder(context, CHANNEL_ID_APK).apply {
setSmallIcon(R.drawable.ic_cloud_upload)
setContentTitle(context.getString(R.string.notification_title))
setContentText(text)
setOngoing(true)
setShowWhen(false)
setWhen(System.currentTimeMillis())
setProgress(expected, transferred, false)
priority = PRIORITY_DEFAULT
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}.build()
/**
* Call after [onApkBackup] or [onAppsNotBackedUp] were called.
*/
fun onApkBackupDone() {
nm.cancel(NOTIFICATION_ID_APK)
nm.cancel(NOTIFICATION_ID_OBSERVER)
}
/**
@ -133,7 +105,7 @@ internal class BackupNotificationManager(private val context: Context) {
*/
fun onBackupStarted(expectedPackages: Int) {
updateBackupNotification(
appName = "", // This passes quickly, no need to show something here
text = "", // This passes quickly, no need to show something here
transferred = 0,
expected = expectedPackages
)
@ -145,44 +117,29 @@ internal class BackupNotificationManager(private val context: Context) {
* this type is is expected to get called after [onApkBackup].
*/
fun onBackupUpdate(app: CharSequence, transferred: Int, total: Int) {
updateBackupNotification(
appName = app,
transferred = min(transferred, total),
expected = total
)
updateBackupNotification(app, min(transferred, total), total)
}
private fun updateBackupNotification(
appName: CharSequence,
transferred: Int,
expected: Int,
text: CharSequence,
transferred: Int = 0,
expected: Int = 0,
) {
val notification = Builder(context, CHANNEL_ID_OBSERVER).apply {
setSmallIcon(R.drawable.ic_cloud_upload)
setContentTitle(context.getString(R.string.notification_title))
setContentText(appName)
setOngoing(true)
setShowWhen(false)
setWhen(System.currentTimeMillis())
setProgress(expected, transferred, false)
priority = PRIORITY_DEFAULT
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}.build()
val notification = getBackupNotification(text, transferred, expected)
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
}
private fun updateBackgroundBackupNotification(infoText: CharSequence) {
Log.i(TAG, "$infoText")
val notification = Builder(context, CHANNEL_ID_OBSERVER).apply {
fun getBackupNotification(text: CharSequence, progress: Int = 0, total: Int = 0): Notification {
return Builder(context, CHANNEL_ID_OBSERVER).apply {
setSmallIcon(R.drawable.ic_cloud_upload)
setContentTitle(context.getString(R.string.notification_title))
setContentText(text)
setOngoing(true)
setShowWhen(false)
setWhen(System.currentTimeMillis())
setProgress(0, 0, true)
priority = PRIORITY_LOW
setProgress(total, progress, progress == 0 && total == 0)
priority = PRIORITY_DEFAULT
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}.build()
nm.notify(NOTIFICATION_ID_BACKGROUND, notification)
}
fun onServiceDestroyed() {

View file

@ -10,7 +10,7 @@ import android.util.Log.isLoggable
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.transport.backup.BackupRequester
import com.stevesoltys.seedvault.worker.BackupRequester
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

View file

@ -12,8 +12,7 @@ import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.worker.AppBackupWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.calyxos.backup.storage.api.StorageBackup
@ -24,7 +23,6 @@ private val TAG = BackupStorageViewModel::class.java.simpleName
internal class BackupStorageViewModel(
private val app: Application,
private val backupManager: IBackupManager,
private val backupCoordinator: BackupCoordinator,
private val storageBackup: StorageBackup,
settingsManager: SettingsManager,
) : StorageViewModel(app, settingsManager) {
@ -73,7 +71,7 @@ internal class BackupStorageViewModel(
// notify the UI that the location has been set
mLocationChecked.postEvent(LocationResult())
if (requestBackup) {
requestBackup(app)
AppBackupWorker.scheduleNow(app)
}
} else {
// notify the UI that the location was invalid

View file

@ -0,0 +1,118 @@
/*
* 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.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.ExistingWorkPolicy.REPLACE
import androidx.work.ForegroundInfo
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 androidx.work.WorkerParameters
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.concurrent.TimeUnit
class AppBackupWorker(
appContext: Context,
workerParams: WorkerParameters,
) : CoroutineWorker(appContext, workerParams), KoinComponent {
companion object {
private val TAG = AppBackupWorker::class.simpleName
private const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP"
private const val TAG_NOW = "com.stevesoltys.seedvault.TAG_NOW"
fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
val workRequest = PeriodicWorkRequestBuilder<AppBackupWorker>(
repeatInterval = 24,
repeatIntervalTimeUnit = TimeUnit.HOURS,
flexTimeInterval = 2,
flexTimeIntervalUnit = TimeUnit.HOURS,
).setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
.build()
val workManager = WorkManager.getInstance(context)
Log.i(TAG, "Scheduling app backup: $workRequest")
workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, existingWorkPolicy, workRequest)
}
fun scheduleNow(context: Context) {
val workRequest = OneTimeWorkRequestBuilder<AppBackupWorker>()
.setExpedited(RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.addTag(TAG_NOW)
.build()
val workManager = WorkManager.getInstance(context)
Log.i(TAG, "Asking to do app backup now...")
workManager.enqueueUniqueWork(UNIQUE_WORK_NAME, REPLACE, workRequest)
}
fun unschedule(context: Context) {
Log.i(TAG, "Unscheduling app backup...")
val workManager = WorkManager.getInstance(context)
workManager.cancelUniqueWork(UNIQUE_WORK_NAME)
}
}
private val backupRequester: BackupRequester by inject()
private val apkBackupManager: ApkBackupManager by inject()
private val nm: BackupNotificationManager by inject()
override suspend fun doWork(): Result {
try {
setForeground(createForegroundInfo())
} catch (e: Exception) {
Log.e(TAG, "Error while running setForeground: ", e)
}
var result: Result = Result.success()
try {
Log.i(TAG, "Starting APK backup...")
apkBackupManager.backup()
} catch (e: Exception) {
Log.e(TAG, "Error backing up APKs: ", e)
result = Result.retry()
} finally {
Log.i(TAG, "Requesting app data backup...")
val requestSuccess = try {
if (backupRequester.isBackupEnabled) {
Log.d(TAG, "Backup is enabled, request backup...")
backupRequester.requestBackup()
} else true
} finally {
// schedule next backup, because the old one gets lost
// when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow()
if (tags.contains(TAG_NOW)) {
// needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled
schedule(applicationContext, CANCEL_AND_REENQUEUE)
}
}
if (!requestSuccess) result = Result.retry()
}
return result
}
private fun createForegroundInfo() = ForegroundInfo(
NOTIFICATION_ID_OBSERVER,
nm.getBackupNotification(""),
FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.transport.backup
package com.stevesoltys.seedvault.worker
import android.app.backup.BackupManager
import android.app.backup.IBackupManager
@ -12,6 +12,7 @@ import android.os.RemoteException
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
import org.koin.core.component.KoinComponent
@ -34,6 +35,8 @@ internal class BackupRequester(
val packageService: PackageService,
) : KoinComponent {
val isBackupEnabled: Boolean get() = backupManager.isBackupEnabled
private val packages = packageService.eligiblePackages
private val observer = NotificationBackupObserver(
context = context,

View file

@ -9,6 +9,13 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val workerModule = module {
factory {
BackupRequester(
context = androidContext(),
backupManager = get(),
packageService = get(),
)
}
single {
ApkBackup(
pm = androidContext().packageManager,

View file

@ -119,7 +119,6 @@
<!-- Notification -->
<string name="notification_channel_title">Backup notification</string>
<string name="notification_apk_channel_title">APK backup notification</string>
<string name="notification_success_channel_title">Success notification</string>
<string name="notification_title">Backup running</string>
<string name="notification_apk_text">Backing up APK of %s</string>