Make 'Backup now' action use AppBackupWorker
This commit is contained in:
parent
49066be31b
commit
8da73ad8d1
16 changed files with 172 additions and 168 deletions
|
@ -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!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue