From 1a7fdfa59ab25d2d0cef55aedd56092dfdf96b43 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 6 Sep 2019 12:36:51 -0300 Subject: [PATCH] Implement restoring of backup and show progress in UI Note that the progress view is not exact as the progress reporting of AOSP seems to be buggy. --- .../java/com/stevesoltys/backup/Backup.kt | 2 + .../backup/NotificationBackupObserver.kt | 13 ++- .../backup/restore/RestoreActivity.kt | 9 +- .../backup/restore/RestoreProgressFragment.kt | 70 +++++++++++ .../backup/restore/RestoreSetAdapter.kt | 2 +- .../backup/restore/RestoreSetFragment.kt | 13 +-- .../backup/restore/RestoreViewModel.kt | 50 ++++++-- .../backup/transport/restore/KVRestore.kt | 4 + .../stevesoltys/backup/ui/BackupActivity.kt | 4 +- .../backup/ui/BackupLocationFragment.kt | 18 +-- .../stevesoltys/backup/ui/BackupViewModel.kt | 10 +- .../backup/ui/RecoveryCodeInputFragment.kt | 4 +- .../res/layout/fragment_restore_progress.xml | 110 ++++++++++++++++++ app/src/main/res/values/strings.xml | 6 + 14 files changed, 268 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/restore/RestoreProgressFragment.kt create mode 100644 app/src/main/res/layout/fragment_restore_progress.xml diff --git a/app/src/main/java/com/stevesoltys/backup/Backup.kt b/app/src/main/java/com/stevesoltys/backup/Backup.kt index 1efe3c72..4b8a5a4f 100644 --- a/app/src/main/java/com/stevesoltys/backup/Backup.kt +++ b/app/src/main/java/com/stevesoltys/backup/Backup.kt @@ -62,3 +62,5 @@ class Backup : Application() { } fun Uri.isOnExternalStorage() = authority == URI_AUTHORITY_EXTERNAL_STORAGE + +fun isDebugBuild() = Build.TYPE == "userdebug" diff --git a/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt index de510f80..4486d627 100644 --- a/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/backup/NotificationBackupObserver.kt @@ -3,6 +3,7 @@ package com.stevesoltys.backup import android.app.backup.BackupProgress import android.app.backup.IBackupObserver import android.content.Context +import android.content.pm.PackageManager import android.util.Log import android.util.Log.INFO import android.util.Log.isLoggable @@ -62,10 +63,12 @@ class NotificationBackupObserver(context: Context, private val userInitiated: Bo nm.onBackupFinished() } - private fun getAppName(packageId: String): CharSequence { - if (packageId == "@pm@") return packageId - val appInfo = pm.getApplicationInfo(packageId, 0) - return pm.getApplicationLabel(appInfo) - } + private fun getAppName(packageId: String): CharSequence = getAppName(pm, packageId) } + +fun getAppName(pm: PackageManager, packageId: String): CharSequence { + if (packageId == "@pm@") return packageId + val appInfo = pm.getApplicationInfo(packageId, 0) + return pm.getApplicationLabel(appInfo) +} diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt index 09c73839..bd9d0660 100644 --- a/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt @@ -1,6 +1,7 @@ package com.stevesoltys.backup.restore import android.os.Bundle +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.stevesoltys.backup.R import com.stevesoltys.backup.ui.BackupActivity @@ -20,7 +21,13 @@ class RestoreActivity : BackupActivity() { setContentView(R.layout.activity_fragment_container) - if (savedInstanceState == null) showFragment(getInitialFragment()) + viewModel.chosenRestoreSet.observe(this, Observer { set -> + if (set != null) showFragment(RestoreProgressFragment()) + }) + + if (savedInstanceState == null && viewModel.validLocationIsSet()) { + showFragment(getInitialFragment()) + } } override fun onInvalidLocation() { diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreProgressFragment.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreProgressFragment.kt new file mode 100644 index 00000000..171cd88f --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreProgressFragment.kt @@ -0,0 +1,70 @@ +package com.stevesoltys.backup.restore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.R +import com.stevesoltys.backup.getAppName +import com.stevesoltys.backup.isDebugBuild +import kotlinx.android.synthetic.main.fragment_restore_progress.* + +class RestoreProgressFragment : Fragment() { + + private lateinit var viewModel: RestoreViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_restore_progress, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + // decryption will fail when the device is locked, so keep the screen on to prevent locking + requireActivity().window.addFlags(FLAG_KEEP_SCREEN_ON) + + viewModel = ViewModelProviders.of(requireActivity()).get(RestoreViewModel::class.java) + + viewModel.numPackages.observe(this, Observer { numPackages -> + progressBar.min = 0 + progressBar.max = numPackages + }) + + viewModel.chosenRestoreSet.observe(this, Observer { set -> + backupNameView.text = set.device + }) + + viewModel.restoreProgress.observe(this, Observer { progress -> + progressBar.progress = progress.nowBeingRestored + val appName = getAppName(requireActivity().packageManager, progress.currentPackage) + val displayName = if (isDebugBuild()) "$appName (${progress.currentPackage})" else appName + currentPackageView.text = getString(R.string.restore_current_package, displayName) + }) + + viewModel.restoreFinished.observe(this, Observer { finished -> + progressBarIndefinite.visibility = INVISIBLE + progressBar.progress = viewModel.numPackages.value ?: progressBar.max + button.visibility = VISIBLE + if (finished == 0) { + // success + currentPackageView.text = getString(R.string.restore_finished_success) + warningView.visibility = VISIBLE + } else { + // error + currentPackageView.text = getString(R.string.restore_finished_error) + currentPackageView.setTextColor(warningView.textColors) + } + activity?.window?.clearFlags(FLAG_KEEP_SCREEN_ON) + }) + + button.setOnClickListener { requireActivity().finishAfterTransition() } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt index b3f74c2d..d6acf602 100644 --- a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt @@ -34,7 +34,7 @@ internal class RestoreSetAdapter( internal fun bind(item: RestoreSet) { v.setOnClickListener { listener.onRestoreSetClicked(item) } titleView.text = item.name - subtitleView.text = item.device + subtitleView.text = "Android Backup" // TODO change to backup date when available } } diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt index 4b7120b4..39611868 100644 --- a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt @@ -2,7 +2,6 @@ package com.stevesoltys.backup.restore import android.app.backup.RestoreSet import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.View.INVISIBLE @@ -14,7 +13,7 @@ import androidx.lifecycle.ViewModelProviders import com.stevesoltys.backup.R import kotlinx.android.synthetic.main.fragment_restore_set.* -class RestoreSetFragment : Fragment(), RestoreSetClickListener { +class RestoreSetFragment : Fragment() { private lateinit var viewModel: RestoreViewModel @@ -34,7 +33,9 @@ class RestoreSetFragment : Fragment(), RestoreSetClickListener { override fun onStart() { super.onStart() - viewModel.loadRestoreSets() + if (viewModel.recoveryCodeIsSet() && viewModel.validLocationIsSet()) { + viewModel.loadRestoreSets() + } } private fun onRestoreSetsLoaded(result: RestoreSetResult) { @@ -49,14 +50,10 @@ class RestoreSetFragment : Fragment(), RestoreSetClickListener { listView.visibility = VISIBLE progressBar.visibility = INVISIBLE - listView.adapter = RestoreSetAdapter(this, result.sets) + listView.adapter = RestoreSetAdapter(viewModel, result.sets) } } - override fun onRestoreSetClicked(set: RestoreSet) { - Log.e("TEST", "RESTORE SET CLICKED: ${set.name} ${set.device} ${set.token}") - } - } internal interface RestoreSetClickListener { diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt index cc66dae9..d6d5e0d0 100644 --- a/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt @@ -6,6 +6,7 @@ import android.app.backup.IRestoreSession import android.app.backup.RestoreSet import android.net.Uri import android.util.Log +import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.stevesoltys.backup.Backup @@ -16,7 +17,7 @@ import com.stevesoltys.backup.ui.BackupViewModel private val TAG = RestoreViewModel::class.java.simpleName -class RestoreViewModel(app: Application) : BackupViewModel(app) { +class RestoreViewModel(app: Application) : BackupViewModel(app), RestoreSetClickListener { private val backupManager = Backup.backupManager @@ -27,8 +28,21 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) { private val mRestoreSets = MutableLiveData() internal val restoreSets: LiveData get() = mRestoreSets + private val mChosenRestoreSet = MutableLiveData() + internal val chosenRestoreSet: LiveData get() = mChosenRestoreSet + + private var mNumPackages = MutableLiveData() + internal val numPackages: LiveData get() = mNumPackages + + private val mRestoreProgress = MutableLiveData() + internal val restoreProgress: LiveData get() = mRestoreProgress + + private val mRestoreFinished = MutableLiveData() + // Zero on success; a nonzero error code if the restore operation as a whole failed. + internal val restoreFinished: LiveData get() = mRestoreFinished + override fun acceptBackupLocation(folderUri: Uri): Boolean { - // TODO + // TODO search if there's really a backup available in this location and see if we can decrypt it return true } @@ -52,6 +66,14 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) { } } + override fun onRestoreSetClicked(set: RestoreSet) { + val session = this.session + check(session != null) + session.restoreAll(set.token, observer, monitor) + + mChosenRestoreSet.value = set + } + override fun onCleared() { super.onCleared() endSession() @@ -63,8 +85,11 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) { observer = null } + @WorkerThread private inner class RestoreObserver : IRestoreObserver.Stub() { + private var correctedNow: Int = -1 + /** * Supply a list of the restore datasets available from the current transport. * This method is invoked as a callback following the application's use of the @@ -76,7 +101,7 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) { */ override fun restoreSetsAvailable(restoreSets: Array?) { if (restoreSets == null || restoreSets.isEmpty()) { - mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_empty_result)) + mRestoreSets.postValue(RestoreSetResult(app.getString(R.string.restore_set_empty_result))) } else { mRestoreSets.postValue(RestoreSetResult(restoreSets)) } @@ -88,7 +113,7 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) { * @param numPackages The total number of packages being processed in this restore operation. */ override fun restoreStarting(numPackages: Int) { - Log.e(TAG, "RESTORE STARTING $numPackages") + mNumPackages.postValue(numPackages) } /** @@ -101,17 +126,22 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) { * @param currentPackage The name of the package now being restored. */ override fun onUpdate(nowBeingRestored: Int, currentPackage: String) { - Log.e(TAG, "RESTORE UPDATE $nowBeingRestored $currentPackage") + if (nowBeingRestored <= correctedNow) { + correctedNow += 1 + } else { + correctedNow = nowBeingRestored + } + mRestoreProgress.postValue(RestoreProgress(correctedNow, currentPackage)) } /** * The restore operation has completed. * - * @param error Zero on success; a nonzero error code if the restore operation + * @param result Zero on success; a nonzero error code if the restore operation * as a whole failed. */ - override fun restoreFinished(error: Int) { - Log.e(TAG, "RESTORE FINISHED $error") + override fun restoreFinished(result: Int) { + mRestoreFinished.postValue(result) endSession() } @@ -129,3 +159,7 @@ internal class RestoreSetResult( internal fun hasError(): Boolean = errorMsg != null } + +internal class RestoreProgress( + internal val nowBeingRestored: Int, + internal val currentPackage: String) diff --git a/app/src/main/java/com/stevesoltys/backup/transport/restore/KVRestore.kt b/app/src/main/java/com/stevesoltys/backup/transport/restore/KVRestore.kt index 4259998b..6ce3de67 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/restore/KVRestore.kt +++ b/app/src/main/java/com/stevesoltys/backup/transport/restore/KVRestore.kt @@ -13,6 +13,7 @@ import com.stevesoltys.backup.header.UnsupportedVersionException import libcore.io.IoUtils.closeQuietly import java.io.IOException import java.util.* +import javax.crypto.AEADBadTagException private class KVRestoreState( internal val token: Long, @@ -86,6 +87,9 @@ internal class KVRestore( } catch (e: UnsupportedVersionException) { Log.e(TAG, "Unsupported version in backup: ${e.version}", e) TRANSPORT_ERROR + } catch (e: AEADBadTagException) { + Log.e(TAG, "Decryption failed", e) + TRANSPORT_ERROR } finally { this.state = null closeQuietly(data) diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt index 3df956fd..666bdd0a 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt @@ -59,7 +59,9 @@ abstract class BackupActivity : AppCompatActivity() { @CallSuper override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { - if (resultCode != RESULT_OK) { + if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) { + getViewModel().handleChooseFolderResult(result) + } else if (resultCode != RESULT_OK) { Log.w(TAG, "Error in activity result: $requestCode") finishAfterTransition() } else { diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt index d58fbff7..7007ede0 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt @@ -1,6 +1,5 @@ package com.stevesoltys.backup.ui -import android.app.Activity.RESULT_OK import android.content.ActivityNotFoundException import android.content.Intent import android.content.Intent.* @@ -8,23 +7,17 @@ import android.os.Bundle import android.provider.DocumentsContract.EXTRA_PROMPT import android.widget.Toast import android.widget.Toast.LENGTH_LONG -import androidx.lifecycle.ViewModelProviders import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.stevesoltys.backup.R -import com.stevesoltys.backup.settings.SettingsViewModel class BackupLocationFragment : PreferenceFragmentCompat() { - private lateinit var viewModel: SettingsViewModel - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.backup_location, rootKey) requireActivity().setTitle(R.string.settings_backup_location_title) - viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java) - val externalStorage = Preference(requireContext()).apply { setIcon(R.drawable.ic_storage) setTitle(R.string.settings_backup_external_storage) @@ -43,18 +36,11 @@ class BackupLocationFragment : PreferenceFragmentCompat() { FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) try { val documentChooser = createChooser(openTreeIntent, null) - startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE) + // start from the activity context, so we can receive and handle the result there + requireActivity().startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE) } catch (ex: ActivityNotFoundException) { Toast.makeText(requireContext(), "Please install a file manager.", LENGTH_LONG).show() } } - override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { - if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) { - viewModel.handleChooseFolderResult(result) - } else { - super.onActivityResult(requestCode, resultCode, result) - } - } - } diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt index 7f622afc..5845a6ec 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt @@ -23,10 +23,10 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode * Will be set to true if this is the initial location. * It will be false if an existing location was changed. */ - internal val onLocationSet: LiveEvent = locationWasSet + internal val onLocationSet: LiveEvent get() = locationWasSet private val mChooseBackupLocation = MutableLiveEvent() - internal val chooseBackupLocation: LiveEvent = mChooseBackupLocation + internal val chooseBackupLocation: LiveEvent get() = mChooseBackupLocation internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true) internal fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey() @@ -52,12 +52,12 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode // store backup folder location in settings setBackupFolderUri(app, folderUri) - // notify the UI that the location has been set - locationWasSet.setEvent(LocationResult(true, initialSetUp)) - // stop backup service to be sure the old location will get updated app.stopService(Intent(app, ConfigurableBackupTransportService::class.java)) + // notify the UI that the location has been set + locationWasSet.setEvent(LocationResult(true, initialSetUp)) + Log.d(TAG, "New storage location chosen: $folderUri") } else { Log.w(TAG, "Location was rejected: $folderUri") diff --git a/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt index d631ac1b..6841acb5 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt @@ -1,6 +1,5 @@ package com.stevesoltys.backup.ui -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -11,6 +10,7 @@ import android.widget.Toast.LENGTH_LONG import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders import com.stevesoltys.backup.R +import com.stevesoltys.backup.isDebugBuild import io.github.novacrypto.bip39.Validation.InvalidChecksumException import io.github.novacrypto.bip39.Validation.WordNotFoundException import kotlinx.android.synthetic.main.fragment_recovery_code_input.* @@ -37,7 +37,7 @@ class RecoveryCodeInputFragment : Fragment() { } doneButton.setOnClickListener { done() } - if (Build.TYPE == "userdebug") debugPreFill() + if (isDebugBuild()) debugPreFill() } private fun getInput(): List = ArrayList(WORD_NUM).apply { diff --git a/app/src/main/res/layout/fragment_restore_progress.xml b/app/src/main/res/layout/fragment_restore_progress.xml new file mode 100644 index 00000000..44856401 --- /dev/null +++ b/app/src/main/res/layout/fragment_restore_progress.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + +