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 { BackupNotificationManager(this@App) }
single { StoragePluginManager(this@App, get(), get(), get()) }
single { BackupStateManager(this@App) }
single { Clock() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
factory { AppListRetriever(this@App, get(), get(), get()) }
@ -72,6 +73,7 @@ open class App : Application() {
storageBackup = get(),
backupManager = get(),
backupInitializer = get(),
backupStateManager = 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
fun canDoBackupNow(): Boolean {
val storage = storageProperties ?: return false
val systemContext = context.getStorageContext { storage.isUsb }
return !storage.isUnavailableUsb(systemContext) &&
return !isOnUnavailableUsb() &&
!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)
}
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
viewModel.onWorkerStateChanged()
setAppBackupSchedulingSummary(workInfo)
}

View file

@ -28,8 +28,8 @@ import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.DiffUtil.calculateDiff
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager
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.Companion.UNIQUE_WORK_NAME
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.withContext
import org.calyxos.backup.storage.api.StorageBackup
@ -67,6 +70,7 @@ internal class SettingsViewModel(
private val storageBackup: StorageBackup,
private val backupManager: IBackupManager,
private val backupInitializer: BackupInitializer,
backupStateManager: BackupStateManager,
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
private val contentResolver = app.contentResolver
@ -76,6 +80,7 @@ internal class SettingsViewModel(
override val isRestoreOperation = false
private val isBackupRunning: StateFlow<Boolean>
private val mBackupPossible = MutableLiveData(false)
val backupPossible: LiveData<Boolean> = mBackupPossible
@ -125,9 +130,18 @@ internal class SettingsViewModel(
// this shouldn't cause disk reads, but it still does
viewModelScope
}
isBackupRunning = backupStateManager.isBackupRunning.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
scope.launch {
// ensures the lastBackupTime LiveData gets set
metadataManager.getLastBackupTime()
// update running state
isBackupRunning.collect {
onBackupRunningStateChanged()
}
}
onStoragePropertiesChanged()
loadFilesSummary()
@ -150,10 +164,10 @@ internal class SettingsViewModel(
onStoragePropertiesChanged()
}
fun onWorkerStateChanged() {
viewModelScope.launch(Dispatchers.IO) {
val canDo = pluginManager.canDoBackupNow() &&
appBackupWorkInfo.value?.state != WorkInfo.State.RUNNING
private fun onBackupRunningStateChanged() {
if (isBackupRunning.value) mBackupPossible.postValue(false)
else viewModelScope.launch(Dispatchers.IO) {
val canDo = !isBackupRunning.value && !pluginManager.isOnUnavailableUsb()
mBackupPossible.postValue(canDo)
}
}
@ -180,6 +194,7 @@ internal class SettingsViewModel(
connectivityManager?.unregisterNetworkCallback(networkCallback)
networkCallback.registered = false
} 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()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
@ -187,7 +202,7 @@ internal class SettingsViewModel(
networkCallback.registered = true
}
// update whether we can do backups right now or not
onWorkerStateChanged()
onBackupRunningStateChanged()
}
override fun onCleared() {

View file

@ -3,6 +3,8 @@ package com.stevesoltys.seedvault.storage
import android.content.Intent
import com.stevesoltys.seedvault.plugins.StoragePluginManager
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.RestoreObserver
import org.calyxos.backup.storage.api.StorageBackup
@ -31,6 +33,8 @@ internal class StorageBackupService : BackupService() {
companion object {
internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning.asStateFlow()
}
override val storageBackup: StorageBackup by inject()
@ -41,6 +45,16 @@ internal class StorageBackupService : BackupService() {
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) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
val isUsb = storagePluginManager.storageProperties?.isUsb ?: false

View file

@ -7,6 +7,8 @@ import android.os.IBinder
import android.util.Log
import com.stevesoltys.seedvault.crypto.KeyManager
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.inject
@ -18,6 +20,11 @@ private val TAG = ConfigurableBackupTransportService::class.java.simpleName
*/
class ConfigurableBackupTransportService : Service(), KoinComponent {
companion object {
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning.asStateFlow()
}
private var transport: ConfigurableBackupTransport? = null
private val keyManager: KeyManager by inject()
@ -27,6 +34,7 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
override fun onCreate() {
super.onCreate()
transport = ConfigurableBackupTransport(applicationContext)
_isRunning.value = true
Log.d(TAG, "Service created.")
}
@ -47,6 +55,7 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
super.onDestroy()
notificationManager.onServiceDestroyed()
transport = null
_isRunning.value = false
Log.d(TAG, "Service destroyed.")
}