From 0e4c37e7961ca6b15939da410309a92509836dd5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 16 Apr 2024 16:31:53 -0300 Subject: [PATCH] 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. --- .../java/com/stevesoltys/seedvault/App.kt | 2 + .../seedvault/BackupStateManager.kt | 40 +++++++++++++++++++ .../seedvault/plugins/StoragePluginManager.kt | 18 ++++++++- .../seedvault/settings/SettingsFragment.kt | 1 - .../seedvault/settings/SettingsViewModel.kt | 27 ++++++++++--- .../stevesoltys/seedvault/storage/Services.kt | 14 +++++++ .../ConfigurableBackupTransportService.kt | 9 +++++ 7 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/BackupStateManager.kt diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 8b2d3676..7cd7966f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -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.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()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupStateManager.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupStateManager.kt new file mode 100644 index 00000000..036a4387 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/BackupStateManager.kt @@ -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 = 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 + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt index ce2e1b58..7e60b6ff 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt @@ -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) + } + } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 58832235..132b3147 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -151,7 +151,6 @@ class SettingsFragment : PreferenceFragmentCompat() { setAppBackupStatusSummary(time) } viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo -> - viewModel.onWorkerStateChanged() setAppBackupSchedulingSummary(workInfo) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index f247e24e..7981fed0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -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 private val mBackupPossible = MutableLiveData(false) val backupPossible: LiveData = 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() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt index 3eba826d..3acd8e77 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 9d81d3e5..581468d6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -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.") }