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:
Torsten Grote 2024-04-16 16:31:53 -03:00
parent b1c87a8a9e
commit 0e4c37e796
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
7 changed files with 102 additions and 9 deletions

View file

@ -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()) }

View file

@ -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
}
}

View file

@ -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)
}
} }

View file

@ -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)
} }

View file

@ -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() {

View file

@ -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

View file

@ -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.")
} }