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