Get rid of device folders, use unix epoch as backup token and store it
This commit is contained in:
parent
8b6656a350
commit
af43c6154d
22 changed files with 160 additions and 103 deletions
|
@ -106,10 +106,10 @@ dependencies {
|
|||
implementation 'commons-io:commons-io:2.6'
|
||||
implementation 'io.github.novacrypto:BIP39:2019.01.27'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.0.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.0.0'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.stevesoltys.backup.settings.getBackupFolderUri
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import com.stevesoltys.backup.transport.backup.plugins.DocumentsStorage
|
||||
import com.stevesoltys.backup.transport.backup.plugins.createOrGetFile
|
||||
import org.junit.After
|
||||
|
@ -20,9 +21,9 @@ private const val filename = "test-file"
|
|||
class DocumentsStorageTest {
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val token = getBackupToken(context)
|
||||
private val folderUri = getBackupFolderUri(context)
|
||||
private val deviceName = "device name"
|
||||
private val storage = DocumentsStorage(context, folderUri, deviceName)
|
||||
private val storage = DocumentsStorage(context, folderUri, token)
|
||||
|
||||
private lateinit var file: DocumentFile
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@ package com.stevesoltys.backup.metadata
|
|||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import com.stevesoltys.backup.header.VERSION
|
||||
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||
import java.io.InputStream
|
||||
|
||||
data class BackupMetadata(
|
||||
internal val version: Byte = VERSION,
|
||||
internal val token: Long = DEFAULT_RESTORE_SET_TOKEN,
|
||||
internal val token: Long,
|
||||
internal val androidVersion: Int = SDK_INT,
|
||||
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
)
|
||||
|
@ -22,5 +21,8 @@ class FormatException(cause: Throwable) : Exception(cause)
|
|||
|
||||
class EncryptedBackupMetadata private constructor(val token: Long, val inputStream: InputStream?, val error: Boolean) {
|
||||
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
|
||||
constructor(token: Long, error: Boolean) : this(token, null, false)
|
||||
/**
|
||||
* Indicates that there was an error retrieving the encrypted backup metadata.
|
||||
*/
|
||||
constructor(token: Long) : this(token, null, true)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.stevesoltys.backup.metadata
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import com.stevesoltys.backup.Utf8
|
||||
import com.stevesoltys.backup.crypto.Crypto
|
||||
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
@ -11,7 +10,7 @@ import java.io.OutputStream
|
|||
interface MetadataWriter {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(outputStream: OutputStream, token: Long = DEFAULT_RESTORE_SET_TOKEN)
|
||||
fun write(outputStream: OutputStream, token: Long)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,8 @@ import android.util.Log
|
|||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_SHORT
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.TwoStatePreference
|
||||
|
@ -37,7 +36,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
|
||||
|
||||
backup = findPreference("backup") as TwoStatePreference
|
||||
backup = findPreference("backup")!!
|
||||
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||
val enabled = newValue as Boolean
|
||||
try {
|
||||
|
@ -50,13 +49,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
val backupLocation = findPreference("backup_location")
|
||||
val backupLocation = findPreference<Preference>("backup_location")!!
|
||||
backupLocation.setOnPreferenceClickListener {
|
||||
viewModel.chooseBackupLocation()
|
||||
true
|
||||
}
|
||||
|
||||
autoRestore = findPreference("auto_restore") as TwoStatePreference
|
||||
autoRestore = findPreference("auto_restore")!!
|
||||
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||
val enabled = newValue as Boolean
|
||||
try {
|
||||
|
|
|
@ -3,8 +3,10 @@ package com.stevesoltys.backup.settings
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.preference.PreferenceManager.getDefaultSharedPreferences
|
||||
import java.util.*
|
||||
|
||||
private const val PREF_KEY_BACKUP_URI = "backupUri"
|
||||
private const val PREF_KEY_BACKUP_TOKEN = "backupToken"
|
||||
private const val PREF_KEY_DEVICE_NAME = "deviceName"
|
||||
private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"
|
||||
|
||||
|
@ -21,6 +23,24 @@ fun getBackupFolderUri(context: Context): Uri? {
|
|||
return Uri.parse(uriStr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns a new backup token while saving it as well.
|
||||
* Subsequent calls to [getBackupToken] will return this new token once saved.
|
||||
*/
|
||||
fun getAndSaveNewBackupToken(context: Context): Long = Date().time.apply {
|
||||
getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putLong(PREF_KEY_BACKUP_TOKEN, this)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current backup token or 0 if none exists.
|
||||
*/
|
||||
fun getBackupToken(context: Context): Long {
|
||||
return getDefaultSharedPreferences(context).getLong(PREF_KEY_BACKUP_TOKEN, 0L)
|
||||
}
|
||||
|
||||
fun setDeviceName(context: Context, name: String) {
|
||||
getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
|
|
|
@ -13,7 +13,6 @@ 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"
|
||||
private val TAG = ConfigurableBackupTransport::class.java.simpleName
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.stevesoltys.backup.header.HeaderWriterImpl
|
|||
import com.stevesoltys.backup.metadata.MetadataReaderImpl
|
||||
import com.stevesoltys.backup.metadata.MetadataWriterImpl
|
||||
import com.stevesoltys.backup.settings.getBackupFolderUri
|
||||
import com.stevesoltys.backup.settings.getDeviceName
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import com.stevesoltys.backup.transport.backup.BackupCoordinator
|
||||
import com.stevesoltys.backup.transport.backup.FullBackup
|
||||
import com.stevesoltys.backup.transport.backup.InputFactory
|
||||
|
@ -26,7 +26,7 @@ class PluginManager(context: Context) {
|
|||
|
||||
// We can think about using an injection framework such as Dagger to simplify this.
|
||||
|
||||
private val storage = DocumentsStorage(context, getBackupFolderUri(context), getDeviceName(context)!!)
|
||||
private val storage = DocumentsStorage(context, getBackupFolderUri(context), getBackupToken(context))
|
||||
|
||||
private val headerWriter = HeaderWriterImpl()
|
||||
private val headerReader = HeaderReaderImpl()
|
||||
|
@ -42,7 +42,7 @@ class PluginManager(context: Context) {
|
|||
private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto)
|
||||
private val notificationManager = (context.applicationContext as Backup).notificationManager
|
||||
|
||||
internal val backupCoordinator = BackupCoordinator(backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager)
|
||||
internal val backupCoordinator = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager)
|
||||
|
||||
|
||||
private val restorePlugin = DocumentsProviderRestorePlugin(storage)
|
||||
|
@ -50,6 +50,6 @@ class PluginManager(context: Context) {
|
|||
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto)
|
||||
private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, outputFactory, headerReader, crypto)
|
||||
|
||||
internal val restoreCoordinator = RestoreCoordinator(restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
internal val restoreCoordinator = RestoreCoordinator(context, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@ package com.stevesoltys.backup.transport.backup
|
|||
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.BackupNotificationManager
|
||||
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = BackupCoordinator::class.java.simpleName
|
||||
|
@ -16,6 +18,7 @@ private val TAG = BackupCoordinator::class.java.simpleName
|
|||
* @author Torsten Grote
|
||||
*/
|
||||
class BackupCoordinator(
|
||||
private val context: Context,
|
||||
private val plugin: BackupPlugin,
|
||||
private val kv: KVBackup,
|
||||
private val full: FullBackup,
|
||||
|
@ -51,7 +54,7 @@ class BackupCoordinator(
|
|||
Log.i(TAG, "Initialize Device!")
|
||||
return try {
|
||||
plugin.initializeDevice()
|
||||
writeBackupMetadata()
|
||||
writeBackupMetadata(getBackupToken(context))
|
||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||
// so we remember that we initialized successfully
|
||||
calledInitialize = true
|
||||
|
@ -148,9 +151,9 @@ class BackupCoordinator(
|
|||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeBackupMetadata() {
|
||||
private fun writeBackupMetadata(token: Long) {
|
||||
val outputStream = plugin.getMetadataOutputStream()
|
||||
metadataWriter.write(outputStream)
|
||||
metadataWriter.write(outputStream, token)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ class DocumentsProviderBackupPlugin(
|
|||
storage.rootBackupDir ?: throw IOException()
|
||||
|
||||
// create backup folders
|
||||
val kvDir = storage.defaultKvBackupDir
|
||||
val fullDir = storage.defaultFullBackupDir
|
||||
val kvDir = storage.currentKvBackupDir
|
||||
val fullDir = storage.currentFullBackupDir
|
||||
|
||||
// wipe existing data
|
||||
storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete()
|
||||
|
|
|
@ -16,7 +16,7 @@ class DocumentsProviderFullBackup(
|
|||
|
||||
@Throws(IOException::class)
|
||||
override fun getOutputStream(targetPackage: PackageInfo): OutputStream {
|
||||
val file = storage.defaultFullBackupDir?.createOrGetFile(targetPackage.packageName)
|
||||
val file = storage.currentFullBackupDir?.createOrGetFile(targetPackage.packageName)
|
||||
?: throw IOException()
|
||||
return storage.getOutputStream(file)
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class DocumentsProviderFullBackup(
|
|||
override fun removeDataOfPackage(packageInfo: PackageInfo) {
|
||||
val packageName = packageInfo.packageName
|
||||
Log.i(TAG, "Deleting $packageName...")
|
||||
val file = storage.defaultFullBackupDir?.findFile(packageName) ?: return
|
||||
val file = storage.currentFullBackupDir?.findFile(packageName) ?: return
|
||||
if (!file.delete()) throw IOException("Failed to delete $packageName")
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class DocumentsProviderKVBackup(private val storage: DocumentsStorage) : KVBacku
|
|||
|
||||
@Throws(IOException::class)
|
||||
override fun hasDataForPackage(packageInfo: PackageInfo): Boolean {
|
||||
val packageFile = storage.defaultKvBackupDir?.findFile(packageInfo.packageName)
|
||||
val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName)
|
||||
?: return false
|
||||
return packageFile.listFiles().isNotEmpty()
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class DocumentsProviderKVBackup(private val storage: DocumentsStorage) : KVBacku
|
|||
override fun removeDataOfPackage(packageInfo: PackageInfo) {
|
||||
// we cannot use the cached this.packageFile here,
|
||||
// because this can be called before [ensureRecordStorageForPackage]
|
||||
val packageFile = storage.defaultKvBackupDir?.findFile(packageInfo.packageName) ?: return
|
||||
val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName) ?: return
|
||||
packageFile.delete()
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.pm.PackageInfo
|
|||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
@ -13,15 +13,13 @@ import java.io.OutputStream
|
|||
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 NO_MEDIA = ".nomedia"
|
||||
private const val MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private val TAG = DocumentsStorage::class.java.simpleName
|
||||
|
||||
class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String) {
|
||||
|
||||
private val contentResolver = context.contentResolver
|
||||
class DocumentsStorage(private val context: Context, parentFolder: Uri?, token: Long) {
|
||||
|
||||
internal val rootBackupDir: DocumentFile? by lazy {
|
||||
val folderUri = parentFolder ?: return@lazy null
|
||||
|
@ -30,7 +28,7 @@ class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String)
|
|||
try {
|
||||
val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME)
|
||||
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup
|
||||
rootDir.createOrGetFile(NO_MEDIA)
|
||||
rootDir.createOrGetFile(FILE_NO_MEDIA)
|
||||
rootDir
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating root backup dir.", e)
|
||||
|
@ -38,73 +36,71 @@ class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String)
|
|||
}
|
||||
}
|
||||
|
||||
private val deviceDir: DocumentFile? by lazy {
|
||||
private val currentToken: Long by lazy {
|
||||
if (token != 0L) token
|
||||
else getAndSaveNewBackupToken(context).apply {
|
||||
Log.d(TAG, "Using a fresh backup token: $this")
|
||||
}
|
||||
}
|
||||
|
||||
private val currentSetDir: DocumentFile? by lazy {
|
||||
val currentSetName = currentToken.toString()
|
||||
try {
|
||||
rootBackupDir?.createOrGetDirectory(deviceName)
|
||||
rootBackupDir?.createOrGetDirectory(currentSetName)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating current restore set dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultSetDir: DocumentFile? by lazy {
|
||||
val currentSetName = DEFAULT_RESTORE_SET_TOKEN.toString()
|
||||
val currentFullBackupDir: DocumentFile? by lazy {
|
||||
try {
|
||||
deviceDir?.createOrGetDirectory(currentSetName)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating current restore set dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val defaultFullBackupDir: DocumentFile? by lazy {
|
||||
try {
|
||||
defaultSetDir?.createOrGetDirectory(DIRECTORY_FULL_BACKUP)
|
||||
currentSetDir?.createOrGetDirectory(DIRECTORY_FULL_BACKUP)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating full backup dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val defaultKvBackupDir: DocumentFile? by lazy {
|
||||
val currentKvBackupDir: DocumentFile? by lazy {
|
||||
try {
|
||||
defaultSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
|
||||
currentSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error creating K/V backup dir.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSetDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? {
|
||||
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultSetDir
|
||||
return deviceDir?.findFile(token.toString())
|
||||
fun getSetDir(token: Long = currentToken): DocumentFile? {
|
||||
if (token == currentToken) return currentSetDir
|
||||
return rootBackupDir?.findFile(token.toString())
|
||||
}
|
||||
|
||||
fun getKVBackupDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? {
|
||||
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultKvBackupDir ?: throw IOException()
|
||||
fun getKVBackupDir(token: Long = currentToken): DocumentFile? {
|
||||
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
|
||||
return getSetDir(token)?.findFile(DIRECTORY_KEY_VALUE_BACKUP)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getOrCreateKVBackupDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile {
|
||||
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultKvBackupDir ?: throw IOException()
|
||||
fun getOrCreateKVBackupDir(token: Long = currentToken): DocumentFile {
|
||||
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
|
||||
val setDir = getSetDir(token) ?: throw IOException()
|
||||
return setDir.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
|
||||
}
|
||||
|
||||
fun getFullBackupDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? {
|
||||
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultFullBackupDir ?: throw IOException()
|
||||
fun getFullBackupDir(token: Long = currentToken): DocumentFile? {
|
||||
if (token == currentToken) return currentFullBackupDir ?: throw IOException()
|
||||
return getSetDir(token)?.findFile(DIRECTORY_FULL_BACKUP)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getInputStream(file: DocumentFile): InputStream {
|
||||
return contentResolver.openInputStream(file.uri) ?: throw IOException()
|
||||
return context.contentResolver.openInputStream(file.uri) ?: throw IOException()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getOutputStream(file: DocumentFile): OutputStream {
|
||||
return contentResolver.openOutputStream(file.uri) ?: throw IOException()
|
||||
return context.contentResolver.openOutputStream(file.uri) ?: throw IOException()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
|
|||
import android.app.backup.RestoreDescription
|
||||
import android.app.backup.RestoreDescription.*
|
||||
import android.app.backup.RestoreSet
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||
import com.stevesoltys.backup.metadata.FormatException
|
||||
import com.stevesoltys.backup.metadata.MetadataReader
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
|
||||
|
@ -21,6 +23,7 @@ private class RestoreCoordinatorState(
|
|||
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||
|
||||
internal class RestoreCoordinator(
|
||||
private val context: Context,
|
||||
private val plugin: RestorePlugin,
|
||||
private val kv: KVRestore,
|
||||
private val full: FullRestore,
|
||||
|
@ -64,8 +67,15 @@ internal class RestoreCoordinator(
|
|||
return restoreSets.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identifying token of the backup set currently being stored from this device.
|
||||
* This is used in the case of applications wishing to restore their last-known-good data.
|
||||
*
|
||||
* @return A token that can be used for restore,
|
||||
* or 0 if there is no backup set available corresponding to the current device state.
|
||||
*/
|
||||
fun getCurrentRestoreSet(): Long {
|
||||
return plugin.getCurrentRestoreSet()
|
||||
return getBackupToken(context)
|
||||
.apply { Log.i(TAG, "Got current restore set token: $this") }
|
||||
}
|
||||
|
||||
|
|
|
@ -16,13 +16,4 @@ interface RestorePlugin {
|
|||
**/
|
||||
fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
|
||||
|
||||
/**
|
||||
* Get the identifying token of the backup set currently being stored from this device.
|
||||
* This is used in the case of applications wishing to restore their last-known-good data.
|
||||
*
|
||||
* @return A token that can be used for restore,
|
||||
* or 0 if there is no backup set available corresponding to the current device state.
|
||||
*/
|
||||
fun getCurrentRestoreSet(): Long
|
||||
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ package com.stevesoltys.backup.transport.restore.plugins
|
|||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.backup.metadata.EncryptedBackupMetadata
|
||||
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||
import com.stevesoltys.backup.transport.backup.plugins.DocumentsStorage
|
||||
import com.stevesoltys.backup.transport.backup.plugins.FILE_BACKUP_METADATA
|
||||
import com.stevesoltys.backup.transport.backup.plugins.FILE_NO_MEDIA
|
||||
import com.stevesoltys.backup.transport.restore.FullRestorePlugin
|
||||
import com.stevesoltys.backup.transport.restore.KVRestorePlugin
|
||||
import com.stevesoltys.backup.transport.restore.RestorePlugin
|
||||
|
@ -25,30 +25,41 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re
|
|||
|
||||
override fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
|
||||
val rootDir = storage.rootBackupDir ?: return null
|
||||
val files = ArrayList<DocumentFile>()
|
||||
for (file in rootDir.listFiles()) {
|
||||
file.isDirectory || continue
|
||||
val set = file.findFile(DEFAULT_RESTORE_SET_TOKEN.toString()) ?: continue
|
||||
val metadata = set.findFile(FILE_BACKUP_METADATA) ?: continue
|
||||
files.add(metadata)
|
||||
val files = ArrayList<Pair<Long, DocumentFile>>()
|
||||
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()
|
||||
return generateSequence {
|
||||
if (!iterator.hasNext()) return@generateSequence null // end sequence
|
||||
val metadata = iterator.next()
|
||||
val token = metadata.parentFile!!.name!!.toLong()
|
||||
val pair = iterator.next()
|
||||
val token = pair.first
|
||||
val metadata = pair.second
|
||||
try {
|
||||
val stream = storage.getInputStream(metadata)
|
||||
EncryptedBackupMetadata(token, stream)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error getting InputStream for backup metadata.", e)
|
||||
EncryptedBackupMetadata(token, true)
|
||||
EncryptedBackupMetadata(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentRestoreSet(): Long {
|
||||
return DEFAULT_RESTORE_SET_TOKEN
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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
|
||||
|
@ -13,6 +15,7 @@ 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
|
||||
|
||||
|
@ -55,10 +58,11 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
|
|||
// 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")
|
||||
|
||||
// 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")
|
||||
|
||||
|
@ -71,6 +75,27 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
|
|||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LocationResult(val validLocation: Boolean, val initialSetup: Boolean)
|
||||
|
|
|
@ -43,18 +43,18 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val backup = BackupCoordinator(backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager)
|
||||
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager)
|
||||
|
||||
private val restorePlugin = mockk<RestorePlugin>()
|
||||
private val kvRestorePlugin = mockk<KVRestorePlugin>()
|
||||
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
||||
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val restore = RestoreCoordinator(restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
private val restore = RestoreCoordinator(context, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
|
||||
private val backupDataInput = mockk<BackupDataInput>()
|
||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
private val token = DEFAULT_RESTORE_SET_TOKEN
|
||||
private val token = Random.nextLong()
|
||||
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
|
||||
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
|
||||
private val key = "RestoreKey"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.backup.transport
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.crypto.Crypto
|
||||
|
@ -13,6 +14,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
|
|||
abstract class TransportTest {
|
||||
|
||||
protected val crypto = mockk<Crypto>()
|
||||
protected val context = mockk<Context>(relaxed = true)
|
||||
|
||||
protected val packageInfo = PackageInfo().apply { packageName = "org.example" }
|
||||
|
||||
|
|
|
@ -23,14 +23,14 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
private val metadataWriter = mockk<MetadataWriter>()
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
|
||||
private val backup = BackupCoordinator(plugin, kv, full, metadataWriter, notificationManager)
|
||||
private val backup = BackupCoordinator(context, plugin, kv, full, metadataWriter, notificationManager)
|
||||
|
||||
private val metadataOutputStream = mockk<OutputStream>()
|
||||
|
||||
@Test
|
||||
fun `device initialization succeeds and delegates to plugin`() {
|
||||
every { plugin.initializeDevice() } just Runs
|
||||
expectWritingMetadata()
|
||||
expectWritingMetadata(0L)
|
||||
every { kv.hasState() } returns false
|
||||
every { full.hasState() } returns false
|
||||
|
||||
|
@ -116,9 +116,9 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
assertEquals(result, backup.finishBackup())
|
||||
}
|
||||
|
||||
private fun expectWritingMetadata() {
|
||||
private fun expectWritingMetadata(token: Long = this.token) {
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataWriter.write(metadataOutputStream) } just Runs
|
||||
every { metadataWriter.write(metadataOutputStream, token) } just Runs
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package com.stevesoltys.backup.transport.backup
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.stevesoltys.backup.transport.TransportTest
|
||||
import com.stevesoltys.backup.header.HeaderWriter
|
||||
import com.stevesoltys.backup.header.VersionHeader
|
||||
import com.stevesoltys.backup.transport.TransportTest
|
||||
import io.mockk.mockk
|
||||
import java.io.OutputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
internal abstract class BackupTest : TransportTest() {
|
||||
|
||||
|
@ -14,6 +15,7 @@ internal abstract class BackupTest : TransportTest() {
|
|||
protected val data = mockk<ParcelFileDescriptor>()
|
||||
protected val outputStream = mockk<OutputStream>()
|
||||
|
||||
protected val token = Random.nextLong()
|
||||
protected val header = VersionHeader(packageName = packageInfo.packageName)
|
||||
protected val quota = 42L
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
private val full = mockk<FullRestore>()
|
||||
private val metadataReader = mockk<MetadataReader>()
|
||||
|
||||
private val restore = RestoreCoordinator(plugin, kv, full, metadataReader)
|
||||
private val restore = RestoreCoordinator(context, plugin, kv, full, metadataReader)
|
||||
|
||||
private val token = Random.nextLong()
|
||||
private val inputStream = mockk<InputStream>()
|
||||
|
@ -56,11 +56,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `getCurrentRestoreSet() delegates to plugin`() {
|
||||
val currentRestoreSet = Random.nextLong()
|
||||
|
||||
every { plugin.getCurrentRestoreSet() } returns currentRestoreSet
|
||||
|
||||
assertEquals(currentRestoreSet, restore.getCurrentRestoreSet())
|
||||
// We don't mock the SettingsManager, so the default value is returned here
|
||||
assertEquals(0L, restore.getCurrentRestoreSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue