1
0
Fork 0

Add UI for restoring files after app restore

This commit is contained in:
Torsten Grote 2021-02-22 11:26:21 -03:00 committed by Chirayu Desai
parent fa123f07a0
commit bdefb04a0d
12 changed files with 238 additions and 10 deletions

View file

@ -125,6 +125,12 @@
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
android:label="BackupService" /> android:label="BackupService" />
<!-- Does restore as a foreground service -->
<service
android:name=".storage.StorageRestoreService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:label="RestoreService" />
</application> </application>
</manifest> </manifest>

View file

@ -49,7 +49,7 @@ open class App : Application() {
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, 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()) } viewModel { FileSelectionViewModel(this@App, get()) }
} }

View file

@ -5,6 +5,8 @@ import androidx.annotation.CallSuper
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS 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_STARTED
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
import com.stevesoltys.seedvault.ui.LiveEventHandler import com.stevesoltys.seedvault.ui.LiveEventHandler
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
@ -28,6 +30,8 @@ class RestoreActivity : RequireProvisioningActivity() {
when (fragment) { when (fragment) {
RESTORE_APPS -> showFragment(InstallProgressFragment()) RESTORE_APPS -> showFragment(InstallProgressFragment())
RESTORE_BACKUP -> showFragment(RestoreProgressFragment()) RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
RESTORE_FILES -> showFragment(RestoreFilesFragment())
RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment())
else -> throw AssertionError() else -> throw AssertionError()
} }
}) })

View file

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

View file

@ -1,6 +1,5 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.restore
import android.app.Activity.RESULT_OK
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -38,7 +37,7 @@ class RestoreProgressFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false) val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false)
progressBar = v.findViewById(R.id.progressBar) progressBar = v.findViewById(R.id.progressBar)
@ -61,8 +60,7 @@ class RestoreProgressFragment : Fragment() {
button.setText(R.string.restore_finished_button) button.setText(R.string.restore_finished_button)
button.setOnClickListener { button.setOnClickListener {
requireActivity().setResult(RESULT_OK) viewModel.onFinishClickedAfterRestoringAppData()
requireActivity().finishAfterTransition()
} }
// decryption will fail when the device is locked, so keep the screen on to prevent locking // decryption will fail when the device is locked, so keep the screen on to prevent locking

View file

@ -5,9 +5,11 @@ import android.app.backup.IBackupManager
import android.app.backup.IRestoreObserver import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.content.Intent
import android.os.RemoteException import android.os.RemoteException
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData 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.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS 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_STARTED
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
import com.stevesoltys.seedvault.restore.install.InstallResult import com.stevesoltys.seedvault.restore.install.InstallResult
import com.stevesoltys.seedvault.restore.install.isInstalled import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState
@ -54,6 +59,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch 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 java.util.LinkedList
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -68,8 +77,10 @@ internal class RestoreViewModel(
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val restoreCoordinator: RestoreCoordinator, private val restoreCoordinator: RestoreCoordinator,
private val apkRestore: ApkRestore, private val apkRestore: ApkRestore,
storageBackup: StorageBackup,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : RequireProvisioningViewModel(app, settingsManager, keyManager), RestorableBackupClickListener { ) : RequireProvisioningViewModel(app, settingsManager, keyManager),
RestorableBackupClickListener, SnapshotViewModel {
override val isRestoreOperation = true override val isRestoreOperation = true
@ -110,6 +121,8 @@ internal class RestoreViewModel(
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>() private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
@Throws(RemoteException::class) @Throws(RemoteException::class)
private fun getOrStartSession(): IRestoreSession { private fun getOrStartSession(): IRestoreSession {
val session = this.session val session = this.session
@ -168,7 +181,7 @@ internal class RestoreViewModel(
.asLiveData(ioDispatcher) .asLiveData(ioDispatcher)
} }
internal fun onNextClicked() { internal fun onNextClickedAfterInstallingApps() {
mDisplayFragment.postEvent(RESTORE_BACKUP) mDisplayFragment.postEvent(RESTORE_BACKUP)
val token = mChosenRestorableBackup.value?.token ?: throw AssertionError() val token = mChosenRestorableBackup.value?.token ?: throw AssertionError()
viewModelScope.launch(ioDispatcher) { 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( internal class RestoreSetResult(
@ -389,4 +415,6 @@ internal class RestoreBackupResult(val errorMsg: String? = null) {
internal fun hasError(): Boolean = errorMsg != 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
}

View file

@ -59,7 +59,7 @@ class InstallProgressFragment : Fragment(), InstallItemListener {
addItemDecoration(DividerItemDecoration(context, VERTICAL)) addItemDecoration(DividerItemDecoration(context, VERTICAL))
} }
button.setText(R.string.restore_next) button.setText(R.string.restore_next)
button.setOnClickListener { viewModel.onNextClicked() } button.setOnClickListener { viewModel.onNextClickedAfterInstallingApps() }
viewModel.chosenRestorableBackup.observe(viewLifecycleOwner, Observer { restorableBackup -> viewModel.chosenRestorableBackup.observe(viewLifecycleOwner, Observer { restorableBackup ->
backupNameView.text = restorableBackup.name backupNameView.text = restorableBackup.name
@ -76,7 +76,7 @@ class InstallProgressFragment : Fragment(), InstallItemListener {
private fun onInstallResult(installResult: InstallResult) { private fun onInstallResult(installResult: InstallResult) {
// skip this screen, if there are no apps to install // 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 finished, treat all still queued apps as failed and resort/redisplay adapter items
if (installResult.isFinished) { if (installResult.isFinished) {

View file

@ -1,10 +1,13 @@
package com.stevesoltys.seedvault.storage package com.stevesoltys.seedvault.storage
import org.calyxos.backup.storage.api.BackupObserver 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.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.backup.storage.backup.BackupService import org.calyxos.backup.storage.backup.BackupService
import org.calyxos.backup.storage.backup.NotificationBackupObserver 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 import org.koin.android.ext.android.inject
/* /*
@ -28,3 +31,12 @@ internal class StorageBackupService : BackupService() {
NotificationBackupObserver(applicationContext) 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)
}
}

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

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

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

View file

@ -152,6 +152,13 @@
<string name="restore_finished_success">Restore complete</string> <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_error">An error occurred while restoring the backup.</string>
<string name="restore_finished_button">Finish</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_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_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> <string name="storage_internal_warning_choose_other">Choose other</string>