From 9cede639f3548d3f7ca9a926d987dda820c33f1e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 11 Sep 2019 17:26:03 -0300 Subject: [PATCH] When restoring, ask for location first and then restore code This also checks if there's a backup at the chosen location and requires the user to select another once, if we can not find a backup. --- .../backup/restore/RestoreActivity.kt | 25 +++++++- .../backup/restore/RestoreViewModel.kt | 45 ++++++++++++- .../backup/settings/SettingsActivity.kt | 20 +++++- .../backup/settings/SettingsViewModel.kt | 50 +++++++++++++++ .../backup/plugins/DocumentsStorage.kt | 4 +- .../plugins/DocumentsProviderRestorePlugin.kt | 63 ++++++++++--------- .../stevesoltys/backup/ui/BackupActivity.kt | 24 +------ .../stevesoltys/backup/ui/BackupViewModel.kt | 59 ++--------------- app/src/main/res/values/strings.xml | 2 + 9 files changed, 180 insertions(+), 112 deletions(-) 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 83f26664..1c0689d2 100644 --- a/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt @@ -1,10 +1,14 @@ package com.stevesoltys.backup.restore import android.os.Bundle +import androidx.annotation.CallSuper +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.stevesoltys.backup.R +import com.stevesoltys.backup.transport.backup.plugins.DIRECTORY_ROOT import com.stevesoltys.backup.ui.BackupActivity +import com.stevesoltys.backup.ui.BackupLocationFragment import com.stevesoltys.backup.ui.BackupViewModel class RestoreActivity : BackupActivity() { @@ -15,8 +19,6 @@ class RestoreActivity : BackupActivity() { override fun getInitialFragment() = RestoreSetFragment() - override fun isRestoreOperation() = true - override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this).get(RestoreViewModel::class.java) super.onCreate(savedInstanceState) @@ -32,8 +34,25 @@ class RestoreActivity : BackupActivity() { } } + @CallSuper + override fun onStart() { + super.onStart() + if (isFinishing) return + + // check that backup is provisioned + if (!viewModel.validLocationIsSet()) { + showFragment(BackupLocationFragment()) + } else if (!viewModel.recoveryCodeIsSet()) { + showRecoveryCodeActivity() + } + } + override fun onInvalidLocation() { - // TODO alert dialog? + AlertDialog.Builder(this) + .setTitle(getString(R.string.restore_invalid_location_title)) + .setMessage(getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT)) + .setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() } + .show() } } 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 d6d5e0d0..24a1a2c3 100644 --- a/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt @@ -4,16 +4,23 @@ import android.app.Application import android.app.backup.IRestoreObserver import android.app.backup.IRestoreSession import android.app.backup.RestoreSet +import android.content.Intent import android.net.Uri import android.util.Log import androidx.annotation.WorkerThread +import androidx.documentfile.provider.DocumentFile 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.settings.setBackupFolderUri +import com.stevesoltys.backup.transport.ConfigurableBackupTransportService import com.stevesoltys.backup.transport.TRANSPORT_ID +import com.stevesoltys.backup.transport.backup.plugins.DIRECTORY_ROOT +import com.stevesoltys.backup.transport.restore.plugins.DocumentsProviderRestorePlugin.Companion.getBackups import com.stevesoltys.backup.ui.BackupViewModel +import com.stevesoltys.backup.ui.LocationResult private val TAG = RestoreViewModel::class.java.simpleName @@ -21,6 +28,8 @@ class RestoreViewModel(app: Application) : BackupViewModel(app), RestoreSetClick private val backupManager = Backup.backupManager + override val isRestoreOperation = true + private var session: IRestoreSession? = null private var observer: RestoreObserver? = null private val monitor = BackupMonitor() @@ -41,9 +50,39 @@ class RestoreViewModel(app: Application) : BackupViewModel(app), RestoreSetClick // 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 search if there's really a backup available in this location and see if we can decrypt it - return true + override fun onLocationSet(folderUri: Uri, isInitialSetup: Boolean) { + if (hasBackup(folderUri)) { + // store backup folder location in settings + setBackupFolderUri(app, folderUri) + + // 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") + + mLocationSet.setEvent(LocationResult(false, isInitialSetup)) + } else { + Log.w(TAG, "Location was rejected: $folderUri") + + // notify the UI that the location was invalid + mLocationSet.setEvent(LocationResult(false, isInitialSetup)) + } + } + + /** + * Searches if there's really a backup available in the given location. + * Returns true if at least one was found and false otherwise. + * + * This method is not plugin-agnostic and breaks encapsulation. + * It is specific to the (currently only) DocumentsProvider plugin. + * + * TODO maybe move this to the RestoreCoordinator once we can inject it + */ + private fun hasBackup(folderUri: Uri): Boolean { + val parent = DocumentFile.fromTreeUri(app, folderUri) ?: throw AssertionError() + val rootDir = parent.findFile(DIRECTORY_ROOT) ?: return false + val backupSets = getBackups(rootDir) + return backupSets.isNotEmpty() } internal fun loadRestoreSets() { 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 11b2ae4f..bb86976b 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt @@ -1,9 +1,12 @@ package com.stevesoltys.backup.settings import android.os.Bundle +import androidx.annotation.CallSuper import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.Backup import com.stevesoltys.backup.R import com.stevesoltys.backup.ui.BackupActivity +import com.stevesoltys.backup.ui.BackupLocationFragment import com.stevesoltys.backup.ui.BackupViewModel class SettingsActivity : BackupActivity() { @@ -14,8 +17,6 @@ class SettingsActivity : BackupActivity() { override fun getInitialFragment() = SettingsFragment() - override fun isRestoreOperation() = false - override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java) super.onCreate(savedInstanceState) @@ -27,4 +28,19 @@ class SettingsActivity : BackupActivity() { if (savedInstanceState == null) showFragment(getInitialFragment()) } + @CallSuper + 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() + } + } + } 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 fadfb8e2..dcd10a37 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -1,11 +1,61 @@ package com.stevesoltys.backup.settings import android.app.Application +import android.app.backup.BackupProgress +import android.app.backup.IBackupObserver +import android.content.Intent +import android.net.Uri +import android.util.Log +import androidx.annotation.WorkerThread +import com.stevesoltys.backup.Backup +import com.stevesoltys.backup.transport.ConfigurableBackupTransportService +import com.stevesoltys.backup.transport.TRANSPORT_ID import com.stevesoltys.backup.transport.requestBackup import com.stevesoltys.backup.ui.BackupViewModel +import com.stevesoltys.backup.ui.LocationResult + +private val TAG = SettingsViewModel::class.java.simpleName class SettingsViewModel(app: Application) : BackupViewModel(app) { + override val isRestoreOperation = false + fun backupNow() = Thread { requestBackup(app) }.start() + override fun onLocationSet(folderUri: Uri, isInitialSetup: Boolean) { + // store backup folder location in settings + setBackupFolderUri(app, folderUri) + + // 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") + + // initialize the new location + val observer = InitializationObserver(isInitialSetup) + Backup.backupManager.initializeTransports(arrayOf(TRANSPORT_ID), observer) + } + + @WorkerThread + private inner class InitializationObserver(private val initialSetUp: Boolean) : IBackupObserver.Stub() { + override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) { + // noop + } + override fun onResult(target: String, status: Int) { + // noop + } + override fun backupFinished(status: Int) { + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i(TAG, "Initialization finished. Status: $status") + } + if (status == 0) { + // notify the UI that the location has been set + mLocationSet.postEvent(LocationResult(true, initialSetUp)) + } else { + // notify the UI that the location was invalid + mLocationSet.postEvent(LocationResult(false, initialSetUp)) + } + } + } + } diff --git a/app/src/main/java/com/stevesoltys/backup/transport/backup/plugins/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/backup/transport/backup/plugins/DocumentsStorage.kt index 9e6d686b..a5f3882d 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/backup/plugins/DocumentsStorage.kt +++ b/app/src/main/java/com/stevesoltys/backup/transport/backup/plugins/DocumentsStorage.kt @@ -10,11 +10,11 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream +const val DIRECTORY_ROOT = ".AndroidBackup" const val DIRECTORY_FULL_BACKUP = "full" const val DIRECTORY_KEY_VALUE_BACKUP = "kv" const val FILE_BACKUP_METADATA = ".backup.metadata" const val FILE_NO_MEDIA = ".nomedia" -private const val ROOT_DIR_NAME = ".AndroidBackup" private const val MIME_TYPE = "application/octet-stream" private val TAG = DocumentsStorage::class.java.simpleName @@ -26,7 +26,7 @@ class DocumentsStorage(private val context: Context, parentFolder: Uri?, token: // [fromTreeUri] should only return null when SDK_INT < 21 val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError() try { - val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME) + val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT) // create .nomedia file to prevent Android's MediaScanner from trying to index the backup rootDir.createOrGetFile(FILE_NO_MEDIA) rootDir 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 3372cfb5..5b852e20 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 @@ -25,41 +25,48 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re override fun getAvailableBackups(): Sequence? { val rootDir = storage.rootBackupDir ?: return null - val files = ArrayList>() - for (set in rootDir.listFiles()) { - if (!set.isDirectory || set.name == null) { - if (set.name != FILE_NO_MEDIA) { - Log.w(TAG, "Found invalid backup set folder: ${set.name}") - } - continue - } - val token = try { - set.name!!.toLong() - } catch (e: NumberFormatException) { - Log.w(TAG, "Found invalid backup set folder: ${set.name}", e) - continue - } - val metadata = set.findFile(FILE_BACKUP_METADATA) - if (metadata == null) { - Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}") - } else { - files.add(Pair(token, metadata)) - } - } - val iterator = files.iterator() + val backupSets = getBackups(rootDir) + val iterator = backupSets.iterator() return generateSequence { if (!iterator.hasNext()) return@generateSequence null // end sequence - val pair = iterator.next() - val token = pair.first - val metadata = pair.second + val backupSet = iterator.next() try { - val stream = storage.getInputStream(metadata) - EncryptedBackupMetadata(token, stream) + val stream = storage.getInputStream(backupSet.metadataFile) + EncryptedBackupMetadata(backupSet.token, stream) } catch (e: IOException) { Log.e(TAG, "Error getting InputStream for backup metadata.", e) - EncryptedBackupMetadata(token) + EncryptedBackupMetadata(backupSet.token) } } } + companion object { + fun getBackups(rootDir: DocumentFile): List { + val backupSets = ArrayList() + for (set in rootDir.listFiles()) { + if (!set.isDirectory || set.name == null) { + if (set.name != FILE_NO_MEDIA) { + Log.w(TAG, "Found invalid backup set folder: ${set.name}") + } + continue + } + val token = try { + set.name!!.toLong() + } catch (e: NumberFormatException) { + Log.w(TAG, "Found invalid backup set folder: ${set.name}", e) + continue + } + val metadata = set.findFile(FILE_BACKUP_METADATA) + if (metadata == null) { + Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}") + } else { + backupSets.add(BackupSet(token, metadata)) + } + } + return backupSets + } + } + } + +class BackupSet(val token: Long, val metadataFile: DocumentFile) 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 e01d88a4..5880013a 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt @@ -9,7 +9,6 @@ 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 @@ -27,13 +26,11 @@ abstract class BackupActivity : AppCompatActivity() { protected abstract fun getInitialFragment(): Fragment - protected abstract fun isRestoreOperation(): Boolean - @CallSuper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - getViewModel().onLocationSet.observeEvent(this, LiveEventHandler { result -> + getViewModel().locationSet.observeEvent(this, LiveEventHandler { result -> if (result.validLocation) { if (result.initialSetup) showFragment(getInitialFragment()) else supportFragmentManager.popBackStack() @@ -44,21 +41,6 @@ abstract class BackupActivity : AppCompatActivity() { }) } - @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 && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) { @@ -80,9 +62,9 @@ abstract class BackupActivity : AppCompatActivity() { else -> super.onOptionsItemSelected(item) } - private fun showRecoveryCodeActivity() { + protected fun showRecoveryCodeActivity() { val intent = Intent(this, RecoveryCodeActivity::class.java) - intent.putExtra(INTENT_EXTRA_IS_RESTORE, isRestoreOperation()) + intent.putExtra(INTENT_EXTRA_IS_RESTORE, getViewModel().isRestoreOperation) startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE) } 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 0565285e..25cb74ed 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt @@ -1,32 +1,26 @@ package com.stevesoltys.backup.ui import android.app.Application -import android.app.backup.BackupProgress -import android.app.backup.IBackupObserver 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 -import com.stevesoltys.backup.transport.TRANSPORT_ID private val TAG = BackupViewModel::class.java.simpleName abstract class BackupViewModel(protected val app: Application) : AndroidViewModel(app) { - private val locationWasSet = MutableLiveEvent() + protected val mLocationSet = 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 get() = locationWasSet + internal val locationSet: LiveEvent get() = mLocationSet private val mChooseBackupLocation = MutableLiveEvent() internal val chooseBackupLocation: LiveEvent get() = mChooseBackupLocation @@ -41,6 +35,8 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode return file.isDirectory } + abstract val isRestoreOperation: Boolean + internal fun handleChooseFolderResult(result: Intent?) { val folderUri = result?.data ?: return @@ -48,53 +44,10 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode 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) - - // 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") - - // initialize the new location - // TODO don't do this when restoring - Backup.backupManager.initializeTransports(arrayOf(TRANSPORT_ID), InitializationObserver(initialSetUp)) - } else { - Log.w(TAG, "Location was rejected: $folderUri") - - // notify the UI that the location was invalid - locationWasSet.setEvent(LocationResult(false, initialSetUp)) - } + onLocationSet(folderUri, !validLocationIsSet()) } - protected open fun acceptBackupLocation(folderUri: Uri): Boolean { - return true - } - - private inner class InitializationObserver(private val initialSetUp: Boolean) : IBackupObserver.Stub() { - override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) { - // noop - } - override fun onResult(target: String, status: Int) { - // noop - } - override fun backupFinished(status: Int) { - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i(TAG, "Initialization finished. Status: $status") - } - if (status == 0) { - // notify the UI that the location has been set - locationWasSet.postEvent(LocationResult(true, initialSetUp)) - } else { - // notify the UI that the location was invalid - locationWasSet.postEvent(LocationResult(false, initialSetUp)) - } - } - } + abstract fun onLocationSet(folderUri: Uri, isInitialSetup: Boolean) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e277ffd3..5ab5803c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,6 +75,8 @@ Restore from Backup Choose a backup to restore Don\'t restore + No backups found + We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder. An error occurred loading the backups. No backups found at given location. Restoring Backup