Migrate to own backup scheduling

This commit is contained in:
Torsten Grote 2024-02-21 17:08:35 -03:00
parent 911a8dabf4
commit 04fc90e9f7
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
6 changed files with 91 additions and 51 deletions

View file

@ -9,7 +9,10 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.ServiceManager.getService
import android.os.StrictMode
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.work.WorkManager
import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.MetadataManager
@ -28,6 +31,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.AppBackupWorker
import com.stevesoltys.seedvault.worker.workerModule
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
@ -43,6 +47,8 @@ import org.koin.dsl.module
*/
open class App : Application() {
open val isTest: Boolean = false
private val appModule = module {
single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) }
@ -79,6 +85,7 @@ open class App : Application() {
permitDiskReads {
migrateTokenFromMetadataToSettingsManager()
}
if (!isTest) migrateToOwnScheduling()
}
protected open fun startKoin() = startKoin {
@ -102,6 +109,7 @@ open class App : Application() {
private val settingsManager: SettingsManager by inject()
private val metadataManager: MetadataManager by inject()
private val backupManager: IBackupManager by inject()
/**
* The responsibility for the current token was moved to the [SettingsManager]
@ -117,6 +125,23 @@ open class App : Application() {
}
}
/**
* Disables the framework scheduling in favor of our own.
* Introduced in the first half of 2024 and can be removed after a suitable migration period.
*/
protected open fun migrateToOwnScheduling() {
if (!isFrameworkSchedulingEnabled()) return // already on own scheduling
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
if (backupManager.isBackupEnabled) AppBackupWorker.schedule(applicationContext)
// cancel old D2D worker
WorkManager.getInstance(this).cancelUniqueWork("APP_BACKUP")
}
private fun isFrameworkSchedulingEnabled(): Boolean = Settings.Secure.getInt(
contentResolver, Settings.Secure.BACKUP_SCHEDULING_ENABLED, 1
) == 1 // 1 means enabled which is the default
}
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL

View file

@ -44,8 +44,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
viewModel.onD2dChanged(newValue as Boolean)
d2dPreference.isChecked = newValue
d2dPreference.isChecked = newValue as Boolean
// automatically enable unlimited quota when enabling D2D backups
if (d2dPreference.isChecked) {

View file

@ -23,6 +23,7 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.restore.RestoreActivity
import com.stevesoltys.seedvault.ui.toRelativeTime
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@ -125,8 +126,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
backupStorage = findPreference("backup_storage")!!
backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val disable = !(newValue as Boolean)
// TODO this should really get moved out off the UI layer
if (disable) {
viewModel.disableStorageBackup()
viewModel.cancelBackupWorkers()
return@OnPreferenceChangeListener true
}
onEnablingStorageBackup()
@ -208,10 +210,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
else -> super.onOptionsItemSelected(item)
}
// TODO this should really get moved out off the UI layer
private fun trySetBackupEnabled(enabled: Boolean): Boolean {
return try {
backupManager.isBackupEnabled = enabled
if (enabled) viewModel.enableCallLogBackup()
if (enabled) {
AppBackupWorker.schedule(requireContext())
viewModel.enableCallLogBackup()
} else {
AppBackupWorker.unschedule(requireContext())
}
backup.isChecked = enabled
true
} catch (e: RemoteException) {
@ -307,7 +315,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
LENGTH_LONG
).show()
}
viewModel.enableStorageBackup()
viewModel.scheduleBackupWorkers()
backupStorage.isChecked = true
dialog.dismiss()
}

View file

