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

View file

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

View file

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

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

View file

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

View file

@ -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) {

View file

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

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