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:
parent
e1d55c8a4e
commit
b9cac5ea87
27 changed files with 361 additions and 135 deletions
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()) }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
13
app/src/main/java/com/stevesoltys/seedvault/Clock.kt
Normal file
13
app/src/main/java/com/stevesoltys/seedvault/Clock.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue