diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 20bdb3c4..5eb0f1ef 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -125,6 +125,12 @@
android:exported="false"
android:foregroundServiceType="dataSync"
android:label="BackupService" />
+
+
diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt
index eafcf5c8..b2d3e0e5 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/App.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt
@@ -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()) }
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt
index 635d10d7..9b78d7d3 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt
@@ -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()
}
})
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt
new file mode 100644
index 00000000..235ce9ab
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt
index e3b4ce77..adc8ced4 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt
@@ -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
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
index 199de755..6fb3dea8 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
@@ -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()
internal val restoreBackupResult: LiveData 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
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt
index 9388c7e2..a4c2c813 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt
@@ -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) {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt
index 30b82e28..5f55caec 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/res/layout/footer_snapshots.xml b/app/src/main/res/layout/footer_snapshots.xml
new file mode 100644
index 00000000..9b4e4d84
--- /dev/null
+++ b/app/src/main/res/layout/footer_snapshots.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_restore_files_started.xml b/app/src/main/res/layout/fragment_restore_files_started.xml
new file mode 100644
index 00000000..8746717b
--- /dev/null
+++ b/app/src/main/res/layout/fragment_restore_files_started.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/header_snapshots.xml b/app/src/main/res/layout/header_snapshots.xml
new file mode 100644
index 00000000..d91bfa44
--- /dev/null
+++ b/app/src/main/res/layout/header_snapshots.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7af0fd91..d6335f55 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -152,6 +152,13 @@
Restore complete
An error occurred while restoring the backup.
Finish
+
+ Skip restoring files
+ Choose a storage backup to restore (experimental)
+ Files are being restored…
+ 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.
+ Got it
+
Warning
You have chosen internal storage for your backup. This will not be available when your phone is lost or broken.
Choose other