diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt index 6a2e56013..c00438f20 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt @@ -47,7 +47,7 @@ class KoinInstrumentationTestApp : App() { viewModel { currentBackupStorageViewModel = - spyk(BackupStorageViewModel(context, get(), get(), get())) + spyk(BackupStorageViewModel(context, get(), get(), get(), get())) currentBackupStorageViewModel!! } diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index a28ac628c..9391a801e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -59,7 +59,7 @@ open class App : Application() { viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } - viewModel { BackupStorageViewModel(this@App, get(), get(), get()) } + viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { FileSelectionViewModel(this@App, get()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt index 0dbc6c50e..4829a8d16 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt @@ -37,7 +37,8 @@ internal class DocumentsProviderStoragePlugin( storage.reset(token) // get or create root backup dir - storage.rootBackupDir ?: throw IOException() + // TODO do we need to do this? +// storage.rootBackupDir ?: throw IOException() } @Throws(IOException::class) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt new file mode 100644 index 000000000..abc9bbc81 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.transport.backup + +import android.app.backup.BackupProgress +import android.app.backup.IBackupManager +import android.app.backup.IBackupObserver +import android.os.UserHandle +import android.util.Log +import androidx.annotation.WorkerThread +import com.stevesoltys.seedvault.BackupMonitor +import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER +import com.stevesoltys.seedvault.transport.TRANSPORT_ID + +class BackupInitializer( + private val backupManager: IBackupManager, +) { + + companion object { + private val TAG = BackupInitializer::class.simpleName + } + + fun initialize(onError: () -> Unit, onSuccess: () -> Unit) { + val observer = BackupObserver("Initialization", onError) { + // After successful initialization, we request a @pm@ backup right away, + // because if this finds empty state, it asks us to do another initialization. + // And then we end up with yet another restore set token. + // Since we want the final token as soon as possible, we need to get it here. + backupManager.requestBackup( + arrayOf(MAGIC_PACKAGE_MANAGER), + BackupObserver("Initial backup of @pm@", onError, onSuccess), + BackupMonitor(), + 0, + ) + } + backupManager.initializeTransportsForUser( + UserHandle.myUserId(), + arrayOf(TRANSPORT_ID), + observer, + ) + } + + @WorkerThread + private inner class BackupObserver( + private val operation: String, + private val onError: () -> Unit, + private val onSuccess: () -> Unit, + ) : IBackupObserver.Stub() { + override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) { + // noop + } + + override fun onResult(target: String, status: Int) { + // noop + } + + override fun backupFinished(status: Int) { + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i(TAG, "$operation finished. Status: $status") + } + if (status == 0) { + onSuccess() + } else { + onError() + } + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt index 3ed9caedd..6bb6f6e2c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt @@ -4,6 +4,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val backupModule = module { + single { BackupInitializer(get()) } single { InputFactory() } single { PackageService( diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt index ba126ab1a..274187c3b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt @@ -1,7 +1,6 @@ package com.stevesoltys.seedvault.ui.recoverycode import android.app.backup.IBackupManager -import android.os.UserHandle import android.util.Log import androidx.lifecycle.AndroidViewModel import cash.z.ecc.android.bip39.Mnemonics @@ -12,8 +11,7 @@ import cash.z.ecc.android.bip39.toSeed import com.stevesoltys.seedvault.App import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.KeyManager -import com.stevesoltys.seedvault.transport.TRANSPORT_ID -import com.stevesoltys.seedvault.transport.backup.BackupCoordinator +import com.stevesoltys.seedvault.transport.backup.BackupInitializer import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager @@ -32,7 +30,7 @@ internal class RecoveryCodeViewModel( private val crypto: Crypto, private val keyManager: KeyManager, private val backupManager: IBackupManager, - private val backupCoordinator: BackupCoordinator, + private val backupInitializer: BackupInitializer, private val notificationManager: BackupNotificationManager, private val storageBackup: StorageBackup, ) : AndroidViewModel(app) { @@ -109,11 +107,9 @@ internal class RecoveryCodeViewModel( storageBackup.clearCache() try { // initialize the new location - if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser( - UserHandle.myUserId(), - arrayOf(TRANSPORT_ID), - null - ) + if (backupManager.isBackupEnabled) backupInitializer.initialize({ }) { + // no-op + } } catch (e: IOException) { Log.e(TAG, "Error starting new RestoreSet", e) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 7a3a06a6f..746d93ef9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -1,20 +1,16 @@ package com.stevesoltys.seedvault.ui.storage import android.app.Application -import android.app.backup.BackupProgress import android.app.backup.IBackupManager -import android.app.backup.IBackupObserver import android.app.job.JobInfo import android.net.Uri -import android.os.UserHandle import android.util.Log -import androidx.annotation.WorkerThread import androidx.lifecycle.viewModelScope import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageBackupJobService -import com.stevesoltys.seedvault.transport.TRANSPORT_ID +import com.stevesoltys.seedvault.transport.backup.BackupInitializer import com.stevesoltys.seedvault.worker.AppBackupWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,6 +24,7 @@ private val TAG = BackupStorageViewModel::class.java.simpleName internal class BackupStorageViewModel( private val app: Application, private val backupManager: IBackupManager, + private val backupInitializer: BackupInitializer, private val storageBackup: StorageBackup, settingsManager: SettingsManager, ) : StorageViewModel(app, settingsManager) { @@ -52,13 +49,22 @@ internal class BackupStorageViewModel( storageBackup.clearCache() try { // initialize the new location (if backups are enabled) - if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser( - UserHandle.myUserId(), - arrayOf(TRANSPORT_ID), - // if storage is on USB and this is not SetupWizard, do a backup right away - InitializationObserver(isUsb && !isSetupWizard) - ) else { - InitializationObserver(false).backupFinished(0) + if (backupManager.isBackupEnabled) { + val onError = { + Log.e(TAG, "Error starting new RestoreSet") + onInitializationError() + } + backupInitializer.initialize(onError) { + val requestBackup = isUsb && !isSetupWizard + if (requestBackup) { + AppBackupWorker.scheduleNow(app, reschedule = false) + } + // notify the UI that the location has been set + mLocationChecked.postEvent(LocationResult()) + } + } else { + // notify the UI that the location has been set + mLocationChecked.postEvent(LocationResult()) } } catch (e: IOException) { Log.e(TAG, "Error starting new RestoreSet", e) @@ -90,35 +96,6 @@ internal class BackupStorageViewModel( BackupJobService.cancelJob(app) } - @WorkerThread - private inner class InitializationObserver(val requestBackup: Boolean) : - IBackupObserver.Stub() { - override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) { - // noop - } - - override fun onResult(target: String, status: Int) { - // noop - } - - override fun backupFinished(status: Int) { - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i(TAG, "Initialization finished. Status: $status") - } - if (status == 0) { - // notify the UI that the location has been set - mLocationChecked.postEvent(LocationResult()) - if (requestBackup) { - val isUsb = settingsManager.getStorage()?.isUsb ?: false - AppBackupWorker.scheduleNow(app, reschedule = !isUsb) - } - } else { - // notify the UI that the location was invalid - onInitializationError() - } - } - } - private fun onInitializationError() { val errorMsg = app.getString(R.string.storage_check_fragment_backup_error) mLocationChecked.postEvent(LocationResult(errorMsg))