Introduce MetadataManager to handle all metadata related to backups

This now updates the metadata on remote storage and internal cache
after each successful package backup.
This commit is contained in:
Torsten Grote 2019-12-18 13:14:25 -03:00
parent e1d55c8a4e
commit b9cac5ea87
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
27 changed files with 361 additions and 135 deletions

View file

@ -13,6 +13,7 @@ android {
minSdkVersion 29 minSdkVersion 29
targetSdkVersion 29 targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments disableAnalytics: 'true'
} }
buildTypes { buildTypes {
@ -35,6 +36,9 @@ android {
events "passed", "skipped", "failed" events "passed", "skipped", "failed"
} }
} }
unitTests {
includeAndroidResources = true
}
} }
sourceSets { sourceSets {
@ -119,10 +123,14 @@ dependencies {
lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha' lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
def junit_version = "5.5.2"
testImplementation aospDeps testImplementation aospDeps
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
testImplementation 'io.mockk:mockk:1.9.3' testImplementation 'io.mockk:mockk:1.9.3'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2' testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit_version"
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'

View file

@ -3,9 +3,10 @@ package com.stevesoltys.seedvault
import androidx.documentfile.provider.DocumentFile 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.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.createOrGetFile import com.stevesoltys.seedvault.plugins.saf.createOrGetFile
import com.stevesoltys.seedvault.settings.SettingsManager
import org.junit.After import org.junit.After
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -22,8 +23,9 @@ private const val filename = "test-file"
class DocumentsStorageTest : KoinComponent { class DocumentsStorageTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val metadataManager by inject<MetadataManager>()
private val settingsManager by inject<SettingsManager>() private val settingsManager by inject<SettingsManager>()
private val storage = DocumentsStorage(context, settingsManager) private val storage = DocumentsStorage(context, metadataManager, settingsManager)
private lateinit var file: DocumentFile private lateinit var file: DocumentFile

View file

@ -9,11 +9,11 @@ import android.os.ServiceManager.getService
import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.metadataModule import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.SettingsViewModel import com.stevesoltys.seedvault.settings.SettingsViewModel
import com.stevesoltys.seedvault.transport.backup.backupModule import com.stevesoltys.seedvault.transport.backup.backupModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
import com.stevesoltys.seedvault.transport.restore.restoreModule import com.stevesoltys.seedvault.transport.restore.restoreModule
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
@ -33,9 +33,10 @@ class App : Application() {
private val appModule = module { private val appModule = module {
single { SettingsManager(this@App) } single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) } single { BackupNotificationManager(this@App) }
single { Clock() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
viewModel { SettingsViewModel(this@App, get(), get()) } viewModel { SettingsViewModel(this@App, get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get()) } viewModel { RecoveryCodeViewModel(this@App, get()) }
viewModel { BackupStorageViewModel(this@App, get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) }

View file

@ -10,7 +10,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat.* import androidx.core.app.NotificationCompat.*
import com.stevesoltys.seedvault.settings.SettingsActivity import com.stevesoltys.seedvault.settings.SettingsActivity
import java.util.*
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
private const val CHANNEL_ID_ERROR = "NotificationError" private const val CHANNEL_ID_ERROR = "NotificationError"
@ -48,7 +47,7 @@ class BackupNotificationManager(private val context: Context) {
val notification = observerBuilder.apply { val notification = observerBuilder.apply {
setContentTitle(context.getString(R.string.notification_title)) setContentTitle(context.getString(R.string.notification_title))
setContentText(app) setContentText(app)
setWhen(Date().time) setWhen(System.currentTimeMillis())
setProgress(expected, transferred, false) setProgress(expected, transferred, false)
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
}.build() }.build()
@ -64,7 +63,7 @@ class BackupNotificationManager(private val context: Context) {
val notification = observerBuilder.apply { val notification = observerBuilder.apply {
setContentTitle(title) setContentTitle(title)
setContentText(app) setContentText(app)
setWhen(Date().time) setWhen(System.currentTimeMillis())
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
}.build() }.build()
nm.notify(NOTIFICATION_ID_OBSERVER, notification) nm.notify(NOTIFICATION_ID_OBSERVER, notification)
@ -82,7 +81,7 @@ class BackupNotificationManager(private val context: Context) {
val notification = errorBuilder.apply { val notification = errorBuilder.apply {
setContentTitle(context.getString(R.string.notification_error_title)) setContentTitle(context.getString(R.string.notification_error_title))
setContentText(context.getString(R.string.notification_error_text)) setContentText(context.getString(R.string.notification_error_text))
setWhen(Date().time) setWhen(System.currentTimeMillis())
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setAutoCancel(true) setAutoCancel(true)
mActions = arrayListOf(action) mActions = arrayListOf(action)

View file

@ -0,0 +1,13 @@
package com.stevesoltys.seedvault
/**
* This class only exists, so we can mock the time in tests.
*/
class Clock {
/**
* Returns the current time in milliseconds (Unix time).
*/
fun time(): Long {
return System.currentTimeMillis()
}
}

View file

@ -11,20 +11,20 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.FlashDrive import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
import org.koin.core.KoinComponent import org.koin.core.context.GlobalContext.get
import org.koin.core.inject
import java.util.*
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
private val TAG = UsbIntentReceiver::class.java.simpleName private val TAG = UsbIntentReceiver::class.java.simpleName
class UsbIntentReceiver : UsbMonitor(), KoinComponent { class UsbIntentReceiver : UsbMonitor() {
private val settingsManager by inject<SettingsManager>() private val settingsManager: SettingsManager by lazy { get().koin.get<SettingsManager>() }
private val metadataManager: MetadataManager by lazy { get().koin.get<MetadataManager>() }
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean { override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
if (action != ACTION_USB_DEVICE_ATTACHED) return false if (action != ACTION_USB_DEVICE_ATTACHED) return false
@ -33,7 +33,7 @@ class UsbIntentReceiver : UsbMonitor(), KoinComponent {
val attachedFlashDrive = FlashDrive.from(device) val attachedFlashDrive = FlashDrive.from(device)
return if (savedFlashDrive == attachedFlashDrive) { return if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, checking backup time...") Log.d(TAG, "Matches stored device, checking backup time...")
if (Date().time - settingsManager.getBackupTime() >= HOURS.toMillis(24)) { if (System.currentTimeMillis() - metadataManager.getLastBackupTime() >= HOURS.toMillis(24)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...") Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
true true
} else { } else {

View file

@ -7,11 +7,11 @@ import java.io.InputStream
data class BackupMetadata( data class BackupMetadata(
internal val version: Byte = VERSION, internal val version: Byte = VERSION,
internal val token: Long, internal val token: Long,
internal val time: Long = System.currentTimeMillis(), internal var time: Long = 0L,
internal val androidVersion: Int = Build.VERSION.SDK_INT, internal val androidVersion: Int = Build.VERSION.SDK_INT,
internal val androidIncremental: String = Build.VERSION.INCREMENTAL, internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}", internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
internal val packageMetadata: Map<String, PackageMetadata> = HashMap() internal val packageMetadata: HashMap<String, PackageMetadata> = HashMap()
) )
internal const val JSON_METADATA = "@meta@" internal const val JSON_METADATA = "@meta@"
@ -23,7 +23,7 @@ internal const val JSON_METADATA_INCREMENTAL = "incremental"
internal const val JSON_METADATA_NAME = "name" internal const val JSON_METADATA_NAME = "name"
data class PackageMetadata( data class PackageMetadata(
internal val time: Long, internal var time: Long,
internal val version: Long? = null, internal val version: Long? = null,
internal val installer: String? = null, internal val installer: String? = null,
internal val signatures: List<String>? = null internal val signatures: List<String>? = null

View file

@ -0,0 +1,119 @@
package com.stevesoltys.seedvault.metadata
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock
import java.io.FileNotFoundException
import java.io.IOException
import java.io.OutputStream
private val TAG = MetadataManager::class.java.simpleName
@VisibleForTesting
internal const val METADATA_CACHE_FILE = "metadata.cache"
@WorkerThread
class MetadataManager(
private val context: Context,
private val clock: Clock,
private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader) {
private val uninitializedMetadata = BackupMetadata(token = 0L)
private var metadata: BackupMetadata = uninitializedMetadata
get() {
if (field == uninitializedMetadata) {
field = try {
getMetadataFromCache() ?: throw IOException()
} catch (e: IOException) {
// create new default metadata
// Attention: If this happens due to a read error, we will overwrite remote metadata
Log.w(TAG, "Creating new metadata...")
BackupMetadata(token = clock.time())
}
}
return field
}
/**
* Call this when initializing a new device.
*
* A new backup token will be generated.
* Existing [BackupMetadata] will be cleared
* and written encrypted to the given [OutputStream] as well as the internal cache.
*/
@Synchronized
@Throws(IOException::class)
fun onDeviceInitialization(metadataOutputStream: OutputStream) {
metadata = BackupMetadata(token = clock.time())
metadataWriter.write(metadata, metadataOutputStream)
writeMetadataToCache()
}
/**
* Call this after a package has been backed up successfully.
*
* It updates the packages' metadata
* and writes it encrypted to the given [OutputStream] as well as the internal cache.
*/
@Synchronized
@Throws(IOException::class)
fun onPackageBackedUp(packageName: String, metadataOutputStream: OutputStream) {
val oldMetadata = metadata.copy()
val now = clock.time()
metadata.time = now
if (metadata.packageMetadata.containsKey(packageName)) {
metadata.packageMetadata[packageName]?.time = now
} else {
metadata.packageMetadata[packageName] = PackageMetadata(time = now)
}
try {
metadataWriter.write(metadata, metadataOutputStream)
} catch (e: IOException) {
Log.w(TAG, "Error writing metadata to storage", e)
// revert metadata and do not write it to cache
metadata = oldMetadata
throw IOException(e)
}
writeMetadataToCache()
}
@Synchronized
fun getBackupToken(): Long = metadata.token
/**
* Returns the last backup time in unix epoch milli seconds.
*
* Note that this might be a blocking I/O call.
*/
@Synchronized
fun getLastBackupTime(): Long = metadata.time
@Synchronized
@VisibleForTesting
private fun getMetadataFromCache(): BackupMetadata? {
try {
with(context.openFileInput(METADATA_CACHE_FILE)) {
return metadataReader.decode(readBytes())
}
} catch (e: SecurityException) {
Log.e(TAG, "Error parsing cached metadata", e)
return null
} catch (e: FileNotFoundException) {
Log.d(TAG, "Cached metadata not found, creating...")
return null
}
}
@Synchronized
@VisibleForTesting
@Throws(IOException::class)
private fun writeMetadataToCache() {
with(context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE)) {
write(metadataWriter.encode(metadata))
}
}
}

View file

@ -1,8 +1,10 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val metadataModule = module { val metadataModule = module {
single { MetadataManager(androidContext(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) } single<MetadataWriter> { MetadataWriterImpl(get()) }
single<MetadataReader> { MetadataReaderImpl(get()) } single<MetadataReader> { MetadataReaderImpl(get()) }
} }

View file

@ -1,6 +1,5 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import androidx.annotation.VisibleForTesting
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.UnsupportedVersionException
@ -16,6 +15,9 @@ interface MetadataReader {
@Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class) @Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class)
fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata
@Throws(SecurityException::class)
fun decode(bytes: ByteArray, expectedVersion: Byte? = null, expectedToken: Long? = null): BackupMetadata
} }
internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
@ -33,9 +35,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
return decode(metadataBytes, version, expectedToken) return decode(metadataBytes, version, expectedToken)
} }
@VisibleForTesting
@Throws(SecurityException::class) @Throws(SecurityException::class)
internal fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata { override fun decode(bytes: ByteArray, expectedVersion: Byte?, expectedToken: Long?): BackupMetadata {
// NOTE: We don't do extensive validation of the parsed input here, // NOTE: We don't do extensive validation of the parsed input here,
// because it was encrypted with authentication, so we should be able to trust it. // because it was encrypted with authentication, so we should be able to trust it.
// //
@ -46,11 +47,11 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
// get backup metadata and check expectations // get backup metadata and check expectations
val meta = json.getJSONObject(JSON_METADATA) val meta = json.getJSONObject(JSON_METADATA)
val version = meta.getInt(JSON_METADATA_VERSION).toByte() val version = meta.getInt(JSON_METADATA_VERSION).toByte()
if (version != expectedVersion) { if (expectedVersion != null && version != expectedVersion) {
throw SecurityException("Invalid version '${version.toInt()}' in metadata, expected '${expectedVersion.toInt()}'.") throw SecurityException("Invalid version '${version.toInt()}' in metadata, expected '${expectedVersion.toInt()}'.")
} }
val token = meta.getLong(JSON_METADATA_TOKEN) val token = meta.getLong(JSON_METADATA_TOKEN)
if (token != expectedToken) { if (expectedToken != null && token != expectedToken) {
throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.") throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.")
} }
// get package metadata // get package metadata

View file

@ -1,6 +1,5 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import androidx.annotation.VisibleForTesting
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import org.json.JSONArray import org.json.JSONArray
@ -9,23 +8,21 @@ import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
interface MetadataWriter { interface MetadataWriter {
@Throws(IOException::class) @Throws(IOException::class)
fun write(outputStream: OutputStream, token: Long) fun write(metadata: BackupMetadata, outputStream: OutputStream)
fun encode(metadata: BackupMetadata): ByteArray
} }
internal class MetadataWriterImpl(private val crypto: Crypto): MetadataWriter { internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
@Throws(IOException::class) @Throws(IOException::class)
override fun write(outputStream: OutputStream, token: Long) { override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
val metadata = BackupMetadata(token = token)
outputStream.write(ByteArray(1).apply { this[0] = metadata.version }) outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
crypto.encryptMultipleSegments(outputStream, encode(metadata)) crypto.encryptMultipleSegments(outputStream, encode(metadata))
} }
@VisibleForTesting override fun encode(metadata: BackupMetadata): ByteArray {
internal fun encode(metadata: BackupMetadata): ByteArray {
val json = JSONObject().apply { val json = JSONObject().apply {
put(JSON_METADATA, JSONObject().apply { put(JSON_METADATA, JSONObject().apply {
put(JSON_METADATA_VERSION, metadata.version.toInt()) put(JSON_METADATA_VERSION, metadata.version.toInt())

View file

@ -6,7 +6,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val documentsProviderModule = module { val documentsProviderModule = module {
single { DocumentsStorage(androidContext(), get()) } single { DocumentsStorage(androidContext(), get(), get()) }
single<BackupPlugin> { DocumentsProviderBackupPlugin(get(), androidContext().packageManager) } single<BackupPlugin> { DocumentsProviderBackupPlugin(get(), androidContext().packageManager) }
single<RestorePlugin> { DocumentsProviderRestorePlugin(androidContext(), get()) } single<RestorePlugin> { DocumentsProviderRestorePlugin(androidContext(), get()) }
} }

View file

@ -9,6 +9,7 @@ import android.provider.DocumentsContract.*
import android.provider.DocumentsContract.Document.* import android.provider.DocumentsContract.Document.*
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.settings.Storage
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
@ -28,10 +29,10 @@ private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage( internal class DocumentsStorage(
private val context: Context, private val context: Context,
private val settingsManager: SettingsManager) { private val metadataManager: MetadataManager,
settingsManager: SettingsManager) {
private val storage: Storage? = settingsManager.getStorage() private val storage: Storage? = settingsManager.getStorage()
private val token: Long = settingsManager.getBackupToken()
internal val rootBackupDir: DocumentFile? by lazy { internal val rootBackupDir: DocumentFile? by lazy {
val parent = storage?.getDocumentFile(context) ?: return@lazy null val parent = storage?.getDocumentFile(context) ?: return@lazy null
@ -47,10 +48,7 @@ internal class DocumentsStorage(
} }
private val currentToken: Long by lazy { private val currentToken: Long by lazy {
if (token != 0L) token metadataManager.getBackupToken()
else settingsManager.getAndSaveNewBackupToken().apply {
Log.d(TAG, "Using a fresh backup token: $this")
}
} }
private val currentSetDir: DocumentFile? by lazy { private val currentSetDir: DocumentFile? by lazy {

View file

@ -18,6 +18,7 @@ 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 androidx.lifecycle.Observer
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceChangeListener import androidx.preference.Preference.OnPreferenceChangeListener
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@ -28,7 +29,6 @@ import com.stevesoltys.seedvault.isMassStorage
import com.stevesoltys.seedvault.restore.RestoreActivity import com.stevesoltys.seedvault.restore.RestoreActivity
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import java.util.*
private val TAG = SettingsFragment::class.java.name private val TAG = SettingsFragment::class.java.name
@ -94,6 +94,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
return@OnPreferenceChangeListener false return@OnPreferenceChangeListener false
} }
} }
viewModel.lastBackupTime.observe(this, Observer { time -> setBackupLocationSummary(time) })
} }
override fun onStart() { override fun onStart() {
@ -105,8 +107,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
storage = settingsManager.getStorage() storage = settingsManager.getStorage()
setBackupState() setBackupState()
setAutoRestoreState() setAutoRestoreState()
setBackupLocationSummary()
setMenuItemStates() setMenuItemStates()
viewModel.updateLastBackupTime()
if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter) if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
} }
@ -159,16 +161,15 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
} }
private fun setBackupLocationSummary() { private fun setBackupLocationSummary(lastBackupInMillis: Long) {
// get name of storage location // get name of storage location
val storageName = storage?.name ?: getString(R.string.settings_backup_location_none) val storageName = storage?.name ?: getString(R.string.settings_backup_location_none)
// get time of last backup // set time of last backup
val lastBackupInMillis = settingsManager.getBackupTime()
val lastBackup = if (lastBackupInMillis == 0L) { val lastBackup = if (lastBackupInMillis == 0L) {
getString(R.string.settings_backup_last_backup_never) getString(R.string.settings_backup_last_backup_never)
} else { } else {
getRelativeTimeSpanString(lastBackupInMillis, Date().time, MINUTE_IN_MILLIS, 0) getRelativeTimeSpanString(lastBackupInMillis, System.currentTimeMillis(), MINUTE_IN_MILLIS, 0)
} }
backupLocation.summary = getString(R.string.settings_backup_location_summary, storageName, lastBackup) backupLocation.summary = getString(R.string.settings_backup_location_summary, storageName, lastBackup)
} }

View file

@ -5,7 +5,6 @@ import android.hardware.usb.UsbDevice
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.util.*
private const val PREF_KEY_STORAGE_URI = "storageUri" private const val PREF_KEY_STORAGE_URI = "storageUri"
private const val PREF_KEY_STORAGE_NAME = "storageName" private const val PREF_KEY_STORAGE_NAME = "storageName"
@ -16,9 +15,6 @@ private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber"
private const val PREF_KEY_FLASH_DRIVE_VENDOR_ID = "flashDriveVendorId" private const val PREF_KEY_FLASH_DRIVE_VENDOR_ID = "flashDriveVendorId"
private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId" private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
private const val PREF_KEY_BACKUP_TOKEN = "backupToken"
private const val PREF_KEY_BACKUP_TIME = "backupTime"
class SettingsManager(context: Context) { class SettingsManager(context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -66,48 +62,6 @@ class SettingsManager(context: Context) {
return FlashDrive(name, serialNumber, vendorId, productId) return FlashDrive(name, serialNumber, vendorId, productId)
} }
/**
* 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(): Long = Date().time.apply {
prefs.edit()
.putLong(PREF_KEY_BACKUP_TOKEN, this)
.apply()
}
/**
* Returns the current backup token or 0 if none exists.
*/
fun getBackupToken(): Long {
return prefs.getLong(PREF_KEY_BACKUP_TOKEN, 0L)
}
/**
* Sets the last backup time to "now".
*/
fun saveNewBackupTime() {
prefs.edit()
.putLong(PREF_KEY_BACKUP_TIME, Date().time)
.apply()
}
/**
* Sets the last backup time to "never".
*/
fun resetBackupTime() {
prefs.edit()
.putLong(PREF_KEY_BACKUP_TIME, 0L)
.apply()
}
/**
* Returns the last backup time in unix epoch milli seconds.
*/
fun getBackupTime(): Long {
return prefs.getLong(PREF_KEY_BACKUP_TIME, 0L)
}
} }
data class Storage( data class Storage(

View file

@ -1,19 +1,30 @@
package com.stevesoltys.seedvault.settings package com.stevesoltys.seedvault.settings
import android.app.Application import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
class SettingsViewModel( class SettingsViewModel(
app: Application, app: Application,
settingsManager: SettingsManager, settingsManager: SettingsManager,
keyManager: KeyManager keyManager: KeyManager,
private val metadataManager: MetadataManager
) : RequireProvisioningViewModel(app, settingsManager, keyManager) { ) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
override val isRestoreOperation = false override val isRestoreOperation = false
fun backupNow() { private val _lastBackupTime = MutableLiveData<Long>()
internal val lastBackupTime: LiveData<Long> = _lastBackupTime
internal fun updateLastBackupTime() {
Thread { _lastBackupTime.postValue(metadataManager.getLastBackupTime()) }.start()
}
internal fun backupNow() {
Thread { requestBackup(app) }.start() Thread { requestBackup(app) }.start()
} }

View file

@ -7,7 +7,7 @@ import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.BackupNotificationManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.MetadataWriter import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit.DAYS import java.util.concurrent.TimeUnit.DAYS
@ -23,7 +23,7 @@ internal class BackupCoordinator(
private val plugin: BackupPlugin, private val plugin: BackupPlugin,
private val kv: KVBackup, private val kv: KVBackup,
private val full: FullBackup, private val full: FullBackup,
private val metadataWriter: MetadataWriter, private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager) { private val nm: BackupNotificationManager) {
@ -56,7 +56,7 @@ internal class BackupCoordinator(
Log.i(TAG, "Initialize Device!") Log.i(TAG, "Initialize Device!")
return try { return try {
plugin.initializeDevice() plugin.initializeDevice()
writeBackupMetadata(settingsManager.getBackupToken()) metadataManager.onDeviceInitialization(plugin.getMetadataOutputStream())
// [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
@ -102,15 +102,15 @@ internal class BackupCoordinator(
} }
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
val packageName = packageInfo.packageName
// backups of package manager metadata do not respect backoff // backups of package manager metadata do not respect backoff
// we need to reject them manually when now is not a good time for a backup // we need to reject them manually when now is not a good time for a backup
if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) { if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
return TRANSPORT_PACKAGE_REJECTED return TRANSPORT_PACKAGE_REJECTED
} }
val result = kv.performBackup(packageInfo, data, flags) val result = kv.performBackup(packageInfo, data, flags)
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime() return onPackageBackedUp(result, packageInfo)
return result
} }
// ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------
@ -138,8 +138,7 @@ internal class BackupCoordinator(
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int { fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
val result = full.performFullBackup(targetPackage, fileDescriptor, flags) val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime() return onPackageBackedUp(result, targetPackage)
return result
} }
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes) fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
@ -193,10 +192,17 @@ internal class BackupCoordinator(
else -> throw IllegalStateException("Unexpected state in finishBackup()") else -> throw IllegalStateException("Unexpected state in finishBackup()")
} }
@Throws(IOException::class) private fun onPackageBackedUp(result: Int, packageInfo: PackageInfo): Int {
private fun writeBackupMetadata(token: Long) { if (result != TRANSPORT_OK) return result
val outputStream = plugin.getMetadataOutputStream() val packageName = packageInfo.packageName
metadataWriter.write(outputStream, token) try {
val outputStream = plugin.getMetadataOutputStream()
metadataManager.onPackageBackedUp(packageName, outputStream)
} catch (e: IOException) {
Log.e(TAG, "Error while writing metadata for $packageName", e)
return TRANSPORT_PACKAGE_REJECTED
}
return result
} }
private fun getBackupBackoff(): Long { private fun getBackupBackoff(): Long {

View file

@ -10,8 +10,8 @@ import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.metadata.DecryptionFailedException import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.settings.SettingsManager
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
@ -22,7 +22,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 settingsManager: SettingsManager, private val metadataManager: MetadataManager,
private val plugin: RestorePlugin, private val plugin: RestorePlugin,
private val kv: KVRestore, private val kv: KVRestore,
private val full: FullRestore, private val full: FullRestore,
@ -76,7 +76,7 @@ internal class RestoreCoordinator(
* or 0 if there is no backup set available corresponding to the current device state. * or 0 if there is no backup set available corresponding to the current device state.
*/ */
fun getCurrentRestoreSet(): Long { fun getCurrentRestoreSet(): Long {
return settingsManager.getBackupToken() return metadataManager.getBackupToken()
.apply { Log.i(TAG, "Got current restore set token: $this") } .apply { Log.i(TAG, "Got current restore set token: $this") }
} }

View file

@ -25,10 +25,7 @@ internal class BackupStorageViewModel(
override fun onLocationSet(uri: Uri) { override fun onLocationSet(uri: Uri) {
val isUsb = saveStorage(uri) val isUsb = saveStorage(uri)
// use a new backup token // initialize the new location, will also generate a new backup token
settingsManager.getAndSaveNewBackupToken()
// initialize the new location
val observer = InitializationObserver() val observer = InitializationObserver()
backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer) backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer)

View file

@ -96,9 +96,6 @@ internal abstract class StorageViewModel(
val storage = Storage(uri, name, root.isUsb) val storage = Storage(uri, name, root.isUsb)
settingsManager.setStorage(storage) settingsManager.setStorage(storage)
// reset time of last backup to "Never"
settingsManager.resetBackupTime()
if (storage.isUsb) { if (storage.isUsb) {
Log.d(TAG, "Selected storage is a removable USB device.") Log.d(TAG, "Selected storage is a removable USB device.")
val wasSaved = saveUsbDevice() val wasSaved = saveUsbDevice()

View file

@ -0,0 +1,124 @@
package com.stevesoltys.seedvault.metadata
import android.content.Context
import android.content.Context.MODE_PRIVATE
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.stopKoin
import java.io.*
@RunWith(AndroidJUnit4::class)
class MetadataManagerTest {
private val context: Context = mockk()
private val clock: Clock = mockk()
private val metadataWriter: MetadataWriter = mockk()
private val metadataReader: MetadataReader = mockk()
private val manager = MetadataManager(context, clock, metadataWriter, metadataReader)
private val time = 42L
private val initialMetadata = BackupMetadata(token = time)
private val storageOutputStream = ByteArrayOutputStream()
private val cacheOutputStream: FileOutputStream = mockk()
private val cacheInputStream: FileInputStream = mockk()
private val encodedMetadata = getRandomByteArray()
@After
fun afterEachTest() {
stopKoin()
}
@Test
fun `test onDeviceInitialization()`() {
every { clock.time() } returns time
every { metadataWriter.write(initialMetadata, storageOutputStream) } just Runs
expectWriteToCache(initialMetadata)
manager.onDeviceInitialization(storageOutputStream)
assertEquals(time, manager.getBackupToken())
assertEquals(0L, manager.getLastBackupTime())
}
@Test
fun `test onPackageBackedUp()`() {
val packageName = getRandomString()
val updatedMetadata = initialMetadata.copy()
updatedMetadata.time = time
updatedMetadata.packageMetadata[packageName] = PackageMetadata(time)
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
every { clock.time() } returns time
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs
expectWriteToCache(updatedMetadata)
manager.onPackageBackedUp(packageName, storageOutputStream)
assertEquals(time, manager.getLastBackupTime())
}
@Test
fun `test onPackageBackedUp() fails to write to storage`() {
val packageName = getRandomString()
val updatedMetadata = initialMetadata.copy()
updatedMetadata.time = time
updatedMetadata.packageMetadata[packageName] = PackageMetadata(time)
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
every { clock.time() } returns time
every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
try {
manager.onPackageBackedUp(packageName, storageOutputStream)
fail()
} catch (e: IOException) {
// expected
}
assertEquals(0L, manager.getLastBackupTime()) // time was reverted
// TODO also assert reverted PackageMetadata once possible
}
@Test
fun `test onPackageBackedUp() with filled cache`() {
val cachedPackageName = getRandomString()
val packageName = getRandomString()
val byteArray = ByteArray(DEFAULT_BUFFER_SIZE)
val cachedMetadata = initialMetadata.copy(time = 23)
cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(23)
cachedMetadata.packageMetadata[packageName] = PackageMetadata(23)
every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream
every { cacheInputStream.available() } returns byteArray.size andThen 0
every { cacheInputStream.read(byteArray) } returns -1
every { metadataReader.decode(ByteArray(0)) } returns cachedMetadata
every { clock.time() } returns time
every { metadataWriter.write(cachedMetadata, storageOutputStream) } just Runs
expectWriteToCache(cachedMetadata)
manager.onPackageBackedUp(packageName, storageOutputStream)
assertEquals(time, manager.getLastBackupTime())
// TODO also assert updated PackageMetadata once possible
}
private fun expectWriteToCache(metadata: BackupMetadata) {
every { metadataWriter.encode(metadata) } returns encodedMetadata
every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream
every { cacheOutputStream.write(encodedMetadata) } just Runs
}
}

View file

@ -122,7 +122,7 @@ class MetadataReaderTest {
assertNull(packageMetadata.signatures) assertNull(packageMetadata.signatures)
} }
private fun getMetadata(packageMetadata: Map<String, PackageMetadata> = HashMap()): BackupMetadata { private fun getMetadata(packageMetadata: HashMap<String, PackageMetadata> = HashMap()): BackupMetadata {
return BackupMetadata( return BackupMetadata(
version = 1.toByte(), version = 1.toByte(),
token = Random.nextLong(), token = Random.nextLong(),

View file

@ -66,7 +66,7 @@ internal class MetadataWriterDecoderTest {
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)) assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
} }
private fun getMetadata(packageMetadata: Map<String, PackageMetadata> = HashMap()): BackupMetadata { private fun getMetadata(packageMetadata: HashMap<String, PackageMetadata> = HashMap()): BackupMetadata {
return BackupMetadata( return BackupMetadata(
version = Random.nextBytes(1)[0], version = Random.nextBytes(1)[0],
token = Random.nextLong(), token = Random.nextLong(),

View file

@ -16,7 +16,6 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.header.HeaderWriterImpl import com.stevesoltys.seedvault.header.HeaderWriterImpl
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.MetadataWriterImpl
import com.stevesoltys.seedvault.transport.backup.* import com.stevesoltys.seedvault.transport.backup.*
import com.stevesoltys.seedvault.transport.restore.* import com.stevesoltys.seedvault.transport.restore.*
import io.mockk.* import io.mockk.*
@ -35,7 +34,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val headerWriter = HeaderWriterImpl() private val headerWriter = HeaderWriterImpl()
private val headerReader = HeaderReaderImpl() private val headerReader = HeaderReaderImpl()
private val cryptoImpl = CryptoImpl(cipherFactory, headerWriter, headerReader) private val cryptoImpl = CryptoImpl(cipherFactory, headerWriter, headerReader)
private val metadataWriter = MetadataWriterImpl(cryptoImpl)
private val metadataReader = MetadataReaderImpl(cryptoImpl) private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val backupPlugin = mockk<BackupPlugin>() private val backupPlugin = mockk<BackupPlugin>()
@ -44,20 +42,21 @@ 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(context, backupPlugin, kvBackup, fullBackup, metadataWriter, settingsManager, notificationManager) private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataManager, settingsManager, 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(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader) private val restore = RestoreCoordinator(metadataManager, 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 = Random.nextLong() 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 metadataOutputStream = ByteArrayOutputStream()
private val key = "RestoreKey" private val key = "RestoreKey"
private val key64 = key.encodeBase64() private val key64 = key.encodeBase64()
private val key2 = "RestoreKey2" private val key2 = "RestoreKey2"
@ -92,7 +91,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData2.size appData2.size
} }
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2 every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
every { settingsManager.saveNewBackupTime() } just Runs every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
// start and finish K/V backup // start and finish K/V backup
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
@ -179,7 +179,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
every { settingsManager.saveNewBackupTime() } just Runs every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
// perform backup to output stream // perform backup to output stream
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))

View file

@ -4,6 +4,7 @@ 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.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -16,6 +17,7 @@ abstract class TransportTest {
protected val crypto = mockk<Crypto>() protected val crypto = mockk<Crypto>()
protected val settingsManager = mockk<SettingsManager>() protected val settingsManager = mockk<SettingsManager>()
protected val metadataManager = mockk<MetadataManager>()
protected val context = mockk<Context>(relaxed = true) 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

@ -6,7 +6,6 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.BackupNotificationManager
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.MetadataWriter
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.settings.Storage
import io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
@ -24,18 +23,17 @@ internal class BackupCoordinatorTest: BackupTest() {
private val plugin = mockk<BackupPlugin>() private val plugin = mockk<BackupPlugin>()
private val kv = mockk<KVBackup>() private val kv = mockk<KVBackup>()
private val full = mockk<FullBackup>() private val full = mockk<FullBackup>()
private val metadataWriter = mockk<MetadataWriter>()
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, plugin, kv, full, metadataWriter, settingsManager, notificationManager) private val backup = BackupCoordinator(context, plugin, kv, full, metadataManager, settingsManager, 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
every { settingsManager.getBackupToken() } returns token every { plugin.getMetadataOutputStream() } returns metadataOutputStream
expectWritingMetadata(token) every { metadataManager.onDeviceInitialization(metadataOutputStream) } just Runs
every { kv.hasState() } returns false every { kv.hasState() } returns false
every { full.hasState() } returns false every { full.hasState() } returns false
@ -145,9 +143,4 @@ internal class BackupCoordinatorTest: BackupTest() {
assertEquals(result, backup.finishBackup()) assertEquals(result, backup.finishBackup())
} }
private fun expectWritingMetadata(token: Long = this.token) {
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataWriter.write(metadataOutputStream, token) } just Runs
}
} }

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(settingsManager, plugin, kv, full, metadataReader) private val restore = RestoreCoordinator(metadataManager, plugin, kv, full, metadataReader)
private val token = Random.nextLong() private val token = Random.nextLong()
private val inputStream = mockk<InputStream>() private val inputStream = mockk<InputStream>()
@ -57,7 +57,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `getCurrentRestoreSet() delegates to plugin`() { fun `getCurrentRestoreSet() delegates to plugin`() {
every { settingsManager.getBackupToken() } returns token every { metadataManager.getBackupToken() } returns token
assertEquals(token, restore.getCurrentRestoreSet()) assertEquals(token, restore.getCurrentRestoreSet())
} }