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 '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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue