From aa3aad8fb3103eb5ca7fe9cf454c5b7c0d94682f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 5 Sep 2019 17:39:48 -0300 Subject: [PATCH 01/17] Return actually available RestoreSets Before, we were always returnign a dummy RestoreSet, if one was actually available or not. Now, we also include the device name. Note that it is planned to store the actual device name and other metadata in an encrypted file so that the backup server will not learn it. --- .../transport/restore/RestoreCoordinator.kt | 2 +- .../plugins/DocumentsProviderRestorePlugin.kt | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/backup/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/backup/transport/restore/RestoreCoordinator.kt index d6dc397a..7d118803 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/backup/transport/restore/RestoreCoordinator.kt @@ -46,7 +46,7 @@ internal class RestoreCoordinator( * or [TRANSPORT_ERROR] (an error occurred, the restore should be aborted and rescheduled). */ fun startRestore(token: Long, packages: Array): Int { - if (state != null) throw IllegalStateException() + check(state == null) Log.i(TAG, "Start restore with ${packages.map { info -> info.packageName }}") state = RestoreCoordinatorState(token, packages.iterator()) return TRANSPORT_OK diff --git a/app/src/main/java/com/stevesoltys/backup/transport/restore/plugins/DocumentsProviderRestorePlugin.kt b/app/src/main/java/com/stevesoltys/backup/transport/restore/plugins/DocumentsProviderRestorePlugin.kt index deb9e327..5fc63579 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/restore/plugins/DocumentsProviderRestorePlugin.kt +++ b/app/src/main/java/com/stevesoltys/backup/transport/restore/plugins/DocumentsProviderRestorePlugin.kt @@ -7,19 +7,26 @@ import com.stevesoltys.backup.transport.restore.FullRestorePlugin import com.stevesoltys.backup.transport.restore.KVRestorePlugin import com.stevesoltys.backup.transport.restore.RestorePlugin -class DocumentsProviderRestorePlugin( - private val documentsStorage: DocumentsStorage) : RestorePlugin { +class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : RestorePlugin { override val kvRestorePlugin: KVRestorePlugin by lazy { - DocumentsProviderKVRestorePlugin(documentsStorage) + DocumentsProviderKVRestorePlugin(storage) } override val fullRestorePlugin: FullRestorePlugin by lazy { - DocumentsProviderFullRestorePlugin(documentsStorage) + DocumentsProviderFullRestorePlugin(storage) } override fun getAvailableRestoreSets(): Array? { - return arrayOf(RestoreSet("default", "device", DEFAULT_RESTORE_SET_TOKEN)) + val rootDir = storage.rootBackupDir ?: return null + val restoreSets = ArrayList() + for (file in rootDir.listFiles()) { + if (file.isDirectory && file.findFile(DEFAULT_RESTORE_SET_TOKEN.toString()) != null) { + // TODO include time of last backup + file.name?.let { restoreSets.add(RestoreSet(it, it, DEFAULT_RESTORE_SET_TOKEN)) } + } + } + return restoreSets.toTypedArray() } override fun getCurrentRestoreSet(): Long { From 491789e8e0710237b490d67c506d7ab858b23c97 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 5 Sep 2019 17:42:39 -0300 Subject: [PATCH 02/17] Add a RestoreActivity that lists available RestoreSets (backups) and allows the user to select one to get restored. --- app/src/main/AndroidManifest.xml | 13 +- .../backup/restore/RestoreActivity.kt | 30 ++++ .../backup/restore/RestoreSetAdapter.kt | 42 ++++++ .../backup/restore/RestoreSetFragment.kt | 64 +++++++++ .../backup/restore/RestoreViewModel.kt | 131 ++++++++++++++++++ .../backup/settings/SettingsActivity.kt | 77 ++-------- .../backup/settings/SettingsFragment.kt | 4 +- .../backup/settings/SettingsViewModel.kt | 60 +------- .../transport/ConfigurableBackupTransport.kt | 4 +- .../stevesoltys/backup/ui/BackupActivity.kt | 95 +++++++++++++ .../BackupLocationFragment.kt | 5 +- .../stevesoltys/backup/ui/BackupViewModel.kt | 76 ++++++++++ .../stevesoltys/backup/{ => ui}/LiveEvent.kt | 4 +- .../backup/{ => ui}/LiveEventHandler.java | 2 +- .../backup/{ => ui}/MutableLiveEvent.kt | 2 +- .../{settings => ui}/RecoveryCodeActivity.kt | 3 +- .../{settings => ui}/RecoveryCodeAdapter.kt | 2 +- .../RecoveryCodeInputFragment.kt | 6 +- .../RecoveryCodeOutputFragment.kt | 2 +- .../{settings => ui}/RecoveryCodeViewModel.kt | 4 +- .../main/res/drawable/ic_cloud_download.xml | 10 ++ .../main/res/drawable/ic_phone_android.xml | 10 ++ ...gs.xml => activity_fragment_container.xml} | 0 .../layout/fragment_recovery_code_input.xml | 2 +- .../main/res/layout/fragment_restore_set.xml | 81 +++++++++++ .../main/res/layout/list_item_restore_set.xml | 45 ++++++ app/src/main/res/values/strings.xml | 8 ++ app/src/main/res/values/themes.xml | 5 + 28 files changed, 640 insertions(+), 147 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt rename app/src/main/java/com/stevesoltys/backup/{settings => ui}/BackupLocationFragment.kt (95%) create mode 100644 app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt rename app/src/main/java/com/stevesoltys/backup/{ => ui}/LiveEvent.kt (91%) rename app/src/main/java/com/stevesoltys/backup/{ => ui}/LiveEventHandler.java (65%) rename app/src/main/java/com/stevesoltys/backup/{ => ui}/MutableLiveEvent.kt (86%) rename app/src/main/java/com/stevesoltys/backup/{settings => ui}/RecoveryCodeActivity.kt (95%) rename app/src/main/java/com/stevesoltys/backup/{settings => ui}/RecoveryCodeAdapter.kt (96%) rename app/src/main/java/com/stevesoltys/backup/{settings => ui}/RecoveryCodeInputFragment.kt (96%) rename app/src/main/java/com/stevesoltys/backup/{settings => ui}/RecoveryCodeOutputFragment.kt (97%) rename app/src/main/java/com/stevesoltys/backup/{settings => ui}/RecoveryCodeViewModel.kt (94%) create mode 100644 app/src/main/res/drawable/ic_cloud_download.xml create mode 100644 app/src/main/res/drawable/ic_phone_android.xml rename app/src/main/res/layout/{activity_settings.xml => activity_fragment_container.xml} (100%) create mode 100644 app/src/main/res/layout/fragment_restore_set.xml create mode 100644 app/src/main/res/layout/list_item_restore_set.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4665b8cb..fa57332b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,9 +32,20 @@ android:exported="true" /> + + + + + + + diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt new file mode 100644 index 00000000..09c73839 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt @@ -0,0 +1,30 @@ +package com.stevesoltys.backup.restore + +import android.os.Bundle +import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.R +import com.stevesoltys.backup.ui.BackupActivity +import com.stevesoltys.backup.ui.BackupViewModel + +class RestoreActivity : BackupActivity() { + + private lateinit var viewModel: RestoreViewModel + + override fun getViewModel(): BackupViewModel = viewModel + + override fun getInitialFragment() = RestoreSetFragment() + + override fun onCreate(savedInstanceState: Bundle?) { + viewModel = ViewModelProviders.of(this).get(RestoreViewModel::class.java) + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_fragment_container) + + if (savedInstanceState == null) showFragment(getInitialFragment()) + } + + override fun onInvalidLocation() { + // TODO alert dialog? + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt new file mode 100644 index 00000000..b3f74c2d --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt @@ -0,0 +1,42 @@ +package com.stevesoltys.backup.restore + +import android.app.backup.RestoreSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.stevesoltys.backup.R +import com.stevesoltys.backup.restore.RestoreSetAdapter.RestoreSetViewHolder + +internal class RestoreSetAdapter( + private val listener: RestoreSetClickListener, + private val items: Array) : Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RestoreSetViewHolder { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_restore_set, parent, false) as View + return RestoreSetViewHolder(v) + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: RestoreSetViewHolder, position: Int) { + holder.bind(items[position]) + } + + inner class RestoreSetViewHolder(private val v: View) : ViewHolder(v) { + + private val titleView = v.findViewById(R.id.titleView) + private val subtitleView = v.findViewById(R.id.subtitleView) + + internal fun bind(item: RestoreSet) { + v.setOnClickListener { listener.onRestoreSetClicked(item) } + titleView.text = item.name + subtitleView.text = item.device + } + + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt new file mode 100644 index 00000000..4b7120b4 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt @@ -0,0 +1,64 @@ +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 +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.R +import kotlinx.android.synthetic.main.fragment_restore_set.* + +class RestoreSetFragment : Fragment(), RestoreSetClickListener { + + private lateinit var viewModel: RestoreViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_restore_set, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProviders.of(requireActivity()).get(RestoreViewModel::class.java) + + viewModel.restoreSets.observe(this, Observer { result -> onRestoreSetsLoaded(result) }) + + backView.setOnClickListener { requireActivity().finishAfterTransition() } + } + + override fun onStart() { + super.onStart() + viewModel.loadRestoreSets() + } + + private fun onRestoreSetsLoaded(result: RestoreSetResult) { + if (result.hasError()) { + errorView.visibility = VISIBLE + listView.visibility = INVISIBLE + progressBar.visibility = INVISIBLE + + errorView.text = result.errorMsg + } else { + errorView.visibility = INVISIBLE + listView.visibility = VISIBLE + progressBar.visibility = INVISIBLE + + listView.adapter = RestoreSetAdapter(this, result.sets) + } + } + + override fun onRestoreSetClicked(set: RestoreSet) { + Log.e("TEST", "RESTORE SET CLICKED: ${set.name} ${set.device} ${set.token}") + } + +} + +internal interface RestoreSetClickListener { + fun onRestoreSetClicked(set: RestoreSet) +} diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt new file mode 100644 index 00000000..cc66dae9 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt @@ -0,0 +1,131 @@ +package com.stevesoltys.backup.restore + +import android.app.Application +import android.app.backup.IRestoreObserver +import android.app.backup.IRestoreSession +import android.app.backup.RestoreSet +import android.net.Uri +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.stevesoltys.backup.Backup +import com.stevesoltys.backup.R +import com.stevesoltys.backup.session.backup.BackupMonitor +import com.stevesoltys.backup.transport.TRANSPORT_ID +import com.stevesoltys.backup.ui.BackupViewModel + +private val TAG = RestoreViewModel::class.java.simpleName + +class RestoreViewModel(app: Application) : BackupViewModel(app) { + + private val backupManager = Backup.backupManager + + private var session: IRestoreSession? = null + private var observer: RestoreObserver? = null + private val monitor = BackupMonitor() + + private val mRestoreSets = MutableLiveData() + internal val restoreSets: LiveData get() = mRestoreSets + + override fun acceptBackupLocation(folderUri: Uri): Boolean { + // TODO + return true + } + + internal fun loadRestoreSets() { + val session = this.session ?: backupManager.beginRestoreSession(null, TRANSPORT_ID) + this.session = session + + if (session == null) { + Log.e(TAG, "beginRestoreSession() returned null session") + mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_error)) + return + } + val observer = this.observer ?: RestoreObserver() + this.observer = observer + + val setResult = session.getAvailableRestoreSets(observer, monitor) + if (setResult != 0) { + Log.e(TAG, "getAvailableRestoreSets() returned non-zero value") + mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_error)) + return + } + } + + override fun onCleared() { + super.onCleared() + endSession() + } + + private fun endSession() { + session?.endRestoreSession() + session = null + observer = null + } + + private inner class RestoreObserver : IRestoreObserver.Stub() { + + /** + * 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 + * [IRestoreSession.getAvailableRestoreSets] method. + * + * @param restoreSets An array of [RestoreSet] objects + * describing all of the available datasets that are candidates for restoring to + * the current device. If no applicable datasets exist, restoreSets will be null. + */ + override fun restoreSetsAvailable(restoreSets: Array?) { + if (restoreSets == null || restoreSets.isEmpty()) { + mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_empty_result)) + } else { + mRestoreSets.postValue(RestoreSetResult(restoreSets)) + } + } + + /** + * The restore operation has begun. + * + * @param numPackages The total number of packages being processed in this restore operation. + */ + override fun restoreStarting(numPackages: Int) { + Log.e(TAG, "RESTORE STARTING $numPackages") + } + + /** + * An indication of which package is being restored currently, + * out of the total number provided in the [restoreStarting] callback. + * This method is not guaranteed to be called. + * + * @param nowBeingRestored The index, between 1 and the numPackages parameter + * to the [restoreStarting] callback, of the package now being restored. + * @param currentPackage The name of the package now being restored. + */ + override fun onUpdate(nowBeingRestored: Int, currentPackage: String) { + Log.e(TAG, "RESTORE UPDATE $nowBeingRestored $currentPackage") + } + + /** + * The restore operation has completed. + * + * @param error 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") + endSession() + } + + } + +} + +internal class RestoreSetResult( + internal val sets: Array, + internal val errorMsg: String?) { + + internal constructor(sets: Array) : this(sets, null) + + internal constructor(errorMsg: String) : this(emptyArray(), errorMsg) + + internal fun hasError(): Boolean = errorMsg != null +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt index 36f52c99..8aefd061 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt @@ -1,85 +1,28 @@ package com.stevesoltys.backup.settings -import android.content.Intent import android.os.Bundle -import android.util.Log -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders -import com.stevesoltys.backup.Backup -import com.stevesoltys.backup.LiveEventHandler import com.stevesoltys.backup.R +import com.stevesoltys.backup.ui.BackupActivity +import com.stevesoltys.backup.ui.BackupViewModel -private val TAG = SettingsActivity::class.java.name - -const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1 -const val REQUEST_CODE_RECOVERY_CODE = 2 - -class SettingsActivity : AppCompatActivity() { +class SettingsActivity : BackupActivity() { private lateinit var viewModel: SettingsViewModel + override fun getViewModel(): BackupViewModel = viewModel + + override fun getInitialFragment() = SettingsFragment() + override fun onCreate(savedInstanceState: Bundle?) { + viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java) super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) - - viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java) - viewModel.onLocationSet.observeEvent(this, LiveEventHandler { initialSetUp -> - if (initialSetUp) showFragment(SettingsFragment()) - else supportFragmentManager.popBackStack() - }) - viewModel.chooseBackupLocation.observeEvent(this, LiveEventHandler { show -> - if (show) showFragment(BackupLocationFragment(), true) - }) + setContentView(R.layout.activity_fragment_container) supportActionBar!!.setDisplayHomeAsUpEnabled(true) - if (savedInstanceState == null) showFragment(SettingsFragment()) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { - if (resultCode != RESULT_OK) { - Log.w(TAG, "Error in activity result: $requestCode") - finishAfterTransition() - } else { - super.onActivityResult(requestCode, resultCode, result) - } - } - - override fun onStart() { - super.onStart() - if (isFinishing) return - - // check that backup is provisioned - if (!viewModel.recoveryCodeIsSet()) { - showRecoveryCodeActivity() - } else if (!viewModel.validLocationIsSet()) { - showFragment(BackupLocationFragment()) - // remove potential error notifications - (application as Backup).notificationManager.onBackupErrorSeen() - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean = when { - item.itemId == android.R.id.home -> { - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } - - private fun showRecoveryCodeActivity() { - val intent = Intent(this, RecoveryCodeActivity::class.java) - startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE) - } - - private fun showFragment(f: Fragment, addToBackStack: Boolean = false) { - val fragmentTransaction = supportFragmentManager.beginTransaction() - .replace(R.id.fragment, f) - if (addToBackStack) fragmentTransaction.addToBackStack(null) - fragmentTransaction.commit() + if (savedInstanceState == null) showFragment(getInitialFragment()) } } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt index e36627d7..9415b38c 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt @@ -1,6 +1,7 @@ package com.stevesoltys.backup.settings import android.content.Context.BACKUP_SERVICE +import android.content.Intent import android.os.Bundle import android.os.RemoteException import android.provider.Settings @@ -17,6 +18,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.TwoStatePreference import com.stevesoltys.backup.Backup import com.stevesoltys.backup.R +import com.stevesoltys.backup.restore.RestoreActivity private val TAG = SettingsFragment::class.java.name @@ -100,7 +102,7 @@ class SettingsFragment : PreferenceFragmentCompat() { true } item.itemId == R.id.action_restore -> { - Toast.makeText(requireContext(), "Not yet implemented", LENGTH_SHORT).show() + startActivity(Intent(requireContext(), RestoreActivity::class.java)) true } else -> super.onOptionsItemSelected(item) diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt index 6840dabd..fadfb8e2 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -1,66 +1,10 @@ package com.stevesoltys.backup.settings import android.app.Application -import android.content.Intent -import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION -import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION -import android.util.Log -import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.AndroidViewModel -import com.stevesoltys.backup.Backup -import com.stevesoltys.backup.LiveEvent -import com.stevesoltys.backup.MutableLiveEvent -import com.stevesoltys.backup.isOnExternalStorage -import com.stevesoltys.backup.transport.ConfigurableBackupTransportService import com.stevesoltys.backup.transport.requestBackup +import com.stevesoltys.backup.ui.BackupViewModel -private val TAG = SettingsViewModel::class.java.simpleName - -class SettingsViewModel(application: Application) : AndroidViewModel(application) { - - private val app = application - - private val locationWasSet = MutableLiveEvent() - /** - * 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 - - private val mChooseBackupLocation = MutableLiveEvent() - internal val chooseBackupLocation: LiveEvent = mChooseBackupLocation - internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true) - - fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey() - - fun validLocationIsSet(): Boolean { - val uri = getBackupFolderUri(app) ?: return false - if (uri.isOnExternalStorage()) return true // might be a temporary failure - val file = DocumentFile.fromTreeUri(app, uri) ?: return false - return file.isDirectory - } - - fun handleChooseFolderResult(result: Intent?) { - val folderUri = result?.data ?: return - - // persist permission to access backup folder across reboots - val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) - app.contentResolver.takePersistableUriPermission(folderUri, takeFlags) - - // check if this is initial set-up or a later change - val initialSetUp = !validLocationIsSet() - - // store backup folder location in settings - setBackupFolderUri(app, folderUri) - - // notify the UI that the location has been set - locationWasSet.setEvent(initialSetUp) - - // stop backup service to be sure the old location will get updated - app.stopService(Intent(app, ConfigurableBackupTransportService::class.java)) - - Log.d(TAG, "New storage location chosen: $folderUri") - } +class SettingsViewModel(app: Application) : BackupViewModel(app) { fun backupNow() = Thread { requestBackup(app) }.start() diff --git a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt index bfc32f0b..bee1f86f 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt +++ b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt @@ -12,6 +12,7 @@ import android.os.ParcelFileDescriptor import android.util.Log import com.stevesoltys.backup.settings.SettingsActivity +val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name const val DEFAULT_RESTORE_SET_TOKEN: Long = 1 private const val TRANSPORT_DIRECTORY_NAME = "com.stevesoltys.backup.transport.ConfigurableBackupTransport" @@ -32,8 +33,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont } override fun name(): String { - // TODO: Make this class non-static in ConfigurableBackupTransportService and use Context and a ComponentName. - return this.javaClass.name + return TRANSPORT_ID } override fun getTransportFlags(): Int { diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt new file mode 100644 index 00000000..3df956fd --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt @@ -0,0 +1,95 @@ +package com.stevesoltys.backup.ui + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.widget.Toast +import android.widget.Toast.LENGTH_LONG +import androidx.annotation.CallSuper +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.stevesoltys.backup.Backup +import com.stevesoltys.backup.R + +const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1 +const val REQUEST_CODE_RECOVERY_CODE = 2 + +private val TAG = BackupActivity::class.java.name + +/** + * An Activity that requires the recovery code and the backup location to be set up + * before starting. + */ +abstract class BackupActivity : AppCompatActivity() { + + protected abstract fun getViewModel(): BackupViewModel + + protected abstract fun getInitialFragment(): Fragment + + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + getViewModel().onLocationSet.observeEvent(this, LiveEventHandler { result -> + if (result.validLocation) { + if (result.initialSetup) showFragment(getInitialFragment()) + else supportFragmentManager.popBackStack() + } else onInvalidLocation() + }) + getViewModel().chooseBackupLocation.observeEvent(this, LiveEventHandler { show -> + if (show) showFragment(BackupLocationFragment(), true) + }) + } + + @CallSuper + override fun onStart() { + super.onStart() + if (isFinishing) return + + // check that backup is provisioned + if (!getViewModel().recoveryCodeIsSet()) { + showRecoveryCodeActivity() + } else if (!getViewModel().validLocationIsSet()) { + showFragment(BackupLocationFragment()) + // remove potential error notifications + (application as Backup).notificationManager.onBackupErrorSeen() + } + } + + @CallSuper + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + if (resultCode != RESULT_OK) { + Log.w(TAG, "Error in activity result: $requestCode") + finishAfterTransition() + } else { + super.onActivityResult(requestCode, resultCode, result) + } + } + + @CallSuper + override fun onOptionsItemSelected(item: MenuItem): Boolean = when { + item.itemId == android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + + private fun showRecoveryCodeActivity() { + val intent = Intent(this, RecoveryCodeActivity::class.java) + startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE) + } + + protected open fun onInvalidLocation() { + Toast.makeText(this, getString(R.string.settings_backup_location_invalid), LENGTH_LONG).show() + } + + protected fun showFragment(f: Fragment, addToBackStack: Boolean = false) { + val fragmentTransaction = supportFragmentManager.beginTransaction() + .replace(R.id.fragment, f) + if (addToBackStack) fragmentTransaction.addToBackStack(null) + fragmentTransaction.commit() + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt similarity index 95% rename from app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt rename to app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt index eab22e01..d58fbff7 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.backup.settings +package com.stevesoltys.backup.ui import android.app.Activity.RESULT_OK import android.content.ActivityNotFoundException @@ -12,8 +12,7 @@ import androidx.lifecycle.ViewModelProviders import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.stevesoltys.backup.R - -private val TAG = BackupLocationFragment::class.java.name +import com.stevesoltys.backup.settings.SettingsViewModel class BackupLocationFragment : PreferenceFragmentCompat() { diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt new file mode 100644 index 00000000..7f622afc --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt @@ -0,0 +1,76 @@ +package com.stevesoltys.backup.ui + +import android.app.Application +import android.content.Intent +import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION +import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION +import android.net.Uri +import android.util.Log +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.AndroidViewModel +import com.stevesoltys.backup.Backup +import com.stevesoltys.backup.isOnExternalStorage +import com.stevesoltys.backup.settings.getBackupFolderUri +import com.stevesoltys.backup.settings.setBackupFolderUri +import com.stevesoltys.backup.transport.ConfigurableBackupTransportService + +private val TAG = BackupViewModel::class.java.simpleName + +abstract class BackupViewModel(protected val app: Application) : AndroidViewModel(app) { + + private val locationWasSet = MutableLiveEvent() + /** + * 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 + + private val mChooseBackupLocation = MutableLiveEvent() + internal val chooseBackupLocation: LiveEvent = mChooseBackupLocation + internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true) + + internal fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey() + + internal fun validLocationIsSet(): Boolean { + val uri = getBackupFolderUri(app) ?: return false + if (uri.isOnExternalStorage()) return true // might be a temporary failure + val file = DocumentFile.fromTreeUri(app, uri) ?: return false + return file.isDirectory + } + + internal fun handleChooseFolderResult(result: Intent?) { + val folderUri = result?.data ?: return + + // persist permission to access backup folder across reboots + val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) + app.contentResolver.takePersistableUriPermission(folderUri, takeFlags) + + // check if this is initial set-up or a later change + val initialSetUp = !validLocationIsSet() + + if (acceptBackupLocation(folderUri)) { + // 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)) + + Log.d(TAG, "New storage location chosen: $folderUri") + } else { + Log.w(TAG, "Location was rejected: $folderUri") + + // notify the UI that the location was invalid + locationWasSet.setEvent(LocationResult(false, initialSetUp)) + } + } + + protected open fun acceptBackupLocation(folderUri: Uri): Boolean { + return true + } + +} + +class LocationResult(val validLocation: Boolean, val initialSetup: Boolean) diff --git a/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt b/app/src/main/java/com/stevesoltys/backup/ui/LiveEvent.kt similarity index 91% rename from app/src/main/java/com/stevesoltys/backup/LiveEvent.kt rename to app/src/main/java/com/stevesoltys/backup/ui/LiveEvent.kt index 83aede27..b01d2840 100644 --- a/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/LiveEvent.kt @@ -1,9 +1,9 @@ -package com.stevesoltys.backup +package com.stevesoltys.backup.ui import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.stevesoltys.backup.LiveEvent.ConsumableEvent +import com.stevesoltys.backup.ui.LiveEvent.ConsumableEvent open class LiveEvent : LiveData>() { diff --git a/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java b/app/src/main/java/com/stevesoltys/backup/ui/LiveEventHandler.java similarity index 65% rename from app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java rename to app/src/main/java/com/stevesoltys/backup/ui/LiveEventHandler.java index 22d86af0..5070ddf4 100644 --- a/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java +++ b/app/src/main/java/com/stevesoltys/backup/ui/LiveEventHandler.java @@ -1,4 +1,4 @@ -package com.stevesoltys.backup; +package com.stevesoltys.backup.ui; public interface LiveEventHandler { void onEvent(T t); diff --git a/app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt b/app/src/main/java/com/stevesoltys/backup/ui/MutableLiveEvent.kt similarity index 86% rename from app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt rename to app/src/main/java/com/stevesoltys/backup/ui/MutableLiveEvent.kt index 7086bd40..ff279f1a 100644 --- a/app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/MutableLiveEvent.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.backup +package com.stevesoltys.backup.ui class MutableLiveEvent : LiveEvent() { diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeActivity.kt similarity index 95% rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeActivity.kt index 0e737f94..69e1673a 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeActivity.kt @@ -1,10 +1,9 @@ -package com.stevesoltys.backup.settings +package com.stevesoltys.backup.ui import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProviders -import com.stevesoltys.backup.LiveEventHandler import com.stevesoltys.backup.R class RecoveryCodeActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeAdapter.kt similarity index 96% rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeAdapter.kt index cc4e009c..28f7ecdd 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeAdapter.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.backup.settings +package com.stevesoltys.backup.ui import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt similarity index 96% rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt index 26918c49..d631ac1b 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.backup.settings +package com.stevesoltys.backup.ui import android.os.Build import android.os.Bundle @@ -57,7 +57,7 @@ class RecoveryCodeInputFragment : Fragment() { } private fun allFilledOut(input: List): Boolean { - for (i in 0 until input.size) { + for (i in input.indices) { if (input[i].isNotEmpty()) continue showError(i, getString(R.string.recovery_code_error_empty_word)) return false @@ -96,7 +96,7 @@ class RecoveryCodeInputFragment : Fragment() { private fun debugPreFill() { val words = viewModel.wordList - for (i in 0 until words.size) { + for (i in words.indices) { getWordLayout(i).editText!!.setText(words[i]) } } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeOutputFragment.kt similarity index 97% rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeOutputFragment.kt index 724cb5a1..03297b53 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeOutputFragment.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.backup.settings +package com.stevesoltys.backup.ui import android.content.res.Configuration import android.os.Bundle diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeViewModel.kt similarity index 94% rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeViewModel.kt index 0083e9a0..a4653e0a 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeViewModel.kt @@ -1,10 +1,8 @@ -package com.stevesoltys.backup.settings +package com.stevesoltys.backup.ui import android.app.Application import androidx.lifecycle.AndroidViewModel import com.stevesoltys.backup.Backup -import com.stevesoltys.backup.LiveEvent -import com.stevesoltys.backup.MutableLiveEvent import io.github.novacrypto.bip39.* import io.github.novacrypto.bip39.Validation.InvalidChecksumException import io.github.novacrypto.bip39.Validation.InvalidWordCountException diff --git a/app/src/main/res/drawable/ic_cloud_download.xml b/app/src/main/res/drawable/ic_cloud_download.xml new file mode 100644 index 00000000..6a3f5f94 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_phone_android.xml b/app/src/main/res/drawable/ic_phone_android.xml new file mode 100644 index 00000000..c3cf49d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone_android.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_fragment_container.xml similarity index 100% rename from app/src/main/res/layout/activity_settings.xml rename to app/src/main/res/layout/activity_fragment_container.xml diff --git a/app/src/main/res/layout/fragment_recovery_code_input.xml b/app/src/main/res/layout/fragment_recovery_code_input.xml index 1442389a..b478d509 100644 --- a/app/src/main/res/layout/fragment_recovery_code_input.xml +++ b/app/src/main/res/layout/fragment_recovery_code_input.xml @@ -9,7 +9,7 @@ + tools:context=".ui.RecoveryCodeInputFragment"> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_restore_set.xml b/app/src/main/res/layout/list_item_restore_set.xml new file mode 100644 index 00000000..a05234a9 --- /dev/null +++ b/app/src/main/res/layout/list_item_restore_set.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dce1cdd0..2a5a3668 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Choose backup location Backup Location Choose where to store your backups. More options might get added in the future. + The chosen location can not be used. External Storage All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code. Automatic restore @@ -70,4 +71,11 @@ A device backup failed to run. Fix + + Restore from Backup + Choose a backup to restore + Don\'t restore + An error occurred loading the backups. + No backups found at given location. + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index cd538f6f..96232824 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -4,4 +4,9 @@ @style/PreferenceThemeOverlay + + From 1a7fdfa59ab25d2d0cef55aedd56092dfdf96b43 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 6 Sep 2019 12:36:51 -0300 Subject: [PATCH 03/17] 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 @@ + + + + + + + + + + + + + + + + + +