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 { 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()) }
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -151,7 +151,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
setAppBackupStatusSummary(time)
|
||||
}
|
||||
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
|
||||
viewModel.onWorkerStateChanged()
|
||||
setAppBackupSchedulingSummary(workInfo)
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue