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.
This commit is contained in:
parent
af43c6154d
commit
9cede639f3
9 changed files with 180 additions and 112 deletions
|
@ -1,10 +1,14 @@
|
||||||
package com.stevesoltys.backup.restore
|
package com.stevesoltys.backup.restore
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import com.stevesoltys.backup.R
|
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.BackupActivity
|
||||||
|
import com.stevesoltys.backup.ui.BackupLocationFragment
|
||||||
import com.stevesoltys.backup.ui.BackupViewModel
|
import com.stevesoltys.backup.ui.BackupViewModel
|
||||||
|
|
||||||
class RestoreActivity : BackupActivity() {
|
class RestoreActivity : BackupActivity() {
|
||||||
|
@ -15,8 +19,6 @@ class RestoreActivity : BackupActivity() {
|
||||||
|
|
||||||
override fun getInitialFragment() = RestoreSetFragment()
|
override fun getInitialFragment() = RestoreSetFragment()
|
||||||
|
|
||||||
override fun isRestoreOperation() = true
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
viewModel = ViewModelProviders.of(this).get(RestoreViewModel::class.java)
|
viewModel = ViewModelProviders.of(this).get(RestoreViewModel::class.java)
|
||||||
super.onCreate(savedInstanceState)
|
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() {
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,23 @@ import android.app.Application
|
||||||
import android.app.backup.IRestoreObserver
|
import android.app.backup.IRestoreObserver
|
||||||
import android.app.backup.IRestoreSession
|
import android.app.backup.IRestoreSession
|
||||||
import android.app.backup.RestoreSet
|
import android.app.backup.RestoreSet
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.stevesoltys.backup.Backup
|
import com.stevesoltys.backup.Backup
|
||||||
import com.stevesoltys.backup.R
|
import com.stevesoltys.backup.R
|
||||||
import com.stevesoltys.backup.session.backup.BackupMonitor
|
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.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.BackupViewModel
|
||||||
|
import com.stevesoltys.backup.ui.LocationResult
|
||||||
|
|
||||||
private val TAG = RestoreViewModel::class.java.simpleName
|
private val TAG = RestoreViewModel::class.java.simpleName
|
||||||
|
|
||||||
|
@ -21,6 +28,8 @@ class RestoreViewModel(app: Application) : BackupViewModel(app), RestoreSetClick
|
||||||
|
|
||||||
private val backupManager = Backup.backupManager
|
private val backupManager = Backup.backupManager
|
||||||
|
|
||||||
|
override val isRestoreOperation = true
|
||||||
|
|
||||||
private var session: IRestoreSession? = null
|
private var session: IRestoreSession? = null
|
||||||
private var observer: RestoreObserver? = null
|
private var observer: RestoreObserver? = null
|
||||||
private val monitor = BackupMonitor()
|
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.
|
// Zero on success; a nonzero error code if the restore operation as a whole failed.
|
||||||
internal val restoreFinished: LiveData<Int> get() = mRestoreFinished
|
internal val restoreFinished: LiveData<Int> get() = mRestoreFinished
|
||||||
|
|
||||||
override fun acceptBackupLocation(folderUri: Uri): Boolean {
|
override fun onLocationSet(folderUri: Uri, isInitialSetup: Boolean) {
|
||||||
// TODO search if there's really a backup available in this location and see if we can decrypt it
|
if (hasBackup(folderUri)) {
|
||||||
return true
|
// 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() {
|
internal fun loadRestoreSets() {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package com.stevesoltys.backup.settings
|
package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import com.stevesoltys.backup.Backup
|
||||||
import com.stevesoltys.backup.R
|
import com.stevesoltys.backup.R
|
||||||
import com.stevesoltys.backup.ui.BackupActivity
|
import com.stevesoltys.backup.ui.BackupActivity
|
||||||
|
import com.stevesoltys.backup.ui.BackupLocationFragment
|
||||||
import com.stevesoltys.backup.ui.BackupViewModel
|
import com.stevesoltys.backup.ui.BackupViewModel
|
||||||
|
|
||||||
class SettingsActivity : BackupActivity() {
|
class SettingsActivity : BackupActivity() {
|
||||||
|
@ -14,8 +17,6 @@ class SettingsActivity : BackupActivity() {
|
||||||
|
|
||||||
override fun getInitialFragment() = SettingsFragment()
|
override fun getInitialFragment() = SettingsFragment()
|
||||||
|
|
||||||
override fun isRestoreOperation() = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
|
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -27,4 +28,19 @@ class SettingsActivity : BackupActivity() {
|
||||||
if (savedInstanceState == null) showFragment(getInitialFragment())
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,61 @@
|
||||||
package com.stevesoltys.backup.settings
|
package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
import android.app.Application
|
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.transport.requestBackup
|
||||||
import com.stevesoltys.backup.ui.BackupViewModel
|
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) {
|
class SettingsViewModel(app: Application) : BackupViewModel(app) {
|
||||||
|
|
||||||
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
fun backupNow() = Thread { requestBackup(app) }.start()
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
const val DIRECTORY_ROOT = ".AndroidBackup"
|
||||||
const val DIRECTORY_FULL_BACKUP = "full"
|
const val DIRECTORY_FULL_BACKUP = "full"
|
||||||
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
||||||
const val FILE_BACKUP_METADATA = ".backup.metadata"
|
const val FILE_BACKUP_METADATA = ".backup.metadata"
|
||||||
const val FILE_NO_MEDIA = ".nomedia"
|
const val FILE_NO_MEDIA = ".nomedia"
|
||||||
private const val ROOT_DIR_NAME = ".AndroidBackup"
|
|
||||||
private const val MIME_TYPE = "application/octet-stream"
|
private const val MIME_TYPE = "application/octet-stream"
|
||||||
|
|
||||||
private val TAG = DocumentsStorage::class.java.simpleName
|
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
|
// [fromTreeUri] should only return null when SDK_INT < 21
|
||||||
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
|
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
|
||||||
try {
|
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
|
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup
|
||||||
rootDir.createOrGetFile(FILE_NO_MEDIA)
|
rootDir.createOrGetFile(FILE_NO_MEDIA)
|
||||||
rootDir
|
rootDir
|
||||||
|
|
|
@ -25,41 +25,48 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re
|
||||||
|
|
||||||
override fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
|
override fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
|
||||||
val rootDir = storage.rootBackupDir ?: return null
|
val rootDir = storage.rootBackupDir ?: return null
|
||||||
val files = ArrayList<Pair<Long, DocumentFile>>()
|
val backupSets = getBackups(rootDir)
|
||||||
for (set in rootDir.listFiles()) {
|
val iterator = backupSets.iterator()
|
||||||
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()
|
|
||||||
return generateSequence {
|
return generateSequence {
|
||||||
if (!iterator.hasNext()) return@generateSequence null // end sequence
|
if (!iterator.hasNext()) return@generateSequence null // end sequence
|
||||||
val pair = iterator.next()
|
val backupSet = iterator.next()
|
||||||
val token = pair.first
|
|
||||||
val metadata = pair.second
|
|
||||||
try {
|
try {
|
||||||
val stream = storage.getInputStream(metadata)
|
val stream = storage.getInputStream(backupSet.metadataFile)
|
||||||
EncryptedBackupMetadata(token, stream)
|
EncryptedBackupMetadata(backupSet.token, stream)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error getting InputStream for backup metadata.", e)
|
Log.e(TAG, "Error getting InputStream for backup metadata.", e)
|
||||||
EncryptedBackupMetadata(token)
|
EncryptedBackupMetadata(backupSet.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getBackups(rootDir: DocumentFile): List<BackupSet> {
|
||||||
|
val backupSets = ArrayList<BackupSet>()
|
||||||
|
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)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.widget.Toast.LENGTH_LONG
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.stevesoltys.backup.Backup
|
|
||||||
import com.stevesoltys.backup.R
|
import com.stevesoltys.backup.R
|
||||||
|
|
||||||
const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1
|
const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1
|
||||||
|
@ -27,13 +26,11 @@ abstract class BackupActivity : AppCompatActivity() {
|
||||||
|
|
||||||
protected abstract fun getInitialFragment(): Fragment
|
protected abstract fun getInitialFragment(): Fragment
|
||||||
|
|
||||||
protected abstract fun isRestoreOperation(): Boolean
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
getViewModel().onLocationSet.observeEvent(this, LiveEventHandler { result ->
|
getViewModel().locationSet.observeEvent(this, LiveEventHandler { result ->
|
||||||
if (result.validLocation) {
|
if (result.validLocation) {
|
||||||
if (result.initialSetup) showFragment(getInitialFragment())
|
if (result.initialSetup) showFragment(getInitialFragment())
|
||||||
else supportFragmentManager.popBackStack()
|
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
|
@CallSuper
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
|
||||||
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
|
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
|
||||||
|
@ -80,9 +62,9 @@ abstract class BackupActivity : AppCompatActivity() {
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showRecoveryCodeActivity() {
|
protected fun showRecoveryCodeActivity() {
|
||||||
val intent = Intent(this, RecoveryCodeActivity::class.java)
|
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)
|
startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,26 @@
|
||||||
package com.stevesoltys.backup.ui
|
package com.stevesoltys.backup.ui
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.backup.BackupProgress
|
|
||||||
import android.app.backup.IBackupObserver
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.stevesoltys.backup.Backup
|
import com.stevesoltys.backup.Backup
|
||||||
import com.stevesoltys.backup.isOnExternalStorage
|
import com.stevesoltys.backup.isOnExternalStorage
|
||||||
import com.stevesoltys.backup.settings.getBackupFolderUri
|
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
|
private val TAG = BackupViewModel::class.java.simpleName
|
||||||
|
|
||||||
abstract class BackupViewModel(protected val app: Application) : AndroidViewModel(app) {
|
abstract class BackupViewModel(protected val app: Application) : AndroidViewModel(app) {
|
||||||
|
|
||||||
private val locationWasSet = MutableLiveEvent<LocationResult>()
|
protected val mLocationSet = MutableLiveEvent<LocationResult>()
|
||||||
/**
|
/**
|
||||||
* Will be set to true if this is the initial location.
|
* Will be set to true if this is the initial location.
|
||||||
* It will be false if an existing location was changed.
|
* It will be false if an existing location was changed.
|
||||||
*/
|
*/
|
||||||
internal val onLocationSet: LiveEvent<LocationResult> get() = locationWasSet
|
internal val locationSet: LiveEvent<LocationResult> get() = mLocationSet
|
||||||
|
|
||||||
private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
|
private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
|
||||||
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
||||||
|
@ -41,6 +35,8 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
|
||||||
return file.isDirectory
|
return file.isDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract val isRestoreOperation: Boolean
|
||||||
|
|
||||||
internal fun handleChooseFolderResult(result: Intent?) {
|
internal fun handleChooseFolderResult(result: Intent?) {
|
||||||
val folderUri = result?.data ?: return
|
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)
|
val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
app.contentResolver.takePersistableUriPermission(folderUri, takeFlags)
|
app.contentResolver.takePersistableUriPermission(folderUri, takeFlags)
|
||||||
|
|
||||||
// check if this is initial set-up or a later change
|
onLocationSet(folderUri, !validLocationIsSet())
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun acceptBackupLocation(folderUri: Uri): Boolean {
|
abstract fun onLocationSet(folderUri: Uri, isInitialSetup: 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,8 @@
|
||||||
<string name="restore_title">Restore from Backup</string>
|
<string name="restore_title">Restore from Backup</string>
|
||||||
<string name="restore_choose_restore_set">Choose a backup to restore</string>
|
<string name="restore_choose_restore_set">Choose a backup to restore</string>
|
||||||
<string name="restore_back">Don\'t restore</string>
|
<string name="restore_back">Don\'t restore</string>
|
||||||
|
<string name="restore_invalid_location_title">No backups found</string>
|
||||||
|
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
||||||
<string name="restore_set_error">An error occurred loading the backups.</string>
|
<string name="restore_set_error">An error occurred loading the backups.</string>
|
||||||
<string name="restore_set_empty_result">No backups found at given location.</string>
|
<string name="restore_set_empty_result">No backups found at given location.</string>
|
||||||
<string name="restore_restoring">Restoring Backup</string>
|
<string name="restore_restoring">Restoring Backup</string>
|
||||||
|
|
Loading…
Reference in a new issue