Migrate to own backup scheduling
This commit is contained in:
parent
911a8dabf4
commit
04fc90e9f7
6 changed files with 91 additions and 51 deletions
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
|
|
Loading…
Reference in a new issue