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.Build
import android.os.ServiceManager.getService import android.os.ServiceManager.getService
import android.os.StrictMode import android.os.StrictMode
import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
import android.provider.Settings
import androidx.work.WorkManager
import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.MetadataManager 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.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.AppBackupWorker
import com.stevesoltys.seedvault.worker.workerModule 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
@ -43,6 +47,8 @@ import org.koin.dsl.module
*/ */
open class App : Application() { open class App : Application() {
open val isTest: Boolean = false
private val appModule = module { private val appModule = module {
single { SettingsManager(this@App) } single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) } single { BackupNotificationManager(this@App) }
@ -79,6 +85,7 @@ open class App : Application() {
permitDiskReads { permitDiskReads {
migrateTokenFromMetadataToSettingsManager() migrateTokenFromMetadataToSettingsManager()
} }
if (!isTest) migrateToOwnScheduling()
} }
protected open fun startKoin() = startKoin { protected open fun startKoin() = startKoin {
@ -102,6 +109,7 @@ open class App : Application() {
private val settingsManager: SettingsManager by inject() private val settingsManager: SettingsManager by inject()
private val metadataManager: MetadataManager 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] * 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 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) val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
d2dPreference?.setOnPreferenceChangeListener { _, newValue -> d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
viewModel.onD2dChanged(newValue as Boolean) d2dPreference.isChecked = newValue as Boolean
d2dPreference.isChecked = newValue
// automatically enable unlimited quota when enabling D2D backups // automatically enable unlimited quota when enabling D2D backups
if (d2dPreference.isChecked) { if (d2dPreference.isChecked) {

View file

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

View file

@ -12,7 +12,6 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.Uri import android.net.Uri
import android.os.Process.myUid import android.os.Process.myUid
import android.os.UserHandle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
@ -92,19 +91,19 @@ internal class SettingsViewModel(
private val storageObserver = object : ContentObserver(null) { private val storageObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uris: MutableCollection<Uri>, flags: Int) { override fun onChange(selfChange: Boolean, uris: MutableCollection<Uri>, flags: Int) {
onStorageLocationChanged() onStoragePropertiesChanged()
} }
} }
private inner class NetworkObserver : ConnectivityManager.NetworkCallback() { private inner class NetworkObserver : ConnectivityManager.NetworkCallback() {
var registered = false var registered = false
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
onStorageLocationChanged() onStoragePropertiesChanged()
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
super.onLost(network) super.onLost(network)
onStorageLocationChanged() onStoragePropertiesChanged()
} }
} }
@ -119,13 +118,29 @@ internal class SettingsViewModel(
// ensures the lastBackupTime LiveData gets set // ensures the lastBackupTime LiveData gets set
metadataManager.getLastBackupTime() metadataManager.getLastBackupTime()
} }
onStorageLocationChanged() onStoragePropertiesChanged()
loadFilesSummary() loadFilesSummary()
} }
override fun onStorageLocationChanged() { override fun onStorageLocationChanged() {
val storage = settingsManager.getStorage() ?: return 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 // register storage observer
try { try {
contentResolver.unregisterContentObserver(storageObserver) contentResolver.unregisterContentObserver(storageObserver)
@ -148,14 +163,6 @@ internal class SettingsViewModel(
networkCallback.registered = true 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) { viewModelScope.launch(Dispatchers.IO) {
val canDo = settingsManager.canDoBackupNow() val canDo = settingsManager.canDoBackupNow()
mBackupPossible.postValue(canDo) mBackupPossible.postValue(canDo)
@ -231,20 +238,24 @@ internal class SettingsViewModel(
return keyManager.hasMainKey() return keyManager.hasMainKey()
} }
fun enableStorageBackup() { fun scheduleBackupWorkers() {
val storage = settingsManager.getStorage() ?: error("no storage available") val storage = settingsManager.getStorage() ?: error("no storage available")
if (!storage.isUsb) BackupJobService.scheduleJob( if (!storage.isUsb) {
context = app, if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app)
jobServiceClass = StorageBackupJobService::class.java, if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob(
periodMillis = HOURS.toMillis(24), context = app,
networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED jobServiceClass = StorageBackupJobService::class.java,
else NETWORK_TYPE_NONE, periodMillis = HOURS.toMillis(24),
deviceIdle = false, networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED
charging = true else NETWORK_TYPE_NONE,
) deviceIdle = false,
charging = true
)
}
} }
fun disableStorageBackup() { fun cancelBackupWorkers() {
AppBackupWorker.unschedule(app)
BackupJobService.cancelJob(app) BackupJobService.cancelJob(app)
} }
@ -272,13 +283,4 @@ internal class SettingsViewModel(
Toast.makeText(app, str, LENGTH_LONG).show() 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) { } catch (e: Exception) {
Log.e(TAG, "Error while running setForeground: ", e) 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() var result: Result = Result.success()
try { try {
Log.i(TAG, "Starting APK backup...") Log.i(TAG, "Starting APK backup...")
@ -92,19 +105,10 @@ class AppBackupWorker(
result = Result.retry() result = Result.retry()
} finally { } finally {
Log.i(TAG, "Requesting app data backup...") Log.i(TAG, "Requesting app data backup...")
val requestSuccess = try { val requestSuccess = if (backupRequester.isBackupEnabled) {
if (backupRequester.isBackupEnabled) { Log.d(TAG, "Backup is enabled, request backup...")
Log.d(TAG, "Backup is enabled, request backup...") backupRequester.requestBackup()
backupRequester.requestBackup() } else true
} 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() if (!requestSuccess) result = Result.retry()
} }
return result return result

View file

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