Integrate files selection into real Seedvault app

This commit is contained in:
Torsten Grote 2024-06-21 17:10:52 -03:00
parent dbb40a4a5b
commit 118b2c0be0
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
13 changed files with 192 additions and 33 deletions

View file

@ -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<FilesItem>) {
slideUpInRootView(button)
}
}

View file

@ -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_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED 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.DisplayFragment.SELECT_APPS
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
@ -35,7 +36,12 @@ class RestoreActivity : RequireProvisioningActivity() {
RESTORE_APPS -> showFragment(InstallProgressFragment()) RESTORE_APPS -> showFragment(InstallProgressFragment())
RESTORE_BACKUP -> showFragment(RestoreProgressFragment()) RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
RESTORE_FILES -> showFragment(RestoreFilesFragment()) 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() else -> throw AssertionError()
} }
} }

View file

@ -47,7 +47,7 @@ internal class RestoreFilesFragment : SnapshotFragment() {
} }
override fun onSnapshotClicked(item: SnapshotItem) { override fun onSnapshotClicked(item: SnapshotItem) {
viewModel.startFilesRestore(item) viewModel.selectFilesForRestore(item)
} }
} }

View file

@ -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_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED 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.DisplayFragment.SELECT_APPS
import com.stevesoltys.seedvault.restore.install.ApkRestore import com.stevesoltys.seedvault.restore.install.ApkRestore
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
@ -44,6 +45,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.calyxos.backup.storage.api.SnapshotItem import org.calyxos.backup.storage.api.SnapshotItem
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.restore.RestoreService.Companion.EXTRA_TIMESTAMP_START 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.restore.RestoreService.Companion.EXTRA_USER_ID
import org.calyxos.backup.storage.ui.restore.FileSelectionManager import org.calyxos.backup.storage.ui.restore.FileSelectionManager
@ -100,6 +102,7 @@ internal class RestoreViewModel(
get() = appDataRestoreManager.restoreBackupResult get() = appDataRestoreManager.restoreBackupResult
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher) override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
private var storedSnapshot: StoredSnapshot? = null
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) ->
@ -181,12 +184,22 @@ internal class RestoreViewModel(
} }
@UiThread @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) val i = Intent(app, StorageRestoreService::class.java)
i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId) i.putExtra(EXTRA_USER_ID, storedSnapshot.userId)
i.putExtra(EXTRA_TIMESTAMP_START, item.time) i.putExtra(EXTRA_TIMESTAMP_START, storedSnapshot.timestamp)
app.startForegroundService(i) app.startForegroundService(i)
mDisplayFragment.setEvent(RESTORE_FILES_STARTED) mDisplayFragment.setEvent(RESTORE_FILES_STARTED)
this.storedSnapshot = null
} }
} }
@ -208,5 +221,10 @@ internal class RestoreBackupResult(val errorMsg: String? = null) {
} }
internal enum class DisplayFragment { 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,
} }

View file

