Make demo restore with file selection functional

This injects FileSelectionManager as a singleton, so we can use its selection to recreate a snapshot, even in a service.

Also includes some UI improvements.
This commit is contained in:
Torsten Grote 2024-06-21 10:27:31 -03:00
parent 5012099419
commit 7d6ab6f8e0
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
17 changed files with 139 additions and 54 deletions

View file

@ -57,6 +57,7 @@ class KoinInstrumentationTestApp : App() {
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
fileSelectionManager = get(),
)
)
currentRestoreViewModel!!

View file

@ -28,8 +28,8 @@ import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.plugins.webdav.storagePluginModuleWebDav
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.restore.restoreUiModule
import com.stevesoltys.seedvault.settings.AppListRetriever
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.SettingsViewModel
@ -37,7 +37,6 @@ import com.stevesoltys.seedvault.storage.storageModule
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.backupModule
import com.stevesoltys.seedvault.transport.restore.restoreModule
import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
@ -97,20 +96,6 @@ open class App : Application() {
)
}
viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) }
viewModel {
RestoreViewModel(
app = this@App,
settingsManager = get(),
keyManager = get(),
backupManager = get(),
restoreCoordinator = get(),
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
)
}
viewModel { FileSelectionViewModel(this@App, get()) }
}
override fun onCreate() {
@ -155,6 +140,7 @@ open class App : Application() {
installModule,
storageModule,
workerModule,
restoreUiModule,
appModule
)

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.restore
import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
import org.koin.android.ext.koin.androidApplication
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val restoreUiModule = module {
single { FileSelectionManager() }
viewModel {
RestoreViewModel(
app = androidApplication(),
settingsManager = get(),
keyManager = get(),
backupManager = get(),
restoreCoordinator = get(),
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
fileSelectionManager = get(),
)
}
viewModel { FileSelectionViewModel(androidApplication(), get()) }
}

View file

@ -64,6 +64,7 @@ internal class RestoreViewModel(
private val iconManager: IconManager,
storageBackup: StorageBackup,
pluginManager: StoragePluginManager,
override val fileSelectionManager: FileSelectionManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
RestorableBackupClickListener, SnapshotViewModel {
@ -99,8 +100,6 @@ internal class RestoreViewModel(
get() = appDataRestoreManager.restoreBackupResult
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
override val fileSelectionManager: FileSelectionManager
get() = TODO("Not yet implemented")
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) ->

View file

@ -18,6 +18,7 @@ import org.calyxos.backup.storage.backup.BackupService
import org.calyxos.backup.storage.backup.NotificationBackupObserver
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
import org.calyxos.backup.storage.restore.RestoreService
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
import org.koin.android.ext.android.inject
/*
@ -70,6 +71,7 @@ internal class StorageBackupService : BackupService() {
internal class StorageRestoreService : RestoreService() {
override val storageBackup: StorageBackup by inject()
override val fileSelectionManager: FileSelectionManager by inject()
// use lazy delegate because context isn't available during construction time
override val restoreObserver: RestoreObserver by lazy {

View file

@ -108,10 +108,6 @@
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
tools:listitem="@layout/list_item_app_status" />
<Button
@ -122,9 +118,6 @@
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:text="@string/restore_backup_button"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appList" />
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -12,6 +12,7 @@ import android.util.Log
import de.grobox.storagebackuptester.plugin.TestSafStoragePlugin
import de.grobox.storagebackuptester.settings.SettingsManager
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
class App : Application() {
@ -20,6 +21,7 @@ class App : Application() {
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
StorageBackup(this, { plugin })
}
val fileSelectionManager: FileSelectionManager get() = FileSelectionManager()
override fun onCreate() {
super.onCreate()

View file

@ -14,6 +14,7 @@ import org.calyxos.backup.storage.backup.BackupService
import org.calyxos.backup.storage.backup.NotificationBackupObserver
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
import org.calyxos.backup.storage.restore.RestoreService
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
import java.util.concurrent.TimeUnit.HOURS
// debug with:
@ -45,6 +46,8 @@ class DemoBackupService : BackupService() {
class DemoRestoreService : RestoreService() {
// use lazy delegate because context isn't available during construction time
override val storageBackup: StorageBackup by lazy { (application as App).storageBackup }
override val fileSelectionManager: FileSelectionManager
get() = (application as App).fileSelectionManager
override val restoreObserver: RestoreObserver by lazy {
NotificationRestoreObserver(applicationContext)
}

View file

@ -24,11 +24,11 @@ import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.SnapshotItem
import org.calyxos.backup.storage.api.SnapshotResult
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.api.StoredSnapshot
import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.backup.storage.scanner.DocumentScanner
import org.calyxos.backup.storage.scanner.MediaScanner
import org.calyxos.backup.storage.ui.backup.BackupContentViewModel
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
private val logEmptyState = """
@ -48,7 +48,7 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
private val app: App = application as App
private val settingsManager = app.settingsManager
override val storageBackup: StorageBackup = app.storageBackup
override val fileSelectionManager = FileSelectionManager()
override val fileSelectionManager = app.fileSelectionManager
private val _backupLog = MutableLiveData(BackupProgress(0, 0, logEmptyState))
val backupLog: LiveData<BackupProgress> = _backupLog
@ -64,6 +64,7 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
override val snapshots: LiveData<SnapshotResult>
get() = storageBackup.getBackupSnapshots().asLiveData(Dispatchers.IO)
private var storedSnapshot: StoredSnapshot? = null
init {
viewModelScope.launch { loadContent() }
@ -128,11 +129,12 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
fun onSnapshotClicked(item: SnapshotItem) {
val snapshot = item.snapshot ?: error("${item.storedSnapshot} had null snapshot")
fileSelectionManager.onSnapshotChosen(snapshot)
storedSnapshot = item.storedSnapshot
}
fun onFilesSelected(item: SnapshotItem) {
val snapshot = item.snapshot
check(snapshot != null)
fun onFilesSelected() {
val storedSnapshot = this.storedSnapshot ?: error("No snapshot stored")
val snapshot = fileSelectionManager.getBackupSnapshotAndReset()
// example for how to do restore via foreground service
// app.startForegroundService(Intent(app, DemoRestoreService::class.java).apply {
@ -144,8 +146,9 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
_restoreProgressVisible.value = true
val restoreObserver = RestoreStats(app, _restoreLog)
viewModelScope.launch {
storageBackup.restoreBackupSnapshot(item.storedSnapshot, snapshot, restoreObserver)
storageBackup.restoreBackupSnapshot(storedSnapshot, snapshot, restoreObserver)
_restoreProgressVisible.value = false
this@MainViewModel.storedSnapshot = null
}
}

View file

@ -9,8 +9,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import androidx.fragment.app.activityViewModels
import de.grobox.storagebackuptester.MainViewModel
import de.grobox.storagebackuptester.R
import org.calyxos.backup.storage.ui.restore.FileSelectionFragment
class DemoFileSelectionFragment : FileSelectionFragment() {
@ -23,14 +25,17 @@ class DemoFileSelectionFragment : FileSelectionFragment() {
savedInstanceState: Bundle?,
): View {
val v = super.onCreateView(inflater, container, savedInstanceState)
// val topStub: ViewStub = v.findViewById(R.id.topStub)
// topStub.layoutResource = R.layout.footer_snapshot
// val header = topStub.inflate()
// header.findViewById<Button>(R.id.button).setOnClickListener {
// requireActivity().onBackPressed()
// }
val topStub: ViewStub = v.findViewById(R.id.topStub)
topStub.layoutResource = R.layout.header_file_select
topStub.inflate()
return v
}
override fun onRestoreButtonClicked() {
viewModel.onFilesSelected()
parentFragmentManager.beginTransaction()
.replace(R.id.container, RestoreFragment.newInstance())
.commit()
}
}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2021 The Calyx Institute
SPDX-License-Identifier: Apache-2.0
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="De-select folders you don't want to restore. Tap folders to see their contents."
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -200,7 +200,7 @@ public class StorageBackup(
public suspend fun restoreBackupSnapshot(
storedSnapshot: StoredSnapshot,
snapshot: BackupSnapshot? = null,
snapshot: BackupSnapshot,
restoreObserver: RestoreObserver? = null,
): Boolean = withContext(dispatcher) {
if (restoreRunning.getAndSet(true)) {

View file

@ -22,7 +22,6 @@ import org.calyxos.backup.storage.plugin.SnapshotRetriever
import java.io.IOException
import java.io.InputStream
import java.security.GeneralSecurityException
import kotlin.time.ExperimentalTime
private const val TAG = "Restore"
@ -99,15 +98,12 @@ internal class Restore(
Log.e(TAG, "Decrypting and parsing $numSnapshots snapshots took $time")
}
@OptIn(ExperimentalTime::class)
@Throws(IOException::class, GeneralSecurityException::class)
suspend fun restoreBackupSnapshot(
storedSnapshot: StoredSnapshot,
optionalSnapshot: BackupSnapshot? = null,
snapshot: BackupSnapshot,
observer: RestoreObserver? = null,
) {
val snapshot = optionalSnapshot ?: snapshotRetriever.getSnapshot(streamKey, storedSnapshot)
val filesTotal = snapshot.mediaFilesList.size + snapshot.documentFilesList.size
val totalSize =
snapshot.mediaFilesList.sumOf { it.size } + snapshot.documentFilesList.sumOf { it.size }

View file

@ -9,8 +9,10 @@ import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.api.StoredSnapshot
@ -18,6 +20,7 @@ import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTA
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
import org.calyxos.backup.storage.ui.NOTIFICATION_ID_RESTORE
import org.calyxos.backup.storage.ui.Notifications
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
/**
* Start to trigger restore as a foreground service. Ensure that you provide the snapshot
@ -36,6 +39,7 @@ public abstract class RestoreService : Service() {
private val n by lazy { Notifications(applicationContext) }
protected abstract val storageBackup: StorageBackup
protected abstract val fileSelectionManager: FileSelectionManager
protected abstract val restoreObserver: RestoreObserver?
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -47,8 +51,11 @@ public abstract class RestoreService : Service() {
startForeground(NOTIFICATION_ID_RESTORE, n.getRestoreNotification())
GlobalScope.launch {
val snapshot = withContext(Dispatchers.Main) {
fileSelectionManager.getBackupSnapshotAndReset()
}
// TODO offer a way to try again if failed, or do an automatic retry here
storageBackup.restoreBackupSnapshot(storedSnapshot, null, restoreObserver)
storageBackup.restoreBackupSnapshot(storedSnapshot, snapshot, restoreObserver)
stopSelf(startId)
}
return START_STICKY_COMPATIBILITY

View file

@ -33,6 +33,9 @@ public abstract class FileSelectionFragment : Fragment() {
val v = inflater.inflate(R.layout.fragment_select_files, container, false)
list = v.findViewById(R.id.list)
v.findViewById<View>(R.id.fab).setOnClickListener {
onRestoreButtonClicked()
}
return v
}
@ -51,8 +54,10 @@ public abstract class FileSelectionFragment : Fragment() {
}
}
protected abstract fun onRestoreButtonClicked()
@CallSuper
public open fun onFileItemsChanged(filesItems: List<FilesItem>) {
protected open fun onFileItemsChanged(filesItems: List<FilesItem>) {
adapter.submitList(filesItems)
}
}

View file

@ -54,6 +54,7 @@ public class FileSelectionManager {
private val allFolders = HashMap<String, FolderItem>()
private val allFiles = HashMap<String, MutableList<FileItem>>()
private var snapshot: BackupSnapshot? = null
private var expandedFolder: String? = null
private val mFiles = MutableStateFlow<List<FilesItem>>(emptyList())
@ -61,6 +62,12 @@ public class FileSelectionManager {
@UiThread
public fun onSnapshotChosen(snapshot: BackupSnapshot) {
// clear previous state if existing
clearState()
// store snapshot for later
this.snapshot = snapshot
// cache files from snapshot within [RestorableFile] (for easier processing)
snapshot.mediaFilesList.forEach { mediaFile ->
cacheFileItem(RestorableFile(mediaFile))
}
@ -126,6 +133,28 @@ public class FileSelectionManager {
mFiles.value = rebuildListFromCache()
}
@UiThread
public fun getBackupSnapshotAndReset(): BackupSnapshot {
val snapshot = this.snapshot ?: error("No snapshot stored")
// clear previous media files from snapshot
val snapshotBuilder = snapshot.toBuilder()
.clearMediaFiles()
.clearDocumentFiles()
// add only selected files back to snapshot
allFiles.values.forEach { fileList ->
fileList.forEach { file ->
if (file.selected && file.file.mediaFile != null) {
snapshotBuilder.addMediaFiles(file.file.mediaFile)
} else if (file.selected && file.file.docFile != null) {
snapshotBuilder.addDocumentFiles(file.file.docFile)
}
}
}
// clear state to free up memory
clearState()
return snapshotBuilder.build()
}
private fun cacheFileItem(restorableFile: RestorableFile) {
val fileItem = FileItem(restorableFile, 0, true)
allFiles.getOrPut(restorableFile.dir) {
@ -215,4 +244,12 @@ public class FileSelectionManager {
)
}
private fun clearState() {
snapshot = null
expandedFolder = null
allFolders.clear()
allFiles.clear()
mFiles.value = emptyList()
}
}

View file

@ -16,7 +16,7 @@
<ViewStub
android:id="@+id/topStub"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/topStub"
app:layout_constraintEnd_toEndOf="parent"
@ -34,10 +34,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/bottomStub"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topStub"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_file" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -51,8 +48,6 @@
app:backgroundTint="?colorAccent"
app:icon="@drawable/ic_cloud_restore"
app:iconTint="#ffffff"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>