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.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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
Loading…
Add table
Reference in a new issue