Add UI for restoring files after app restore
This commit is contained in:
parent
fa123f07a0
commit
bdefb04a0d
12 changed files with 238 additions and 10 deletions
app/src/main
AndroidManifest.xml
java/com/stevesoltys/seedvault
App.kt
restore
storage
res
|
@ -125,6 +125,12 @@
|
|||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="BackupService" />
|
||||
<!-- Does restore as a foreground service -->
|
||||
<service
|
||||
android:name=".storage.StorageRestoreService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="RestoreService" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -49,7 +49,7 @@ open class App : Application() {
|
|||
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get()) }
|
||||
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
|
||||
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
||||
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get()) }
|
||||
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||
viewModel { FileSelectionViewModel(this@App, get()) }
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import androidx.annotation.CallSuper
|
|||
import com.stevesoltys.seedvault.R
|
||||
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.install.InstallProgressFragment
|
||||
import com.stevesoltys.seedvault.ui.LiveEventHandler
|
||||
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
||||
|
@ -28,6 +30,8 @@ class RestoreActivity : RequireProvisioningActivity() {
|
|||
when (fragment) {
|
||||
RESTORE_APPS -> showFragment(InstallProgressFragment())
|
||||
RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
|
||||
RESTORE_FILES -> showFragment(RestoreFilesFragment())
|
||||
RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment())
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
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 android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.stevesoltys.seedvault.R
|
||||
import org.calyxos.backup.storage.api.SnapshotItem
|
||||
import org.calyxos.backup.storage.ui.restore.SnapshotFragment
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
||||
internal class RestoreFilesFragment : SnapshotFragment() {
|
||||
override val viewModel: RestoreViewModel by sharedViewModel()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val topStub: ViewStub = v.findViewById(R.id.topStub)
|
||||
topStub.layoutResource = R.layout.header_snapshots
|
||||
topStub.inflate()
|
||||
|
||||
val bottomStub: ViewStub = v.findViewById(R.id.bottomStub)
|
||||
bottomStub.layoutResource = R.layout.footer_snapshots
|
||||
val footer = bottomStub.inflate()
|
||||
val skipView: TextView = footer.findViewById(R.id.skipView)
|
||||
skipView.setOnClickListener {
|
||||
requireActivity().apply {
|
||||
setResult(RESULT_OK)
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onSnapshotClicked(item: SnapshotItem) {
|
||||
viewModel.startFilesRestore(item)
|
||||
}
|
||||
}
|
||||
|
||||
internal class RestoreFilesStartedFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val v: View = inflater.inflate(R.layout.fragment_restore_files_started, container, false)
|
||||
|
||||
val button: Button = v.findViewById(R.id.button)
|
||||
button.setOnClickListener {
|
||||
requireActivity().apply {
|
||||
setResult(RESULT_OK)
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -38,7 +37,7 @@ class RestoreProgressFragment : Fragment() {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false)
|
||||
|
||||
progressBar = v.findViewById(R.id.progressBar)
|
||||
|
@ -61,8 +60,7 @@ class RestoreProgressFragment : Fragment() {
|
|||
|
||||
button.setText(R.string.restore_finished_button)
|
||||
button.setOnClickListener {
|
||||
requireActivity().setResult(RESULT_OK)
|
||||
requireActivity().finishAfterTransition()
|
||||
viewModel.onFinishClickedAfterRestoringAppData()
|
||||
}
|
||||
|
||||
// decryption will fail when the device is locked, so keep the screen on to prevent locking
|
||||
|
|
|
@ -5,9 +5,11 @@ import android.app.backup.IBackupManager
|
|||
import android.app.backup.IRestoreObserver
|
||||
import android.app.backup.IRestoreSession
|
||||
import android.app.backup.RestoreSet
|
||||
import android.content.Intent
|
||||
import android.os.RemoteException
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
@ -27,11 +29,14 @@ import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
|||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||
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.install.ApkRestore
|
||||
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
||||
import com.stevesoltys.seedvault.restore.install.InstallResult
|
||||
import com.stevesoltys.seedvault.restore.install.isInstalled
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.storage.StorageRestoreService
|
||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||
|
@ -54,6 +59,10 @@ import kotlinx.coroutines.flow.flowOn
|
|||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import org.calyxos.backup.storage.api.SnapshotItem
|
||||
import org.calyxos.backup.storage.api.StorageBackup
|
||||
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTAMP_START
|
||||
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||
import java.util.LinkedList
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
@ -68,8 +77,10 @@ internal class RestoreViewModel(
|
|||
private val backupManager: IBackupManager,
|
||||
private val restoreCoordinator: RestoreCoordinator,
|
||||
private val apkRestore: ApkRestore,
|
||||
storageBackup: StorageBackup,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager), RestorableBackupClickListener {
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager),
|
||||
RestorableBackupClickListener, SnapshotViewModel {
|
||||
|
||||
override val isRestoreOperation = true
|
||||
|
||||
|
@ -110,6 +121,8 @@ internal class RestoreViewModel(
|
|||
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
|
||||
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
|
||||
|
||||
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
private fun getOrStartSession(): IRestoreSession {
|
||||
val session = this.session
|
||||
|
@ -168,7 +181,7 @@ internal class RestoreViewModel(
|
|||
.asLiveData(ioDispatcher)
|
||||
}
|
||||
|
||||
internal fun onNextClicked() {
|
||||
internal fun onNextClickedAfterInstallingApps() {
|
||||
mDisplayFragment.postEvent(RESTORE_BACKUP)
|
||||
val token = mChosenRestorableBackup.value?.token ?: throw AssertionError()
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
|
@ -371,6 +384,19 @@ internal class RestoreViewModel(
|
|||
|
||||
}
|
||||
|
||||
@UiThread
|
||||
internal fun onFinishClickedAfterRestoringAppData() {
|
||||
mDisplayFragment.setEvent(RESTORE_FILES)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
internal fun startFilesRestore(item: SnapshotItem) {
|
||||
val i = Intent(app, StorageRestoreService::class.java)
|
||||
i.putExtra(EXTRA_TIMESTAMP_START, item.time)
|
||||
app.startForegroundService(i)
|
||||
mDisplayFragment.setEvent(RESTORE_FILES_STARTED)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class RestoreSetResult(
|
||||
|
@ -389,4 +415,6 @@ internal class RestoreBackupResult(val errorMsg: String? = null) {
|
|||
internal fun hasError(): Boolean = errorMsg != null
|
||||
}
|
||||
|
||||
internal enum class DisplayFragment { RESTORE_APPS, RESTORE_BACKUP }
|
||||
internal enum class DisplayFragment {
|
||||
RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ class InstallProgressFragment : Fragment(), InstallItemListener {
|
|||
addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
||||
}
|
||||
button.setText(R.string.restore_next)
|
||||
button.setOnClickListener { viewModel.onNextClicked() }
|
||||
button.setOnClickListener { viewModel.onNextClickedAfterInstallingApps() }
|
||||
|
||||
viewModel.chosenRestorableBackup.observe(viewLifecycleOwner, Observer { restorableBackup ->
|
||||
backupNameView.text = restorableBackup.name
|
||||
|
@ -76,7 +76,7 @@ class InstallProgressFragment : Fragment(), InstallItemListener {
|
|||
|
||||
private fun onInstallResult(installResult: InstallResult) {
|
||||
// skip this screen, if there are no apps to install
|
||||
if (installResult.isEmpty) viewModel.onNextClicked()
|
||||
if (installResult.isEmpty) viewModel.onNextClickedAfterInstallingApps()
|
||||
|
||||
// if finished, treat all still queued apps as failed and resort/redisplay adapter items
|
||||
if (installResult.isFinished) {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.stevesoltys.seedvault.storage
|
||||
|
||||
import org.calyxos.backup.storage.api.BackupObserver
|
||||
import org.calyxos.backup.storage.api.RestoreObserver
|
||||
import org.calyxos.backup.storage.api.StorageBackup
|
||||
import org.calyxos.backup.storage.backup.BackupJobService
|
||||
import org.calyxos.backup.storage.backup.BackupService
|
||||
import org.calyxos.backup.storage.backup.NotificationBackupObserver
|
||||
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
|
||||
import org.calyxos.backup.storage.restore.RestoreService
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
/*
|
||||
|
@ -28,3 +31,12 @@ internal class StorageBackupService : BackupService() {
|
|||
NotificationBackupObserver(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
internal class StorageRestoreService : RestoreService() {
|
||||
override val storageBackup: StorageBackup by inject()
|
||||
|
||||
// use lazy delegate because context isn't available during construction time
|
||||
override val restoreObserver: RestoreObserver by lazy {
|
||||
NotificationRestoreObserver(applicationContext)
|
||||
}
|
||||
}
|
||||
|
|
18
app/src/main/res/layout/footer_snapshots.xml
Normal file
18
app/src/main/res/layout/footer_snapshots.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/skipView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/restore_storage_skip"
|
||||
android:textColor="?android:colorAccent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
56
app/src/main/res/layout/fragment_restore_files_started.xml
Normal file
56
app/src/main/res/layout/fragment_restore_files_started.xml
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_margin="16dp"
|
||||
android:tint="?android:colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_cloud_restore"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/restore_storage_in_progress_title"
|
||||
android:textSize="24sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/infoView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/restore_storage_in_progress_info"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/restore_storage_got_it"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/infoView"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
33
app/src/main/res/layout/header_snapshots.xml
Normal file
33
app/src/main/res/layout/header_snapshots.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_margin="16dp"
|
||||
android:tint="?android:colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_cloud_download"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/restore_storage_choose_snapshot"
|
||||
android:textSize="24sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -152,6 +152,13 @@
|
|||
<string name="restore_finished_success">Restore complete</string>
|
||||
<string name="restore_finished_error">An error occurred while restoring the backup.</string>
|
||||
<string name="restore_finished_button">Finish</string>
|
||||
|
||||
<string name="restore_storage_skip">Skip restoring files</string>
|
||||
<string name="restore_storage_choose_snapshot">Choose a storage backup to restore (experimental)</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 not complete.</string>
|
||||
<string name="restore_storage_got_it">Got it</string>
|
||||
|
||||
<string name="storage_internal_warning_title">Warning</string>
|
||||
<string name="storage_internal_warning_message">You have chosen internal storage for your backup. This will not be available when your phone is lost or broken.</string>
|
||||
<string name="storage_internal_warning_choose_other">Choose other</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue