Get rid of device folders, use unix epoch as backup token and store it

This commit is contained in:
Torsten Grote 2019-09-11 15:26:10 -03:00
parent 8b6656a350
commit af43c6154d
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
22 changed files with 160 additions and 103 deletions

View file

@ -106,10 +106,10 @@ dependencies {
implementation 'commons-io:commons-io:2.6' implementation 'commons-io:commons-io:2.6'
implementation 'io.github.novacrypto:BIP39:2019.01.27' implementation 'io.github.novacrypto:BIP39:2019.01.27'
implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.preference:preference-ktx:1.0.0' implementation 'androidx.preference:preference-ktx:1.1.0'
implementation 'com.google.android.material:material:1.0.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' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha' lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'

View file

@ -4,6 +4,7 @@ import androidx.documentfile.provider.DocumentFile
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import com.stevesoltys.backup.settings.getBackupFolderUri 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.DocumentsStorage
import com.stevesoltys.backup.transport.backup.plugins.createOrGetFile import com.stevesoltys.backup.transport.backup.plugins.createOrGetFile
import org.junit.After import org.junit.After
@ -20,9 +21,9 @@ private const val filename = "test-file"
class DocumentsStorageTest { class DocumentsStorageTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val token = getBackupToken(context)
private val folderUri = getBackupFolderUri(context) private val folderUri = getBackupFolderUri(context)
private val deviceName = "device name" private val storage = DocumentsStorage(context, folderUri, token)
private val storage = DocumentsStorage(context, folderUri, deviceName)
private lateinit var file: DocumentFile private lateinit var file: DocumentFile

View file

@ -3,12 +3,11 @@ package com.stevesoltys.backup.metadata
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import com.stevesoltys.backup.header.VERSION import com.stevesoltys.backup.header.VERSION
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
import java.io.InputStream import java.io.InputStream
data class BackupMetadata( data class BackupMetadata(
internal val version: Byte = VERSION, 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 androidVersion: Int = SDK_INT,
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}" 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) { 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, 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)
} }

View file

@ -3,7 +3,6 @@ package com.stevesoltys.backup.metadata
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.stevesoltys.backup.Utf8 import com.stevesoltys.backup.Utf8
import com.stevesoltys.backup.crypto.Crypto import com.stevesoltys.backup.crypto.Crypto
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@ -11,7 +10,7 @@ import java.io.OutputStream
interface MetadataWriter { interface MetadataWriter {
@Throws(IOException::class) @Throws(IOException::class)
fun write(outputStream: OutputStream, token: Long = DEFAULT_RESTORE_SET_TOKEN) fun write(outputStream: OutputStream, token: Long)
} }

View file

@ -10,9 +10,8 @@ import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceChangeListener import androidx.preference.Preference.OnPreferenceChangeListener
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference import androidx.preference.TwoStatePreference
@ -37,7 +36,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java) viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
backup = findPreference("backup") as TwoStatePreference backup = findPreference("backup")!!
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean val enabled = newValue as Boolean
try { try {
@ -50,13 +49,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
} }
val backupLocation = findPreference("backup_location") val backupLocation = findPreference<Preference>("backup_location")!!
backupLocation.setOnPreferenceClickListener { backupLocation.setOnPreferenceClickListener {
viewModel.chooseBackupLocation() viewModel.chooseBackupLocation()
true true
} }
autoRestore = findPreference("auto_restore") as TwoStatePreference autoRestore = findPreference("auto_restore")!!
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean val enabled = newValue as Boolean
try { try {

View file

@ -3,8 +3,10 @@ package com.stevesoltys.backup.settings
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.preference.PreferenceManager.getDefaultSharedPreferences import android.preference.PreferenceManager.getDefaultSharedPreferences
import java.util.*
private const val PREF_KEY_BACKUP_URI = "backupUri" 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_DEVICE_NAME = "deviceName"
private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword" private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"
@ -21,6 +23,24 @@ fun getBackupFolderUri(context: Context): Uri? {
return Uri.parse(uriStr) 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) { fun setDeviceName(context: Context, name: String) {
getDefaultSharedPreferences(context) getDefaultSharedPreferences(context)
.edit() .edit()

View file

@ -13,7 +13,6 @@ import android.util.Log
import com.stevesoltys.backup.settings.SettingsActivity import com.stevesoltys.backup.settings.SettingsActivity
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name 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 const val TRANSPORT_DIRECTORY_NAME = "com.stevesoltys.backup.transport.ConfigurableBackupTransport"
private val TAG = ConfigurableBackupTransport::class.java.simpleName private val TAG = ConfigurableBackupTransport::class.java.simpleName

View file

@ -9,7 +9,7 @@ import com.stevesoltys.backup.header.HeaderWriterImpl
import com.stevesoltys.backup.metadata.MetadataReaderImpl import com.stevesoltys.backup.metadata.MetadataReaderImpl
import com.stevesoltys.backup.metadata.MetadataWriterImpl import com.stevesoltys.backup.metadata.MetadataWriterImpl
import com.stevesoltys.backup.settings.getBackupFolderUri 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.BackupCoordinator
import com.stevesoltys.backup.transport.backup.FullBackup import com.stevesoltys.backup.transport.backup.FullBackup
import com.stevesoltys.backup.transport.backup.InputFactory 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. // 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 headerWriter = HeaderWriterImpl()
private val headerReader = HeaderReaderImpl() private val headerReader = HeaderReaderImpl()
@ -42,7 +42,7 @@ class PluginManager(context: Context) {
private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto) private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto)
private val notificationManager = (context.applicationContext as Backup).notificationManager 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) private val restorePlugin = DocumentsProviderRestorePlugin(storage)
@ -50,6 +50,6 @@ class PluginManager(context: Context) {
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto) private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto)
private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, 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)
} }

View file

@ -2,11 +2,13 @@ package com.stevesoltys.backup.transport.backup
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_OK
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.backup.BackupNotificationManager import com.stevesoltys.backup.BackupNotificationManager
import com.stevesoltys.backup.metadata.MetadataWriter import com.stevesoltys.backup.metadata.MetadataWriter
import com.stevesoltys.backup.settings.getBackupToken
import java.io.IOException import java.io.IOException
private val TAG = BackupCoordinator::class.java.simpleName private val TAG = BackupCoordinator::class.java.simpleName
@ -16,6 +18,7 @@ private val TAG = BackupCoordinator::class.java.simpleName
* @author Torsten Grote * @author Torsten Grote
*/ */
class BackupCoordinator( class BackupCoordinator(
private val context: Context,
private val plugin: BackupPlugin, private val plugin: BackupPlugin,
private val kv: KVBackup, private val kv: KVBackup,
private val full: FullBackup, private val full: FullBackup,
@ -51,7 +54,7 @@ class BackupCoordinator(
Log.i(TAG, "Initialize Device!") Log.i(TAG, "Initialize Device!")
return try { return try {
plugin.initializeDevice() plugin.initializeDevice()
writeBackupMetadata() writeBackupMetadata(getBackupToken(context))
// [finishBackup] will only be called when we return [TRANSPORT_OK] here // [finishBackup] will only be called when we return [TRANSPORT_OK] here
// so we remember that we initialized successfully // so we remember that we initialized successfully
calledInitialize = true calledInitialize = true
@ -148,9 +151,9 @@ class BackupCoordinator(
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun writeBackupMetadata() { private fun writeBackupMetadata(token: Long) {
val outputStream = plugin.getMetadataOutputStream() val outputStream = plugin.getMetadataOutputStream()
metadataWriter.write(outputStream) metadataWriter.write(outputStream, token)
} }
} }

View file

@ -25,8 +25,8 @@ class DocumentsProviderBackupPlugin(
storage.rootBackupDir ?: throw IOException() storage.rootBackupDir ?: throw IOException()
// create backup folders // create backup folders
val kvDir = storage.defaultKvBackupDir val kvDir = storage.currentKvBackupDir
val fullDir = storage.defaultFullBackupDir val fullDir = storage.currentFullBackupDir
// wipe existing data // wipe existing data
storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete() storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete()

View file

@ -16,7 +16,7 @@ class DocumentsProviderFullBackup(
@Throws(IOException::class) @Throws(IOException::class)
override fun getOutputStream(targetPackage: PackageInfo): OutputStream { override fun getOutputStream(targetPackage: PackageInfo): OutputStream {
val file = storage.defaultFullBackupDir?.createOrGetFile(targetPackage.packageName) val file = storage.currentFullBackupDir?.createOrGetFile(targetPackage.packageName)
?: throw IOException() ?: throw IOException()
return storage.getOutputStream(file) return storage.getOutputStream(file)
} }
@ -25,7 +25,7 @@ class DocumentsProviderFullBackup(
override fun removeDataOfPackage(packageInfo: PackageInfo) { override fun removeDataOfPackage(packageInfo: PackageInfo) {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
Log.i(TAG, "Deleting $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") if (!file.delete()) throw IOException("Failed to delete $packageName")
} }

View file

@ -15,7 +15,7 @@ class DocumentsProviderKVBackup(private val storage: DocumentsStorage) : KVBacku
@Throws(IOException::class) @Throws(IOException::class)
override fun hasDataForPackage(packageInfo: PackageInfo): Boolean { override fun hasDataForPackage(packageInfo: PackageInfo): Boolean {
val packageFile = storage.defaultKvBackupDir?.findFile(packageInfo.packageName) val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName)
?: return false ?: return false
return packageFile.listFiles().isNotEmpty() return packageFile.listFiles().isNotEmpty()
} }
@ -30,7 +30,7 @@ class DocumentsProviderKVBackup(private val storage: DocumentsStorage) : KVBacku
override fun removeDataOfPackage(packageInfo: PackageInfo) { override fun removeDataOfPackage(packageInfo: PackageInfo) {
// we cannot use the cached this.packageFile here, // we cannot use the cached this.packageFile here,
// because this can be called before [ensureRecordStorageForPackage] // 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() packageFile.delete()
} }

View file

@ -5,7 +5,7 @@ import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile 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.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -13,15 +13,13 @@ import java.io.OutputStream
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"
private const val ROOT_DIR_NAME = ".AndroidBackup" private const val ROOT_DIR_NAME = ".AndroidBackup"
private const val NO_MEDIA = ".nomedia"
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
class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String) { class DocumentsStorage(private val context: Context, parentFolder: Uri?, token: Long) {
private val contentResolver = context.contentResolver
internal val rootBackupDir: DocumentFile? by lazy { internal val rootBackupDir: DocumentFile? by lazy {
val folderUri = parentFolder ?: return@lazy null val folderUri = parentFolder ?: return@lazy null
@ -30,7 +28,7 @@ class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String)
try { try {
val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME) val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME)
// 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(NO_MEDIA) rootDir.createOrGetFile(FILE_NO_MEDIA)
rootDir rootDir
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error creating root backup dir.", e) 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 { try {
rootBackupDir?.createOrGetDirectory(deviceName) rootBackupDir?.createOrGetDirectory(currentSetName)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error creating current restore set dir.", e) Log.e(TAG, "Error creating current restore set dir.", e)
null null
} }
} }
private val defaultSetDir: DocumentFile? by lazy { val currentFullBackupDir: DocumentFile? by lazy {
val currentSetName = DEFAULT_RESTORE_SET_TOKEN.toString()
try { try {
deviceDir?.createOrGetDirectory(currentSetName) currentSetDir?.createOrGetDirectory(DIRECTORY_FULL_BACKUP)
} 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)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error creating full backup dir.", e) Log.e(TAG, "Error creating full backup dir.", e)
null null
} }
} }
val defaultKvBackupDir: DocumentFile? by lazy { val currentKvBackupDir: DocumentFile? by lazy {
try { try {
defaultSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP) currentSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error creating K/V backup dir.", e) Log.e(TAG, "Error creating K/V backup dir.", e)
null null
} }
} }
fun getSetDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? { fun getSetDir(token: Long = currentToken): DocumentFile? {
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultSetDir if (token == currentToken) return currentSetDir
return deviceDir?.findFile(token.toString()) return rootBackupDir?.findFile(token.toString())
} }
fun getKVBackupDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? { fun getKVBackupDir(token: Long = currentToken): DocumentFile? {
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultKvBackupDir ?: throw IOException() if (token == currentToken) return currentKvBackupDir ?: throw IOException()
return getSetDir(token)?.findFile(DIRECTORY_KEY_VALUE_BACKUP) return getSetDir(token)?.findFile(DIRECTORY_KEY_VALUE_BACKUP)
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getOrCreateKVBackupDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile { fun getOrCreateKVBackupDir(token: Long = currentToken): DocumentFile {
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultKvBackupDir ?: throw IOException() if (token == currentToken) return currentKvBackupDir ?: throw IOException()
val setDir = getSetDir(token) ?: throw IOException() val setDir = getSetDir(token) ?: throw IOException()
return setDir.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP) return setDir.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
} }
fun getFullBackupDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? { fun getFullBackupDir(token: Long = currentToken): DocumentFile? {
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultFullBackupDir ?: throw IOException() if (token == currentToken) return currentFullBackupDir ?: throw IOException()
return getSetDir(token)?.findFile(DIRECTORY_FULL_BACKUP) return getSetDir(token)?.findFile(DIRECTORY_FULL_BACKUP)
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getInputStream(file: DocumentFile): InputStream { fun getInputStream(file: DocumentFile): InputStream {
return contentResolver.openInputStream(file.uri) ?: throw IOException() return context.contentResolver.openInputStream(file.uri) ?: throw IOException()
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getOutputStream(file: DocumentFile): OutputStream { fun getOutputStream(file: DocumentFile): OutputStream {
return contentResolver.openOutputStream(file.uri) ?: throw IOException() return context.contentResolver.openOutputStream(file.uri) ?: throw IOException()
} }
} }

View file

@ -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.RestoreDescription.* import android.app.backup.RestoreDescription.*
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.backup.header.UnsupportedVersionException import com.stevesoltys.backup.header.UnsupportedVersionException
import com.stevesoltys.backup.metadata.FormatException import com.stevesoltys.backup.metadata.FormatException
import com.stevesoltys.backup.metadata.MetadataReader import com.stevesoltys.backup.metadata.MetadataReader
import com.stevesoltys.backup.settings.getBackupToken
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
@ -21,6 +23,7 @@ private class RestoreCoordinatorState(
private val TAG = RestoreCoordinator::class.java.simpleName private val TAG = RestoreCoordinator::class.java.simpleName
internal class RestoreCoordinator( internal class RestoreCoordinator(
private val context: Context,
private val plugin: RestorePlugin, private val plugin: RestorePlugin,
private val kv: KVRestore, private val kv: KVRestore,
private val full: FullRestore, private val full: FullRestore,
@ -64,8 +67,15 @@ internal class RestoreCoordinator(
return restoreSets.toTypedArray() 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 { fun getCurrentRestoreSet(): Long {
return plugin.getCurrentRestoreSet() return getBackupToken(context)
.apply { Log.i(TAG, "Got current restore set token: $this") } .apply { Log.i(TAG, "Got current restore set token: $this") }
} }

View file

@ -16,13 +16,4 @@ interface RestorePlugin {
**/ **/
fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? 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
} }

View file

@ -3,9 +3,9 @@ package com.stevesoltys.backup.transport.restore.plugins
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.backup.metadata.EncryptedBackupMetadata 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.DocumentsStorage
import com.stevesoltys.backup.transport.backup.plugins.FILE_BACKUP_METADATA 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.FullRestorePlugin
import com.stevesoltys.backup.transport.restore.KVRestorePlugin import com.stevesoltys.backup.transport.restore.KVRestorePlugin
import com.stevesoltys.backup.transport.restore.RestorePlugin import com.stevesoltys.backup.transport.restore.RestorePlugin
@ -25,30 +25,41 @@ 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<DocumentFile>() val files = ArrayList<Pair<Long, DocumentFile>>()
for (file in rootDir.listFiles()) { for (set in rootDir.listFiles()) {
file.isDirectory || continue if (!set.isDirectory || set.name == null) {
val set = file.findFile(DEFAULT_RESTORE_SET_TOKEN.toString()) ?: continue if (set.name != FILE_NO_MEDIA) {
val metadata = set.findFile(FILE_BACKUP_METADATA) ?: continue Log.w(TAG, "Found invalid backup set folder: ${set.name}")
files.add(metadata) }
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 iterator = files.iterator()
return generateSequence { return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence if (!iterator.hasNext()) return@generateSequence null // end sequence
val metadata = iterator.next() val pair = iterator.next()
val token = metadata.parentFile!!.name!!.toLong() val token = pair.first
val metadata = pair.second
try { try {
val stream = storage.getInputStream(metadata) val stream = storage.getInputStream(metadata)
EncryptedBackupMetadata(token, stream) EncryptedBackupMetadata(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, true) EncryptedBackupMetadata(token)
} }
} }
} }
override fun getCurrentRestoreSet(): Long {
return DEFAULT_RESTORE_SET_TOKEN
}
} }

View file

@ -1,6 +1,8 @@
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
@ -13,6 +15,7 @@ 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.settings.setBackupFolderUri
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService 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
@ -55,10 +58,11 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
// stop backup service to be sure the old location will get updated // stop backup service to be sure the old location will get updated
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java)) 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") 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 { } else {
Log.w(TAG, "Location was rejected: $folderUri") Log.w(TAG, "Location was rejected: $folderUri")
@ -71,6 +75,27 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
return true 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) class LocationResult(val validLocation: Boolean, val initialSetup: Boolean)

View file

@ -43,18 +43,18 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val fullBackupPlugin = mockk<FullBackupPlugin>() private val fullBackupPlugin = mockk<FullBackupPlugin>()
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl) private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() 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 restorePlugin = mockk<RestorePlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>() private val kvRestorePlugin = mockk<KVRestorePlugin>()
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val fullRestorePlugin = mockk<FullRestorePlugin>() private val fullRestorePlugin = mockk<FullRestorePlugin>()
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) 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 backupDataInput = mockk<BackupDataInput>()
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true) 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 appData = ByteArray(42).apply { Random.nextBytes(this) }
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) } private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
private val key = "RestoreKey" private val key = "RestoreKey"

View file

@ -1,5 +1,6 @@
package com.stevesoltys.backup.transport package com.stevesoltys.backup.transport
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import com.stevesoltys.backup.crypto.Crypto import com.stevesoltys.backup.crypto.Crypto
@ -13,6 +14,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
abstract class TransportTest { abstract class TransportTest {
protected val crypto = mockk<Crypto>() protected val crypto = mockk<Crypto>()
protected val context = mockk<Context>(relaxed = true)
protected val packageInfo = PackageInfo().apply { packageName = "org.example" } protected val packageInfo = PackageInfo().apply { packageName = "org.example" }

View file

@ -23,14 +23,14 @@ internal class BackupCoordinatorTest: BackupTest() {
private val metadataWriter = mockk<MetadataWriter>() private val metadataWriter = mockk<MetadataWriter>()
private val notificationManager = mockk<BackupNotificationManager>() 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>() private val metadataOutputStream = mockk<OutputStream>()
@Test @Test
fun `device initialization succeeds and delegates to plugin`() { fun `device initialization succeeds and delegates to plugin`() {
every { plugin.initializeDevice() } just Runs every { plugin.initializeDevice() } just Runs
expectWritingMetadata() expectWritingMetadata(0L)
every { kv.hasState() } returns false every { kv.hasState() } returns false
every { full.hasState() } returns false every { full.hasState() } returns false
@ -116,9 +116,9 @@ internal class BackupCoordinatorTest: BackupTest() {
assertEquals(result, backup.finishBackup()) assertEquals(result, backup.finishBackup())
} }
private fun expectWritingMetadata() { private fun expectWritingMetadata(token: Long = this.token) {
every { plugin.getMetadataOutputStream() } returns metadataOutputStream every { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataWriter.write(metadataOutputStream) } just Runs every { metadataWriter.write(metadataOutputStream, token) } just Runs
} }
} }

View file

@ -1,11 +1,12 @@
package com.stevesoltys.backup.transport.backup package com.stevesoltys.backup.transport.backup
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.stevesoltys.backup.transport.TransportTest
import com.stevesoltys.backup.header.HeaderWriter import com.stevesoltys.backup.header.HeaderWriter
import com.stevesoltys.backup.header.VersionHeader import com.stevesoltys.backup.header.VersionHeader
import com.stevesoltys.backup.transport.TransportTest
import io.mockk.mockk import io.mockk.mockk
import java.io.OutputStream import java.io.OutputStream
import kotlin.random.Random
internal abstract class BackupTest : TransportTest() { internal abstract class BackupTest : TransportTest() {
@ -14,6 +15,7 @@ internal abstract class BackupTest : TransportTest() {
protected val data = mockk<ParcelFileDescriptor>() protected val data = mockk<ParcelFileDescriptor>()
protected val outputStream = mockk<OutputStream>() protected val outputStream = mockk<OutputStream>()
protected val token = Random.nextLong()
protected val header = VersionHeader(packageName = packageInfo.packageName) protected val header = VersionHeader(packageName = packageInfo.packageName)
protected val quota = 42L protected val quota = 42L

View file

@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
private val full = mockk<FullRestore>() private val full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>() 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 token = Random.nextLong()
private val inputStream = mockk<InputStream>() private val inputStream = mockk<InputStream>()
@ -56,11 +56,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `getCurrentRestoreSet() delegates to plugin`() { fun `getCurrentRestoreSet() delegates to plugin`() {
val currentRestoreSet = Random.nextLong() // We don't mock the SettingsManager, so the default value is returned here
assertEquals(0L, restore.getCurrentRestoreSet())
every { plugin.getCurrentRestoreSet() } returns currentRestoreSet
assertEquals(currentRestoreSet, restore.getCurrentRestoreSet())
} }
@Test @Test