Properly track if a backup is running
this is important, so we don't allow more than one backup running at the same time and not swapping out the storage while one is running. Previously, we had some bare bones tracking, but nothing precise.
This commit is contained in:
parent
b1c87a8a9e
commit
0e4c37e796
7 changed files with 102 additions and 9 deletions
|
@ -57,6 +57,7 @@ open class App : Application() {
|
||||||
single { SettingsManager(this@App) }
|
single { SettingsManager(this@App) }
|
||||||
single { BackupNotificationManager(this@App) }
|
single { BackupNotificationManager(this@App) }
|
||||||
single { StoragePluginManager(this@App, get(), get(), get()) }
|
single { StoragePluginManager(this@App, get(), get(), get()) }
|
||||||
|
single { BackupStateManager(this@App) }
|
||||||
single { Clock() }
|
single { Clock() }
|
||||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
factory { AppListRetriever(this@App, get(), get(), get()) }
|
||||||
|
@ -72,6 +73,7 @@ open class App : Application() {
|
||||||
storageBackup = get(),
|
storageBackup = get(),
|
||||||
backupManager = get(),
|
backupManager = get(),
|
||||||
backupInitializer = get(),
|
backupInitializer = get(),
|
||||||
|
backupStateManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.WorkInfo.State.RUNNING
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageBackupService
|
||||||
|
import com.stevesoltys.seedvault.transport.ConfigurableBackupTransportService
|
||||||
|
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
|
private const val TAG = "BackupStateManager"
|
||||||
|
|
||||||
|
class BackupStateManager(
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val workManager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val isBackupRunning: Flow<Boolean> = combine(
|
||||||
|
flow = ConfigurableBackupTransportService.isRunning,
|
||||||
|
flow2 = StorageBackupService.isRunning,
|
||||||
|
flow3 = workManager.getWorkInfosForUniqueWorkFlow(UNIQUE_WORK_NAME),
|
||||||
|
) { appBackupRunning, filesBackupRunning, workInfos ->
|
||||||
|
val workInfoState = workInfos.getOrNull(0)?.state
|
||||||
|
Log.i(
|
||||||
|
TAG, "appBackupRunning: $appBackupRunning, " +
|
||||||
|
"filesBackupRunning: $filesBackupRunning, " +
|
||||||
|
"workInfoState: ${workInfoState?.name}"
|
||||||
|
)
|
||||||
|
appBackupRunning || filesBackupRunning || workInfoState == RUNNING
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -139,9 +139,23 @@ class StoragePluginManager(
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun canDoBackupNow(): Boolean {
|
fun canDoBackupNow(): Boolean {
|
||||||
val storage = storageProperties ?: return false
|
val storage = storageProperties ?: return false
|
||||||
val systemContext = context.getStorageContext { storage.isUsb }
|
return !isOnUnavailableUsb() &&
|
||||||
return !storage.isUnavailableUsb(systemContext) &&
|
|
||||||
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)
|
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if storage is on a flash drive.
|
||||||
|
*
|
||||||
|
* Should be run off the UI thread (ideally I/O) because of disk access.
|
||||||
|
*
|
||||||
|
* @return true if flash drive is not plugged in,
|
||||||
|
* false if storage isn't on flash drive or it isn't plugged in.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun isOnUnavailableUsb(): Boolean {
|
||||||
|
val storage = storageProperties ?: return false
|
||||||
|
val systemContext = context.getStorageContext { storage.isUsb }
|
||||||
|
return storage.isUnavailableUsb(systemContext)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,7 +151,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
setAppBackupStatusSummary(time)
|
setAppBackupStatusSummary(time)
|
||||||
}
|
}
|
||||||
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
|
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
|
||||||
viewModel.onWorkerStateChanged()
|
|
||||||
setAppBackupSchedulingSummary(workInfo)
|
setAppBackupSchedulingSummary(workInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import com.stevesoltys.seedvault.BackupStateManager
|
||||||
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
|
||||||
|
@ -46,6 +46,9 @@ import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
|
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
@ -67,6 +70,7 @@ internal class SettingsViewModel(
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val backupInitializer: BackupInitializer,
|
private val backupInitializer: BackupInitializer,
|
||||||
|
backupStateManager: BackupStateManager,
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
|
||||||
|
|
||||||
private val contentResolver = app.contentResolver
|
private val contentResolver = app.contentResolver
|
||||||
|
@ -76,6 +80,7 @@ internal class SettingsViewModel(
|
||||||
|
|
||||||
override val isRestoreOperation = false
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
|
private val isBackupRunning: StateFlow<Boolean>
|
||||||
private val mBackupPossible = MutableLiveData(false)
|
private val mBackupPossible = MutableLiveData(false)
|
||||||
val backupPossible: LiveData<Boolean> = mBackupPossible
|
val backupPossible: LiveData<Boolean> = mBackupPossible
|
||||||
|
|
||||||
|
@ -125,9 +130,18 @@ internal class SettingsViewModel(
|
||||||
// this shouldn't cause disk reads, but it still does
|
// this shouldn't cause disk reads, but it still does
|
||||||
viewModelScope
|
viewModelScope
|
||||||
}
|
}
|
||||||
|
isBackupRunning = backupStateManager.isBackupRunning.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = false,
|
||||||
|
)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// ensures the lastBackupTime LiveData gets set
|
// ensures the lastBackupTime LiveData gets set
|
||||||
metadataManager.getLastBackupTime()
|
metadataManager.getLastBackupTime()
|
||||||
|
// update running state
|
||||||
|
isBackupRunning.collect {
|
||||||
|
onBackupRunningStateChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onStoragePropertiesChanged()
|
onStoragePropertiesChanged()
|
||||||
loadFilesSummary()
|
loadFilesSummary()
|
||||||
|
@ -150,10 +164,10 @@ internal class SettingsViewModel(
|
||||||
onStoragePropertiesChanged()
|
onStoragePropertiesChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onWorkerStateChanged() {
|
private fun onBackupRunningStateChanged() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
if (isBackupRunning.value) mBackupPossible.postValue(false)
|
||||||
val canDo = pluginManager.canDoBackupNow() &&
|
else viewModelScope.launch(Dispatchers.IO) {
|
||||||
appBackupWorkInfo.value?.state != WorkInfo.State.RUNNING
|
val canDo = !isBackupRunning.value && !pluginManager.isOnUnavailableUsb()
|
||||||
mBackupPossible.postValue(canDo)
|
mBackupPossible.postValue(canDo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,6 +194,7 @@ internal class SettingsViewModel(
|
||||||
connectivityManager?.unregisterNetworkCallback(networkCallback)
|
connectivityManager?.unregisterNetworkCallback(networkCallback)
|
||||||
networkCallback.registered = false
|
networkCallback.registered = false
|
||||||
} else if (!networkCallback.registered && storage.requiresNetwork) {
|
} else if (!networkCallback.registered && storage.requiresNetwork) {
|
||||||
|
// TODO we may want to warn the user when they start a backup on a metered connection
|
||||||
val request = NetworkRequest.Builder()
|
val request = NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.build()
|
.build()
|
||||||
|
@ -187,7 +202,7 @@ internal class SettingsViewModel(
|
||||||
networkCallback.registered = true
|
networkCallback.registered = true
|
||||||
}
|
}
|
||||||
// update whether we can do backups right now or not
|
// update whether we can do backups right now or not
|
||||||
onWorkerStateChanged()
|
onBackupRunningStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -3,6 +3,8 @@ package com.stevesoltys.seedvault.storage
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
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
|
||||||
|
@ -31,6 +33,8 @@ internal class StorageBackupService : BackupService() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
|
internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
|
||||||
|
private val _isRunning = MutableStateFlow(false)
|
||||||
|
val isRunning = _isRunning.asStateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val storageBackup: StorageBackup by inject()
|
override val storageBackup: StorageBackup by inject()
|
||||||
|
@ -41,6 +45,16 @@ internal class StorageBackupService : BackupService() {
|
||||||
NotificationBackupObserver(applicationContext)
|
NotificationBackupObserver(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
_isRunning.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_isRunning.value = false
|
||||||
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
val isUsb = storagePluginManager.storageProperties?.isUsb ?: false
|
val isUsb = storagePluginManager.storageProperties?.isUsb ?: false
|
||||||
|
|
|
@ -7,6 +7,8 @@ import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
@ -18,6 +20,11 @@ private val TAG = ConfigurableBackupTransportService::class.java.simpleName
|
||||||
*/
|
*/
|
||||||
class ConfigurableBackupTransportService : Service(), KoinComponent {
|
class ConfigurableBackupTransportService : Service(), KoinComponent {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val _isRunning = MutableStateFlow(false)
|
||||||
|
val isRunning = _isRunning.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
private var transport: ConfigurableBackupTransport? = null
|
private var transport: ConfigurableBackupTransport? = null
|
||||||
|
|
||||||
private val keyManager: KeyManager by inject()
|
private val keyManager: KeyManager by inject()
|
||||||
|
@ -27,6 +34,7 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
transport = ConfigurableBackupTransport(applicationContext)
|
transport = ConfigurableBackupTransport(applicationContext)
|
||||||
|
_isRunning.value = true
|
||||||
Log.d(TAG, "Service created.")
|
Log.d(TAG, "Service created.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +55,7 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
notificationManager.onServiceDestroyed()
|
notificationManager.onServiceDestroyed()
|
||||||
transport = null
|
transport = null
|
||||||
|
_isRunning.value = false
|
||||||
Log.d(TAG, "Service destroyed.")
|
Log.d(TAG, "Service destroyed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue