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:
parent
5012099419
commit
7d6ab6f8e0
17 changed files with 139 additions and 54 deletions
|
@ -57,6 +57,7 @@ class KoinInstrumentationTestApp : App() {
|
||||||
iconManager = get(),
|
iconManager = get(),
|
||||||
storageBackup = get(),
|
storageBackup = get(),
|
||||||
pluginManager = get(),
|
pluginManager = get(),
|
||||||
|
fileSelectionManager = get(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
currentRestoreViewModel!!
|
currentRestoreViewModel!!
|
||||||
|
|
|
@ -28,8 +28,8 @@ import com.stevesoltys.seedvault.metadata.metadataModule
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
|
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
|
||||||
import com.stevesoltys.seedvault.plugins.webdav.storagePluginModuleWebDav
|
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.install.installModule
|
||||||
|
import com.stevesoltys.seedvault.restore.restoreUiModule
|
||||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
import com.stevesoltys.seedvault.settings.AppListRetriever
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
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.TRANSPORT_ID
|
||||||
import com.stevesoltys.seedvault.transport.backup.backupModule
|
import com.stevesoltys.seedvault.transport.backup.backupModule
|
||||||
import com.stevesoltys.seedvault.transport.restore.restoreModule
|
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.notification.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
|
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
||||||
|
@ -97,20 +96,6 @@ open class App : Application() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) }
|
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() {
|
override fun onCreate() {
|
||||||
|
@ -155,6 +140,7 @@ open class App : Application() {
|
||||||
installModule,
|
installModule,
|
||||||
storageModule,
|
storageModule,
|
||||||
workerModule,
|
workerModule,
|
||||||
|
restoreUiModule,
|
||||||
appModule
|
appModule
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()) }
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ internal class RestoreViewModel(
|
||||||
private val iconManager: IconManager,
|
private val iconManager: IconManager,
|
||||||
storageBackup: StorageBackup,
|
storageBackup: StorageBackup,
|
||||||
pluginManager: StoragePluginManager,
|
pluginManager: StoragePluginManager,
|
||||||
|
override val fileSelectionManager: FileSelectionManager,
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
|
||||||
RestorableBackupClickListener, SnapshotViewModel {
|
RestorableBackupClickListener, SnapshotViewModel {
|
||||||
|
@ -99,8 +100,6 @@ internal class RestoreViewModel(
|
||||||
get() = appDataRestoreManager.restoreBackupResult
|
get() = appDataRestoreManager.restoreBackupResult
|
||||||
|
|
||||||
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
|
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
|
||||||
override val fileSelectionManager: FileSelectionManager
|
|
||||||
get() = TODO("Not yet implemented")
|
|
||||||
|
|
||||||
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
|
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
|
||||||
val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) ->
|
val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) ->
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.calyxos.backup.storage.backup.BackupService
|
||||||
import org.calyxos.backup.storage.backup.NotificationBackupObserver
|
import org.calyxos.backup.storage.backup.NotificationBackupObserver
|
||||||
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
|
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
|
||||||
import org.calyxos.backup.storage.restore.RestoreService
|
import org.calyxos.backup.storage.restore.RestoreService
|
||||||
|
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -70,6 +71,7 @@ internal class StorageBackupService : BackupService() {
|
||||||
|
|
||||||
internal class StorageRestoreService : RestoreService() {
|
internal class StorageRestoreService : RestoreService() {
|
||||||
override val storageBackup: StorageBackup by inject()
|
override val storageBackup: StorageBackup by inject()
|
||||||
|
override val fileSelectionManager: FileSelectionManager by inject()
|
||||||
|
|
||||||
// use lazy delegate because context isn't available during construction time
|
// use lazy delegate because context isn't available during construction time
|
||||||
override val restoreObserver: RestoreObserver by lazy {
|
override val restoreObserver: RestoreObserver by lazy {
|
||||||
|
|
|
@ -108,10 +108,6 @@
|
||||||
android:layout_marginTop="0dp"
|
android:layout_marginTop="0dp"
|
||||||
android:layout_marginEnd="0dp"
|
android:layout_marginEnd="0dp"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
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" />
|
tools:listitem="@layout/list_item_app_status" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -122,9 +118,6 @@
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:text="@string/restore_backup_button"
|
android:text="@string/restore_backup_button"
|
||||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
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" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.util.Log
|
||||||
import de.grobox.storagebackuptester.plugin.TestSafStoragePlugin
|
import de.grobox.storagebackuptester.plugin.TestSafStoragePlugin
|
||||||
import de.grobox.storagebackuptester.settings.SettingsManager
|
import de.grobox.storagebackuptester.settings.SettingsManager
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
|
||||||
|
|
||||||
class App : Application() {
|
class App : Application() {
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ class App : Application() {
|
||||||
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
|
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
|
||||||
StorageBackup(this, { plugin })
|
StorageBackup(this, { plugin })
|
||||||
}
|
}
|
||||||
|
val fileSelectionManager: FileSelectionManager get() = FileSelectionManager()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.calyxos.backup.storage.backup.BackupService
|
||||||
import org.calyxos.backup.storage.backup.NotificationBackupObserver
|
import org.calyxos.backup.storage.backup.NotificationBackupObserver
|
||||||
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
|
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
|
||||||
import org.calyxos.backup.storage.restore.RestoreService
|
import org.calyxos.backup.storage.restore.RestoreService
|
||||||
|
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
|
||||||
import java.util.concurrent.TimeUnit.HOURS
|
import java.util.concurrent.TimeUnit.HOURS
|
||||||
|
|
||||||
// debug with:
|
// debug with:
|
||||||
|
@ -45,6 +46,8 @@ class DemoBackupService : BackupService() {
|
||||||
class DemoRestoreService : RestoreService() {
|
class DemoRestoreService : RestoreService() {
|
||||||
// use lazy delegate because context isn't available during construction time
|
// use lazy delegate because context isn't available during construction time
|
||||||
override val storageBackup: StorageBackup by lazy { (application as App).storageBackup }
|
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 {
|
override val restoreObserver: RestoreObserver by lazy {
|
||||||
NotificationRestoreObserver(applicationContext)
|
NotificationRestoreObserver(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,11 @@ import kotlinx.coroutines.withContext
|
||||||
import org.calyxos.backup.storage.api.SnapshotItem
|
import org.calyxos.backup.storage.api.SnapshotItem
|
||||||
import org.calyxos.backup.storage.api.SnapshotResult
|
import org.calyxos.backup.storage.api.SnapshotResult
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
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.backup.BackupJobService
|
||||||
import org.calyxos.backup.storage.scanner.DocumentScanner
|
import org.calyxos.backup.storage.scanner.DocumentScanner
|
||||||
import org.calyxos.backup.storage.scanner.MediaScanner
|
import org.calyxos.backup.storage.scanner.MediaScanner
|
||||||
import org.calyxos.backup.storage.ui.backup.BackupContentViewModel
|
import org.calyxos.backup.storage.ui.backup.BackupContentViewModel
|
||||||
import org.calyxos.backup.storage.ui.restore.FileSelectionManager
|
|
||||||
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||||
|
|
||||||
private val logEmptyState = """
|
private val logEmptyState = """
|
||||||
|
@ -48,7 +48,7 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
|
||||||
private val app: App = application as App
|
private val app: App = application as App
|
||||||
private val settingsManager = app.settingsManager
|
private val settingsManager = app.settingsManager
|
||||||
override val storageBackup: StorageBackup = app.storageBackup
|
override val storageBackup: StorageBackup = app.storageBackup
|
||||||
override val fileSelectionManager = FileSelectionManager()
|
override val fileSelectionManager = app.fileSelectionManager
|
||||||
|
|
||||||
private val _backupLog = MutableLiveData(BackupProgress(0, 0, logEmptyState))
|
private val _backupLog = MutableLiveData(BackupProgress(0, 0, logEmptyState))
|
||||||
val backupLog: LiveData<BackupProgress> = _backupLog
|
val backupLog: LiveData<BackupProgress> = _backupLog
|
||||||
|
@ -64,6 +64,7 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
|
||||||
|
|
||||||
override val snapshots: LiveData<SnapshotResult>
|
override val snapshots: LiveData<SnapshotResult>
|
||||||
get() = storageBackup.getBackupSnapshots().asLiveData(Dispatchers.IO)
|
get() = storageBackup.getBackupSnapshots().asLiveData(Dispatchers.IO)
|
||||||
|
private var storedSnapshot: StoredSnapshot? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch { loadContent() }
|
viewModelScope.launch { loadContent() }
|
||||||
|
@ -128,11 +129,12 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
|
||||||
fun onSnapshotClicked(item: SnapshotItem) {
|
fun onSnapshotClicked(item: SnapshotItem) {
|
||||||
val snapshot = item.snapshot ?: error("${item.storedSnapshot} had null snapshot")
|
val snapshot = item.snapshot ?: error("${item.storedSnapshot} had null snapshot")
|
||||||
fileSelectionManager.onSnapshotChosen(snapshot)
|
fileSelectionManager.onSnapshotChosen(snapshot)
|
||||||
|
storedSnapshot = item.storedSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFilesSelected(item: SnapshotItem) {
|
fun onFilesSelected() {
|
||||||
val snapshot = item.snapshot
|
val storedSnapshot = this.storedSnapshot ?: error("No snapshot stored")
|
||||||
check(snapshot != null)
|
val snapshot = fileSelectionManager.getBackupSnapshotAndReset()
|
||||||
|
|
||||||
// example for how to do restore via foreground service
|
// example for how to do restore via foreground service
|
||||||
// app.startForegroundService(Intent(app, DemoRestoreService::class.java).apply {
|
// app.startForegroundService(Intent(app, DemoRestoreService::class.java).apply {
|
||||||
|
@ -144,8 +146,9 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
|
||||||
_restoreProgressVisible.value = true
|
_restoreProgressVisible.value = true
|
||||||
val restoreObserver = RestoreStats(app, _restoreLog)
|
val restoreObserver = RestoreStats(app, _restoreLog)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
storageBackup.restoreBackupSnapshot(item.storedSnapshot, snapshot, restoreObserver)
|
storageBackup.restoreBackupSnapshot(storedSnapshot, snapshot, restoreObserver)
|
||||||
_restoreProgressVisible.value = false
|
_restoreProgressVisible.value = false
|
||||||
|
this@MainViewModel.storedSnapshot = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,10 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewStub
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import de.grobox.storagebackuptester.MainViewModel
|
import de.grobox.storagebackuptester.MainViewModel
|
||||||
|
import de.grobox.storagebackuptester.R
|
||||||
import org.calyxos.backup.storage.ui.restore.FileSelectionFragment
|
import org.calyxos.backup.storage.ui.restore.FileSelectionFragment
|
||||||
|
|
||||||
class DemoFileSelectionFragment : FileSelectionFragment() {
|
class DemoFileSelectionFragment : FileSelectionFragment() {
|
||||||
|
@ -23,14 +25,17 @@ class DemoFileSelectionFragment : FileSelectionFragment() {
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View {
|
): View {
|
||||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||||
// val topStub: ViewStub = v.findViewById(R.id.topStub)
|
val topStub: ViewStub = v.findViewById(R.id.topStub)
|
||||||
// topStub.layoutResource = R.layout.footer_snapshot
|
topStub.layoutResource = R.layout.header_file_select
|
||||||
// val header = topStub.inflate()
|
topStub.inflate()
|
||||||
// header.findViewById<Button>(R.id.button).setOnClickListener {
|
|
||||||
// requireActivity().onBackPressed()
|
|
||||||
// }
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRestoreButtonClicked() {
|
||||||
|
viewModel.onFilesSelected()
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.container, RestoreFragment.newInstance())
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
20
storage/demo/src/main/res/layout/header_file_select.xml
Normal file
20
storage/demo/src/main/res/layout/header_file_select.xml
Normal 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>
|
|
@ -200,7 +200,7 @@ public class StorageBackup(
|
||||||
|
|
||||||
public suspend fun restoreBackupSnapshot(
|
public suspend fun restoreBackupSnapshot(
|
||||||
storedSnapshot: StoredSnapshot,
|
storedSnapshot: StoredSnapshot,
|
||||||
snapshot: BackupSnapshot? = null,
|
snapshot: BackupSnapshot,
|
||||||
restoreObserver: RestoreObserver? = null,
|
restoreObserver: RestoreObserver? = null,
|
||||||
): Boolean = withContext(dispatcher) {
|
): Boolean = withContext(dispatcher) {
|
||||||
if (restoreRunning.getAndSet(true)) {
|
if (restoreRunning.getAndSet(true)) {
|
||||||
|
|
|
@ -22,7 +22,6 @@ import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
private const val TAG = "Restore"
|
private const val TAG = "Restore"
|
||||||
|
|
||||||
|
@ -99,15 +98,12 @@ internal class Restore(
|
||||||
Log.e(TAG, "Decrypting and parsing $numSnapshots snapshots took $time")
|
Log.e(TAG, "Decrypting and parsing $numSnapshots snapshots took $time")
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
@Throws(IOException::class, GeneralSecurityException::class)
|
@Throws(IOException::class, GeneralSecurityException::class)
|
||||||
suspend fun restoreBackupSnapshot(
|
suspend fun restoreBackupSnapshot(
|
||||||
storedSnapshot: StoredSnapshot,
|
storedSnapshot: StoredSnapshot,
|
||||||
optionalSnapshot: BackupSnapshot? = null,
|
snapshot: BackupSnapshot,
|
||||||
observer: RestoreObserver? = null,
|
observer: RestoreObserver? = null,
|
||||||
) {
|
) {
|
||||||
val snapshot = optionalSnapshot ?: snapshotRetriever.getSnapshot(streamKey, storedSnapshot)
|
|
||||||
|
|
||||||
val filesTotal = snapshot.mediaFilesList.size + snapshot.documentFilesList.size
|
val filesTotal = snapshot.mediaFilesList.size + snapshot.documentFilesList.size
|
||||||
val totalSize =
|
val totalSize =
|
||||||
snapshot.mediaFilesList.sumOf { it.size } + snapshot.documentFilesList.sumOf { it.size }
|
snapshot.mediaFilesList.sumOf { it.size } + snapshot.documentFilesList.sumOf { it.size }
|
||||||
|
|
|
@ -9,8 +9,10 @@ import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.calyxos.backup.storage.api.RestoreObserver
|
import org.calyxos.backup.storage.api.RestoreObserver
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
import org.calyxos.backup.storage.api.StoredSnapshot
|
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.restore.RestoreService.Companion.EXTRA_USER_ID
|
||||||
import org.calyxos.backup.storage.ui.NOTIFICATION_ID_RESTORE
|
import org.calyxos.backup.storage.ui.NOTIFICATION_ID_RESTORE
|
||||||
import org.calyxos.backup.storage.ui.Notifications
|
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
|
* 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) }
|
private val n by lazy { Notifications(applicationContext) }
|
||||||
protected abstract val storageBackup: StorageBackup
|
protected abstract val storageBackup: StorageBackup
|
||||||
|
protected abstract val fileSelectionManager: FileSelectionManager
|
||||||
protected abstract val restoreObserver: RestoreObserver?
|
protected abstract val restoreObserver: RestoreObserver?
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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())
|
startForeground(NOTIFICATION_ID_RESTORE, n.getRestoreNotification())
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
|
val snapshot = withContext(Dispatchers.Main) {
|
||||||
|
fileSelectionManager.getBackupSnapshotAndReset()
|
||||||
|
}
|
||||||
// TODO offer a way to try again if failed, or do an automatic retry here
|
// 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)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
return START_STICKY_COMPATIBILITY
|
return START_STICKY_COMPATIBILITY
|
||||||
|
|
|
@ -33,6 +33,9 @@ public abstract class FileSelectionFragment : Fragment() {
|
||||||
|
|
||||||
val v = inflater.inflate(R.layout.fragment_select_files, container, false)
|
val v = inflater.inflate(R.layout.fragment_select_files, container, false)
|
||||||
list = v.findViewById(R.id.list)
|
list = v.findViewById(R.id.list)
|
||||||
|
v.findViewById<View>(R.id.fab).setOnClickListener {
|
||||||
|
onRestoreButtonClicked()
|
||||||
|
}
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
@ -51,8 +54,10 @@ public abstract class FileSelectionFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract fun onRestoreButtonClicked()
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
public open fun onFileItemsChanged(filesItems: List<FilesItem>) {
|
protected open fun onFileItemsChanged(filesItems: List<FilesItem>) {
|
||||||
adapter.submitList(filesItems)
|
adapter.submitList(filesItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ public class FileSelectionManager {
|
||||||
|
|
||||||
private val allFolders = HashMap<String, FolderItem>()
|
private val allFolders = HashMap<String, FolderItem>()
|
||||||
private val allFiles = HashMap<String, MutableList<FileItem>>()
|
private val allFiles = HashMap<String, MutableList<FileItem>>()
|
||||||
|
private var snapshot: BackupSnapshot? = null
|
||||||
private var expandedFolder: String? = null
|
private var expandedFolder: String? = null
|
||||||
|
|
||||||
private val mFiles = MutableStateFlow<List<FilesItem>>(emptyList())
|
private val mFiles = MutableStateFlow<List<FilesItem>>(emptyList())
|
||||||
|
@ -61,6 +62,12 @@ public class FileSelectionManager {
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
public fun onSnapshotChosen(snapshot: BackupSnapshot) {
|
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 ->
|
snapshot.mediaFilesList.forEach { mediaFile ->
|
||||||
cacheFileItem(RestorableFile(mediaFile))
|
cacheFileItem(RestorableFile(mediaFile))
|
||||||
}
|
}
|
||||||
|
@ -126,6 +133,28 @@ public class FileSelectionManager {
|
||||||
mFiles.value = rebuildListFromCache()
|
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) {
|
private fun cacheFileItem(restorableFile: RestorableFile) {
|
||||||
val fileItem = FileItem(restorableFile, 0, true)
|
val fileItem = FileItem(restorableFile, 0, true)
|
||||||
allFiles.getOrPut(restorableFile.dir) {
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/topStub"
|
android:id="@+id/topStub"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inflatedId="@+id/topStub"
|
android:inflatedId="@+id/topStub"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
@ -34,10 +34,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/bottomStub"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/topStub"
|
|
||||||
tools:listitem="@layout/item_file" />
|
tools:listitem="@layout/item_file" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
@ -51,8 +48,6 @@
|
||||||
app:backgroundTint="?colorAccent"
|
app:backgroundTint="?colorAccent"
|
||||||
app:icon="@drawable/ic_cloud_restore"
|
app:icon="@drawable/ic_cloud_restore"
|
||||||
app:iconTint="#ffffff"
|
app:iconTint="#ffffff"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
Loading…
Reference in a new issue