@ -24,8 +24,7 @@ abstract class BackupActivity : AppCompatActivity() {
protected fun showFragment(f: Fragment, addToBackStack: Boolean = false, tag: String? = null) { protected fun showFragment(f: Fragment, addToBackStack: Boolean = false, tag: String? = null) {
supportFragmentManager.beginTransaction().apply { supportFragmentManager.beginTransaction().apply {
if (tag == null) replace(R.id.fragment, f) replace(R.id.fragment, f, tag)
else replace(R.id.fragment, f, tag)
if (addToBackStack) addToBackStack(null) if (addToBackStack) addToBackStack(null)
commit() commit()
} }

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?><!--
SPDX-FileCopyrightText: 2024 The Calyx Institute
SPDX-License-Identifier: Apache-2.0
-->
<androidx.appcompat.widget.AppCompatButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/button"
style="@style/SudPrimaryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:drawableStart="@drawable/ic_cloud_restore"
android:drawablePadding="8dp"
android:drawableTint="?android:textColorPrimaryInverse"
android:text="@string/select_files_button_restore"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?><!--
SPDX-FileCopyrightText: 2020 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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<ImageView
android:id="@+id/imageView"
style="@style/SudHeaderIcon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_cloud_download"
app:tint="?android:colorAccent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/titleView"
style="@style/SudHeaderTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/select_files_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<TextView
android:id="@+id/backupNameView"
style="@style/SudDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/restore_storage_selection_description"
android:textColor="?android:textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -234,6 +234,7 @@
<string name="restore_storage_skip">Skip restoring files</string> <string name="restore_storage_skip">Skip restoring files</string>
<string name="restore_storage_choose_snapshot">Choose a storage backup to restore (beta)</string> <string name="restore_storage_choose_snapshot">Choose a storage backup to restore (beta)</string>
<string name="restore_storage_selection_description">Selected files will be restored. Tap folders to see files in them.</string>
<string name="restore_storage_in_progress_title">Files are being restored…</string> <string name="restore_storage_in_progress_title">Files are being restored…</string>
<string name="restore_storage_in_progress_info">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.</string> <string name="restore_storage_in_progress_info">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.</string>
<string name="restore_storage_got_it">Got it</string> <string name="restore_storage_got_it">Got it</string>

View file

@ -11,13 +11,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewStub import android.view.ViewStub
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import de.grobox.storagebackuptester.MainViewModel import de.grobox.storagebackuptester.MainViewModel
import de.grobox.storagebackuptester.R import de.grobox.storagebackuptester.R
import org.calyxos.backup.storage.ui.restore.FileSelectionFragment import org.calyxos.backup.storage.ui.restore.FileSelectionFragment
import org.calyxos.backup.storage.ui.restore.FilesItem
class DemoFileSelectionFragment : FileSelectionFragment() { class DemoFileSelectionFragment : FileSelectionFragment() {
override val viewModel: MainViewModel by activityViewModels() override val viewModel: MainViewModel by activityViewModels()
private var fab: ExtendedFloatingActionButton? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -28,14 +31,22 @@ class DemoFileSelectionFragment : FileSelectionFragment() {
val topStub: ViewStub = v.findViewById(R.id.topStub) val topStub: ViewStub = v.findViewById(R.id.topStub)
topStub.layoutResource = R.layout.header_file_select topStub.layoutResource = R.layout.header_file_select
topStub.inflate() 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 return v
} }
override fun onRestoreButtonClicked() { override fun onFileItemsChanged(filesItems: List<FilesItem>) {
viewModel.onFilesSelected() super.onFileItemsChanged(filesItems)
parentFragmentManager.beginTransaction() slideUpInRootView(fab!!)
.replace(R.id.container, RestoreFragment.newInstance())
.commit()
} }
} }

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?><!--
SPDX-FileCopyrightText: 2024 The Calyx Institute
SPDX-License-Identifier: Apache-2.0
-->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/select_files_button_restore"
android:textColor="#ffffff"
app:backgroundTint="?colorAccent"
app:icon="@drawable/ic_cloud_restore"
app:iconTint="#ffffff"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />

View file

@ -10,18 +10,20 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.calyxos.backup.storage.R import org.calyxos.backup.storage.R
public abstract class FileSelectionFragment : Fragment() { public abstract class FileSelectionFragment : Fragment() {
protected abstract val viewModel: SnapshotViewModel protected abstract val viewModel: SnapshotViewModel
private lateinit var list: RecyclerView protected lateinit var list: RecyclerView
private lateinit var adapter: FilesAdapter private lateinit var adapter: FilesAdapter
override fun onCreateView( override fun onCreateView(
@ -33,9 +35,6 @@ 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
} }
@ -49,15 +48,19 @@ public abstract class FileSelectionFragment : Fragment() {
list.adapter = adapter list.adapter = adapter
lifecycleScope.launch { lifecycleScope.launch {
viewModel.fileSelectionManager.files.flowWithLifecycle(lifecycle, STARTED).collect { 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<FilesItem>) { protected open fun onFileItemsChanged(filesItems: List<FilesItem>) {
adapter.submitList(filesItems) }
protected fun slideUpInRootView(view: View) {
val layoutParams = view.layoutParams as CoordinatorLayout.LayoutParams
val behavior = layoutParams.behavior as HideBottomViewOnScrollBehavior
behavior.slideUp(view)
} }
} }

View file

@ -37,17 +37,15 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_file" /> tools:listitem="@layout/item_file" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <ViewStub
android:id="@+id/fab" android:id="@+id/bottomStub"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp" android:layout_margin="16dp"
android:text="@string/select_files_button_restore" android:inflatedId="@+id/bottomStub"
android:textColor="#ffffff" app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:backgroundTint="?colorAccent" tools:layout="@layout/item_custom"
app:icon="@drawable/ic_cloud_restore" tools:visibility="visible" />
app:iconTint="#ffffff"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -23,7 +23,7 @@
<string name="snapshots_empty">No storage backups found\n\nSorry, but there is nothing that can be restored.</string> <string name="snapshots_empty">No storage backups found\n\nSorry, but there is nothing that can be restored.</string>
<string name="snapshots_error">Error loading snapshots</string> <string name="snapshots_error">Error loading snapshots</string>
<string name="select_files_title">Files to be restored</string> <string name="select_files_title">Review files for restore</string>
<string name="select_files_number_of_files">%1$d file(s)</string> <string name="select_files_number_of_files">%1$d file(s)</string>
<string name="select_files_button_restore">Restore checked files</string> <string name="select_files_button_restore">Restore selected files</string>
</resources> </resources>