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 { viewModel {
currentBackupStorageViewModel = currentBackupStorageViewModel =
spyk(BackupStorageViewModel(context, get(), get(), get(), get())) spyk(BackupStorageViewModel(context, get(), get(), get()))
currentBackupStorageViewModel!! currentBackupStorageViewModel!!
} }

View file

@ -156,6 +156,11 @@
</intent-filter> </intent-filter>
</receiver> </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 --> <!-- Used to start actual BackupService depending on scheduling criteria -->
<service <service
android:name=".storage.StorageBackupJobService" 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.recoverycode.RecoveryCodeViewModel
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel 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.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger 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 { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, 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 { RestoreStorageViewModel(this@App, get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { FileSelectionViewModel(this@App, get()) } viewModel { FileSelectionViewModel(this@App, get()) }
@ -95,6 +96,7 @@ open class App : Application() {
restoreModule, restoreModule,
installModule, installModule,
storageModule, storageModule,
workerModule,
appModule 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.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP 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.ui.storage.AUTHORITY_STORAGE
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.koin.core.context.GlobalContext.get import org.koin.core.context.GlobalContext.get
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
@ -63,9 +63,7 @@ class UsbIntentReceiver : UsbMonitor() {
i.putExtra(EXTRA_START_APP_BACKUP, true) i.putExtra(EXTRA_START_APP_BACKUP, true)
startForegroundService(context, i) startForegroundService(context, i)
} else { } else {
Thread { AppBackupWorker.scheduleNow(context)
requestBackup(context)
}.start()
} }
} }

View file

@ -39,7 +39,7 @@ import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.transport.TRANSPORT_ID 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.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED import com.stevesoltys.seedvault.ui.AppBackupState.FAILED

View file

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

View file

@ -1,7 +1,7 @@
package com.stevesoltys.seedvault.storage package com.stevesoltys.seedvault.storage
import android.content.Intent 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.BackupObserver
import org.calyxos.backup.storage.api.RestoreObserver import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.api.StorageBackup
@ -40,7 +40,7 @@ internal class StorageBackupService : BackupService() {
override fun onBackupFinished(intent: Intent, success: Boolean) { override fun onBackupFinished(intent: Intent, success: Boolean) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) { 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.Service
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.crypto.KeyManager 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 com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.get
private val TAG = ConfigurableBackupTransportService::class.java.simpleName 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 import kotlin.math.min
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" 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_SUCCESS = "NotificationBackupSuccess"
private const val CHANNEL_ID_ERROR = "NotificationError" private const val CHANNEL_ID_ERROR = "NotificationError"
private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError" private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError"
private const val NOTIFICATION_ID_OBSERVER = 1 internal const val NOTIFICATION_ID_OBSERVER = 1
internal const val NOTIFICATION_ID_APK = 2 private const val NOTIFICATION_ID_SUCCESS = 2
private const val NOTIFICATION_ID_SUCCESS = 3 private const val NOTIFICATION_ID_ERROR = 3
private const val NOTIFICATION_ID_ERROR = 4 private const val NOTIFICATION_ID_RESTORE_ERROR = 4
private const val NOTIFICATION_ID_RESTORE_ERROR = 5 private const val NOTIFICATION_ID_BACKGROUND = 5
private const val NOTIFICATION_ID_BACKGROUND = 6 private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 6
private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 7
private val TAG = BackupNotificationManager::class.java.simpleName 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 { private val nm = context.getSystemService(NotificationManager::class.java)!!.apply {
createNotificationChannel(getObserverChannel()) createNotificationChannel(getObserverChannel())
createNotificationChannel(getApkChannel())
createNotificationChannel(getSuccessChannel()) createNotificationChannel(getSuccessChannel())
createNotificationChannel(getErrorChannel()) createNotificationChannel(getErrorChannel())
createNotificationChannel(getRestoreErrorChannel()) 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 { private fun getSuccessChannel(): NotificationChannel {
val title = context.getString(R.string.notification_success_channel_title) val title = context.getString(R.string.notification_success_channel_title)
return NotificationChannel(CHANNEL_ID_SUCCESS, title, IMPORTANCE_LOW).apply { 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) { fun onApkBackup(packageName: String, name: CharSequence, transferred: Int, expected: Int) {
Log.i(TAG, "$transferred/$expected - $name ($packageName)") Log.i(TAG, "$transferred/$expected - $name ($packageName)")
val text = context.getString(R.string.notification_apk_text, name) val text = context.getString(R.string.notification_apk_text, name)
val notification = getApkBackupNotification(text, transferred, expected) updateBackupNotification(text, transferred, expected)
nm.notify(NOTIFICATION_ID_APK, notification)
} }
/** /**
@ -100,32 +89,15 @@ internal class BackupNotificationManager(private val context: Context) {
*/ */
fun onAppsNotBackedUp() { fun onAppsNotBackedUp() {
Log.i(TAG, "onAppsNotBackedUp") Log.i(TAG, "onAppsNotBackedUp")
val notification = val text = context.getString(R.string.notification_apk_not_backed_up)
getApkBackupNotification(context.getString(R.string.notification_apk_not_backed_up)) updateBackupNotification(text)
nm.notify(NOTIFICATION_ID_APK, notification)
} }
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. * Call after [onApkBackup] or [onAppsNotBackedUp] were called.
*/ */
fun onApkBackupDone() { 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) { fun onBackupStarted(expectedPackages: Int) {
updateBackupNotification( updateBackupNotification(
appName = "", // This passes quickly, no need to show something here text = "", // This passes quickly, no need to show something here
transferred = 0, transferred = 0,
expected = expectedPackages expected = expectedPackages
) )
@ -145,44 +117,29 @@ internal class BackupNotificationManager(private val context: Context) {
* this type is is expected to get called after [onApkBackup]. * this type is is expected to get called after [onApkBackup].
*/ */
fun onBackupUpdate(app: CharSequence, transferred: Int, total: Int) { fun onBackupUpdate(app: CharSequence, transferred: Int, total: Int) {
updateBackupNotification( updateBackupNotification(app, min(transferred, total), total)
appName = app,
transferred = min(transferred, total),
expected = total
)
} }
private fun updateBackupNotification( private fun updateBackupNotification(
appName: CharSequence, text: CharSequence,
transferred: Int, transferred: Int = 0,
expected: Int, expected: Int = 0,
) { ) {
val notification = Builder(context, CHANNEL_ID_OBSERVER).apply { val notification = getBackupNotification(text, transferred, expected)
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()
nm.notify(NOTIFICATION_ID_OBSERVER, notification) nm.notify(NOTIFICATION_ID_OBSERVER, notification)
} }
private fun updateBackgroundBackupNotification(infoText: CharSequence) { fun getBackupNotification(text: CharSequence, progress: Int = 0, total: Int = 0): Notification {
Log.i(TAG, "$infoText") return Builder(context, CHANNEL_ID_OBSERVER).apply {
val notification = Builder(context, CHANNEL_ID_OBSERVER).apply {
setSmallIcon(R.drawable.ic_cloud_upload) setSmallIcon(R.drawable.ic_cloud_upload)
setContentTitle(context.getString(R.string.notification_title)) setContentTitle(context.getString(R.string.notification_title))
setContentText(text)
setOngoing(true) setOngoing(true)
setShowWhen(false) setShowWhen(false)
setWhen(System.currentTimeMillis()) setProgress(total, progress, progress == 0 && total == 0)
setProgress(0, 0, true) priority = PRIORITY_DEFAULT
priority = PRIORITY_LOW foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}.build() }.build()
nm.notify(NOTIFICATION_ID_BACKGROUND, notification)
} }
fun onServiceDestroyed() { fun onServiceDestroyed() {

View file

@ -10,7 +10,7 @@ import android.util.Log.isLoggable
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager 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.KoinComponent
import org.koin.core.component.inject 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.R
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.worker.AppBackupWorker
import com.stevesoltys.seedvault.transport.requestBackup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.api.StorageBackup
@ -24,7 +23,6 @@ private val TAG = BackupStorageViewModel::class.java.simpleName
internal class BackupStorageViewModel( internal class BackupStorageViewModel(
private val app: Application, private val app: Application,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val backupCoordinator: BackupCoordinator,
private val storageBackup: StorageBackup, private val storageBackup: StorageBackup,
settingsManager: SettingsManager, settingsManager: SettingsManager,
) : StorageViewModel(app, settingsManager) { ) : StorageViewModel(app, settingsManager) {
@ -73,7 +71,7 @@ internal class BackupStorageViewModel(
// notify the UI that the location has been set // notify the UI that the location has been set
mLocationChecked.postEvent(LocationResult()) mLocationChecked.postEvent(LocationResult())
if (requestBackup) { if (requestBackup) {
requestBackup(app) AppBackupWorker.scheduleNow(app)
} }
} else { } else {
// notify the UI that the location was invalid // 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 * 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.BackupManager
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
@ -12,6 +12,7 @@ import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupMonitor 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.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -34,6 +35,8 @@ internal class BackupRequester(
val packageService: PackageService, val packageService: PackageService,
) : KoinComponent { ) : KoinComponent {
val isBackupEnabled: Boolean get() = backupManager.isBackupEnabled
private val packages = packageService.eligiblePackages private val packages = packageService.eligiblePackages
private val observer = NotificationBackupObserver( private val observer = NotificationBackupObserver(
context = context, context = context,

View file

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

View file

@ -119,7 +119,6 @@
<!-- Notification --> <!-- Notification -->
<string name="notification_channel_title">Backup notification</string> <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_success_channel_title">Success notification</string>
<string name="notification_title">Backup running</string> <string name="notification_title">Backup running</string>
<string name="notification_apk_text">Backing up APK of %s</string> <string name="notification_apk_text">Backing up APK of %s</string>