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_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()
}
}

View file

@ -47,7 +47,7 @@ internal class RestoreFilesFragment : SnapshotFragment() {
}
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_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,
}

View file

@ -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()
}

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_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_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>

View file

@ -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<FilesItem>) {
super.onFileItemsChanged(filesItems)
slideUpInRootView(fab!!)
}
}

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.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<View>(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<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"
tools:listitem="@layout/item_file" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab"
<ViewStub
android:id="@+id/bottomStub"
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" />
android:inflatedId="@+id/bottomStub"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
tools:layout="@layout/item_custom"
tools:visibility="visible" />
</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_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_button_restore">Restore checked files</string>
<string name="select_files_button_restore">Restore selected files</string>
</resources>