diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/FilesSelectionFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/FilesSelectionFragment.kt new file mode 100644 index 00000000..656be0a9 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/FilesSelectionFragment.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.restore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewStub +import android.widget.Button +import com.stevesoltys.seedvault.R +import org.calyxos.backup.storage.ui.restore.FileSelectionFragment +import org.calyxos.backup.storage.ui.restore.FilesItem +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +internal class FilesSelectionFragment : FileSelectionFragment() { + + override val viewModel: RestoreViewModel by sharedViewModel() + private lateinit var button: Button + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val v = super.onCreateView(inflater, container, savedInstanceState) + val topStub: ViewStub = v.requireViewById(R.id.topStub) + topStub.layoutResource = R.layout.header_files_selection + topStub.inflate() + val bottomStub: ViewStub = v.requireViewById(R.id.bottomStub) + bottomStub.layoutResource = R.layout.footer_files_selection + button = bottomStub.inflate() as Button + button.setOnClickListener { + viewModel.startFilesRestore() + } + return v + } + + override fun onFileItemsChanged(filesItems: List) { + slideUpInRootView(button) + } +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt index 5b5c30bd..8f99db58 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt @@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED +import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_SELECT_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.SELECT_APPS import com.stevesoltys.seedvault.restore.install.InstallProgressFragment import com.stevesoltys.seedvault.ui.RequireProvisioningActivity @@ -35,7 +36,12 @@ class RestoreActivity : RequireProvisioningActivity() { RESTORE_APPS -> showFragment(InstallProgressFragment()) RESTORE_BACKUP -> showFragment(RestoreProgressFragment()) RESTORE_FILES -> showFragment(RestoreFilesFragment()) - RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment()) + RESTORE_SELECT_FILES -> showFragment(FilesSelectionFragment(), true) + RESTORE_FILES_STARTED -> { + // pop back stack, so back navigation doesn't bring us to RESTORE_SELECT_FILES + supportFragmentManager.popBackStackImmediate() + showFragment(RestoreFilesStartedFragment()) + } else -> throw AssertionError() } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt index 281d508e..e3e9187b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt @@ -47,7 +47,7 @@ internal class RestoreFilesFragment : SnapshotFragment() { } override fun onSnapshotClicked(item: SnapshotItem) { - viewModel.startFilesRestore(item) + viewModel.selectFilesForRestore(item) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index 8e7989f5..22af1365 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -23,6 +23,7 @@ import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED +import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_SELECT_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.SELECT_APPS import com.stevesoltys.seedvault.restore.install.ApkRestore import com.stevesoltys.seedvault.restore.install.InstallIntentCreator @@ -44,6 +45,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.calyxos.backup.storage.api.SnapshotItem import org.calyxos.backup.storage.api.StorageBackup +import org.calyxos.backup.storage.api.StoredSnapshot import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTAMP_START import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID import org.calyxos.backup.storage.ui.restore.FileSelectionManager @@ -100,6 +102,7 @@ internal class RestoreViewModel( get() = appDataRestoreManager.restoreBackupResult override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher) + private var storedSnapshot: StoredSnapshot? = null internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) { val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) -> @@ -181,12 +184,22 @@ internal class RestoreViewModel( } @UiThread - internal fun startFilesRestore(item: SnapshotItem) { + internal fun selectFilesForRestore(item: SnapshotItem) { + val snapshot = item.snapshot ?: error("${item.storedSnapshot} had null snapshot") + fileSelectionManager.onSnapshotChosen(snapshot) + storedSnapshot = item.storedSnapshot + mDisplayFragment.setEvent(RESTORE_SELECT_FILES) + } + + @UiThread + internal fun startFilesRestore() { + val storedSnapshot = this.storedSnapshot ?: error("No snapshot stored") val i = Intent(app, StorageRestoreService::class.java) - i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId) - i.putExtra(EXTRA_TIMESTAMP_START, item.time) + i.putExtra(EXTRA_USER_ID, storedSnapshot.userId) + i.putExtra(EXTRA_TIMESTAMP_START, storedSnapshot.timestamp) app.startForegroundService(i) mDisplayFragment.setEvent(RESTORE_FILES_STARTED) + this.storedSnapshot = null } } @@ -208,5 +221,10 @@ internal class RestoreBackupResult(val errorMsg: String? = null) { } internal enum class DisplayFragment { - SELECT_APPS, RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED + SELECT_APPS, + RESTORE_APPS, + RESTORE_BACKUP, + RESTORE_FILES, + RESTORE_SELECT_FILES, + RESTORE_FILES_STARTED, } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt index 02184211..72114e87 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt @@ -24,8 +24,7 @@ abstract class BackupActivity : AppCompatActivity() { protected fun showFragment(f: Fragment, addToBackStack: Boolean = false, tag: String? = null) { supportFragmentManager.beginTransaction().apply { - if (tag == null) replace(R.id.fragment, f) - else replace(R.id.fragment, f, tag) + replace(R.id.fragment, f, tag) if (addToBackStack) addToBackStack(null) commit() } diff --git a/app/src/main/res/layout/footer_files_selection.xml b/app/src/main/res/layout/footer_files_selection.xml new file mode 100644 index 00000000..00891b8b --- /dev/null +++ b/app/src/main/res/layout/footer_files_selection.xml @@ -0,0 +1,17 @@ + + diff --git a/app/src/main/res/layout/header_files_selection.xml b/app/src/main/res/layout/header_files_selection.xml new file mode 100644 index 00000000..615c6f2e --- /dev/null +++ b/app/src/main/res/layout/header_files_selection.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5936dc2e..1e2d5bf3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -234,6 +234,7 @@ Skip restoring files Choose a storage backup to restore (beta) + Selected files will be restored. Tap folders to see files in them. Files are being restored… Your files are being restored in the background. You can start using your phone while this is running.\n\nSome apps (e.g. Signal or WhatsApp) might require files to be fully restored to import a backup. Try to avoid starting those apps before file restore is complete. Got it diff --git a/storage/demo/src/main/java/de/grobox/storagebackuptester/restore/DemoFileSelectionFragment.kt b/storage/demo/src/main/java/de/grobox/storagebackuptester/restore/DemoFileSelectionFragment.kt index b55791e4..362e22e1 100644 --- a/storage/demo/src/main/java/de/grobox/storagebackuptester/restore/DemoFileSelectionFragment.kt +++ b/storage/demo/src/main/java/de/grobox/storagebackuptester/restore/DemoFileSelectionFragment.kt @@ -11,13 +11,16 @@ import android.view.View import android.view.ViewGroup import android.view.ViewStub import androidx.fragment.app.activityViewModels +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton 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.FilesItem class DemoFileSelectionFragment : FileSelectionFragment() { override val viewModel: MainViewModel by activityViewModels() + private var fab: ExtendedFloatingActionButton? = null override fun onCreateView( inflater: LayoutInflater, @@ -28,14 +31,22 @@ class DemoFileSelectionFragment : FileSelectionFragment() { val topStub: ViewStub = v.findViewById(R.id.topStub) topStub.layoutResource = R.layout.header_file_select topStub.inflate() + + val bottomStub: ViewStub = v.findViewById(R.id.bottomStub) + bottomStub.layoutResource = R.layout.footer_files + val footer = bottomStub.inflate() as ExtendedFloatingActionButton + fab = footer + footer.setOnClickListener { + viewModel.onFilesSelected() + parentFragmentManager.beginTransaction() + .replace(R.id.container, RestoreFragment.newInstance()) + .commit() + } return v } - override fun onRestoreButtonClicked() { - viewModel.onFilesSelected() - parentFragmentManager.beginTransaction() - .replace(R.id.container, RestoreFragment.newInstance()) - .commit() + override fun onFileItemsChanged(filesItems: List) { + super.onFileItemsChanged(filesItems) + slideUpInRootView(fab!!) } - } diff --git a/storage/demo/src/main/res/layout/footer_files.xml b/storage/demo/src/main/res/layout/footer_files.xml new file mode 100644 index 00000000..e6cb3251 --- /dev/null +++ b/storage/demo/src/main/res/layout/footer_files.xml @@ -0,0 +1,17 @@ + + diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/ui/restore/FileSelectionFragment.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/ui/restore/FileSelectionFragment.kt index 6be678b9..720a345f 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/ui/restore/FileSelectionFragment.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/ui/restore/FileSelectionFragment.kt @@ -10,18 +10,20 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.behavior.HideBottomViewOnScrollBehavior import kotlinx.coroutines.launch import org.calyxos.backup.storage.R public abstract class FileSelectionFragment : Fragment() { protected abstract val viewModel: SnapshotViewModel - private lateinit var list: RecyclerView + protected lateinit var list: RecyclerView private lateinit var adapter: FilesAdapter override fun onCreateView( @@ -33,9 +35,6 @@ public abstract class FileSelectionFragment : Fragment() { val v = inflater.inflate(R.layout.fragment_select_files, container, false) list = v.findViewById(R.id.list) - v.findViewById(R.id.fab).setOnClickListener { - onRestoreButtonClicked() - } return v } @@ -49,15 +48,19 @@ public abstract class FileSelectionFragment : Fragment() { list.adapter = adapter lifecycleScope.launch { viewModel.fileSelectionManager.files.flowWithLifecycle(lifecycle, STARTED).collect { - onFileItemsChanged(it) + adapter.submitList(it) { + onFileItemsChanged(it) + } } } } - protected abstract fun onRestoreButtonClicked() - - @CallSuper protected open fun onFileItemsChanged(filesItems: List) { - adapter.submitList(filesItems) + } + + protected fun slideUpInRootView(view: View) { + val layoutParams = view.layoutParams as CoordinatorLayout.LayoutParams + val behavior = layoutParams.behavior as HideBottomViewOnScrollBehavior + behavior.slideUp(view) } } diff --git a/storage/lib/src/main/res/layout/fragment_select_files.xml b/storage/lib/src/main/res/layout/fragment_select_files.xml index 41a876b5..4f8c087a 100644 --- a/storage/lib/src/main/res/layout/fragment_select_files.xml +++ b/storage/lib/src/main/res/layout/fragment_select_files.xml @@ -37,17 +37,15 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/item_file" /> - + android:inflatedId="@+id/bottomStub" + app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" + tools:layout="@layout/item_custom" + tools:visibility="visible" /> diff --git a/storage/lib/src/main/res/values/strings.xml b/storage/lib/src/main/res/values/strings.xml index 99e32902..30e4044a 100644 --- a/storage/lib/src/main/res/values/strings.xml +++ b/storage/lib/src/main/res/values/strings.xml @@ -23,7 +23,7 @@ No storage backups found\n\nSorry, but there is nothing that can be restored. Error loading snapshots - Files to be restored + Review files for restore %1$d file(s) - Restore checked files + Restore selected files