@ -12,7 +12,6 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.Uri
import android.os.Process.myUid
import android.os.UserHandle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
@ -92,19 +91,19 @@ internal class SettingsViewModel(
private val storageObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uris: MutableCollection<Uri>, flags: Int) {
onStorageLocationChanged()
onStoragePropertiesChanged()
}
}
private inner class NetworkObserver : ConnectivityManager.NetworkCallback() {
var registered = false
override fun onAvailable(network: Network) {
onStorageLocationChanged()
onStoragePropertiesChanged()
}
override fun onLost(network: Network) {
super.onLost(network)
onStorageLocationChanged()
onStoragePropertiesChanged()
}
}
@ -119,13 +118,29 @@ internal class SettingsViewModel(
// ensures the lastBackupTime LiveData gets set
metadataManager.getLastBackupTime()
}
onStorageLocationChanged()
onStoragePropertiesChanged()
loadFilesSummary()
}
override fun onStorageLocationChanged() {
val storage = settingsManager.getStorage() ?: return
Log.i(TAG, "onStorageLocationChanged")
if (storage.isUsb) {
// disable storage backup if new storage is on USB
cancelBackupWorkers()
} else {
// enable it, just in case the previous storage was on USB,
// also to update the network requirement of the new storage
scheduleBackupWorkers()
}
onStoragePropertiesChanged()
}
private fun onStoragePropertiesChanged() {
val storage = settingsManager.getStorage() ?: return
Log.d(TAG, "onStoragePropertiesChanged")
// register storage observer
try {
contentResolver.unregisterContentObserver(storageObserver)
@ -148,14 +163,6 @@ internal class SettingsViewModel(
networkCallback.registered = true
}
if (settingsManager.isStorageBackupEnabled()) {
// disable storage backup if new storage is on USB
if (storage.isUsb) disableStorageBackup()
// enable it, just in case the previous storage was on USB,
// also to update the network requirement of the new storage
else enableStorageBackup()
}
viewModelScope.launch(Dispatchers.IO) {
val canDo = settingsManager.canDoBackupNow()
mBackupPossible.postValue(canDo)
@ -231,20 +238,24 @@ internal class SettingsViewModel(
return keyManager.hasMainKey()
}
fun enableStorageBackup() {
fun scheduleBackupWorkers() {
val storage = settingsManager.getStorage() ?: error("no storage available")
if (!storage.isUsb) BackupJobService.scheduleJob(
context = app,
jobServiceClass = StorageBackupJobService::class.java,
periodMillis = HOURS.toMillis(24),
networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED
else NETWORK_TYPE_NONE,
deviceIdle = false,
charging = true
)
if (!storage.isUsb) {
if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app)
if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob(
context = app,
jobServiceClass = StorageBackupJobService::class.java,
periodMillis = HOURS.toMillis(24),
networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED
else NETWORK_TYPE_NONE,
deviceIdle = false,
charging = true
)
}
}
fun disableStorageBackup() {
fun cancelBackupWorkers() {
AppBackupWorker.unschedule(app)
BackupJobService.cancelJob(app)
}
@ -272,13 +283,4 @@ internal class SettingsViewModel(
Toast.makeText(app, str, LENGTH_LONG).show()
}
fun onD2dChanged(enabled: Boolean) {
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), !enabled)
if (enabled) {
AppBackupWorker.schedule(app)
} else {
AppBackupWorker.unschedule(app)
}
}
}

View file

@ -83,6 +83,19 @@ class AppBackupWorker(
} catch (e: Exception) {
Log.e(TAG, "Error while running setForeground: ", e)
}
return try {
doBackup()
} 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) && backupRequester.isBackupEnabled) {
// needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled
schedule(applicationContext, CANCEL_AND_REENQUEUE)
}
}
}
private suspend fun doBackup(): Result {
var result: Result = Result.success()
try {
Log.i(TAG, "Starting APK backup...")
@ -92,19 +105,10 @@ class AppBackupWorker(
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)
}
}
val requestSuccess = if (backupRequester.isBackupEnabled) {
Log.d(TAG, "Backup is enabled, request backup...")
backupRequester.requestBackup()
} else true
if (!requestSuccess) result = Result.retry()
}
return result

View file

@ -19,6 +19,8 @@ import org.koin.dsl.module
class TestApp : App() {
override val isTest: Boolean = true
private val testCryptoModule = module {
factory<CipherFactory> { CipherFactoryImpl(get()) }
single<KeyManager> { KeyManagerTestImpl() }