Integrate files selection into real Seedvault app
This commit is contained in:
parent
dbb40a4a5b
commit
118b2c0be0
13 changed files with 192 additions and 33 deletions
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ internal class RestoreFilesFragment : SnapshotFragment() {
|
|||
}
|
||||
|
||||
override fun onSnapshotClicked(item: SnapshotItem) {
|
||||
viewModel.startFilesRestore(item)
|
||||
viewModel.selectFilesForRestore(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
17
app/src/main/res/layout/footer_files_selection.xml
Normal file
17
app/src/main/res/layout/footer_files_selection.xml
Normal 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" />
|
44
app/src/main/res/layout/header_files_selection.xml
Normal file
44
app/src/main/res/layout/header_files_selection.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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!!)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
17
storage/demo/src/main/res/layout/footer_files.xml
Normal file
17
storage/demo/src/main/res/layout/footer_files.xml
Normal 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" />
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue