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
|
||||
targetSdkVersion 29
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments disableAnalytics: 'true'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -35,6 +36,9 @@ android {
|
|||
events "passed", "skipped", "failed"
|
||||
}
|
||||
}
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
@ -119,10 +123,14 @@ dependencies {
|
|||
|
||||
lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
|
||||
|
||||
def junit_version = "5.5.2"
|
||||
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'
|
||||
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:rules:1.2.0'
|
||||
|
|
|
@ -3,9 +3,10 @@ package com.stevesoltys.seedvault
|
|||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
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.createOrGetFile
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
|
@ -22,8 +23,9 @@ private const val filename = "test-file"
|
|||
class DocumentsStorageTest : KoinComponent {
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val metadataManager by inject<MetadataManager>()
|
||||
private val settingsManager by inject<SettingsManager>()
|
||||
private val storage = DocumentsStorage(context, settingsManager)
|
||||
private val storage = DocumentsStorage(context, metadataManager, settingsManager)
|
||||
|
||||
private lateinit var file: DocumentFile
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@ import android.os.ServiceManager.getService
|
|||
import com.stevesoltys.seedvault.crypto.cryptoModule
|
||||
import com.stevesoltys.seedvault.header.headerModule
|
||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
||||
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
||||
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.ui.recoverycode.RecoveryCodeViewModel
|
||||
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
||||
|
@ -33,9 +33,10 @@ class App : Application() {
|
|||
private val appModule = module {
|
||||
single { SettingsManager(this@App) }
|
||||
single { BackupNotificationManager(this@App) }
|
||||
single { Clock() }
|
||||
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 { BackupStorageViewModel(this@App, get(), get()) }
|
||||
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat.*
|
||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
||||
import java.util.*
|
||||
|
||||
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
||||
private const val CHANNEL_ID_ERROR = "NotificationError"
|
||||
|
@ -48,7 +47,7 @@ class BackupNotificationManager(private val context: Context) {
|
|||
val notification = observerBuilder.apply {
|
||||
setContentTitle(context.getString(R.string.notification_title))
|
||||
setContentText(app)
|
||||
setWhen(Date().time)
|
||||
setWhen(System.currentTimeMillis())
|
||||
setProgress(expected, transferred, false)
|
||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||
}.build()
|
||||
|
@ -64,7 +63,7 @@ class BackupNotificationManager(private val context: Context) {
|
|||
val notification = observerBuilder.apply {
|
||||
setContentTitle(title)
|
||||
setContentText(app)
|
||||
setWhen(Date().time)
|
||||
setWhen(System.currentTimeMillis())
|
||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||
|
@ -82,7 +81,7 @@ class BackupNotificationManager(private val context: Context) {
|
|||
val notification = errorBuilder.apply {
|
||||
setContentTitle(context.getString(R.string.notification_error_title))
|
||||
setContentText(context.getString(R.string.notification_error_text))
|
||||
setWhen(Date().time)
|
||||
setWhen(System.currentTimeMillis())
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
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.provider.DocumentsContract
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.settings.FlashDrive
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.requestBackup
|
||||
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import java.util.*
|
||||
import org.koin.core.context.GlobalContext.get
|
||||
import java.util.concurrent.TimeUnit.HOURS
|
||||
|
||||
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 {
|
||||
if (action != ACTION_USB_DEVICE_ATTACHED) return false
|
||||
|
@ -33,7 +33,7 @@ class UsbIntentReceiver : UsbMonitor(), KoinComponent {
|
|||
val attachedFlashDrive = FlashDrive.from(device)
|
||||
return if (savedFlashDrive == attachedFlashDrive) {
|
||||
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...")
|
||||
true
|
||||
} else {
|
||||
|
|
|
@ -7,11 +7,11 @@ import java.io.InputStream
|
|||
data class BackupMetadata(
|
||||
internal val version: Byte = VERSION,
|
||||
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 androidIncremental: String = Build.VERSION.INCREMENTAL,
|
||||
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@"
|
||||
|
@ -23,7 +23,7 @@ internal const val JSON_METADATA_INCREMENTAL = "incremental"
|
|||
internal const val JSON_METADATA_NAME = "name"
|
||||
|
||||
data class PackageMetadata(
|
||||
internal val time: Long,
|
||||
internal var time: Long,
|
||||
internal val version: Long? = null,
|
||||
internal val installer: 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
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val metadataModule = module {
|
||||
single { MetadataManager(androidContext(), get(), get(), get()) }
|
||||
single<MetadataWriter> { MetadataWriterImpl(get()) }
|
||||
single<MetadataReader> { MetadataReaderImpl(get()) }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.seedvault.metadata
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.stevesoltys.seedvault.Utf8
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
|
@ -16,6 +15,9 @@ interface MetadataReader {
|
|||
@Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class)
|
||||
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 {
|
||||
|
@ -33,9 +35,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
|||
return decode(metadataBytes, version, expectedToken)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@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,
|
||||
// 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
|
||||
val meta = json.getJSONObject(JSON_METADATA)
|
||||
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()}'.")
|
||||
}
|
||||
val token = meta.getLong(JSON_METADATA_TOKEN)
|
||||
if (token != expectedToken) {
|
||||
if (expectedToken != null && token != expectedToken) {
|
||||
throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.")
|
||||
}
|
||||
// get package metadata
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.seedvault.metadata
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.stevesoltys.seedvault.Utf8
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import org.json.JSONArray
|
||||
|
@ -9,23 +8,21 @@ import java.io.IOException
|
|||
import java.io.OutputStream
|
||||
|
||||
interface MetadataWriter {
|
||||
|
||||
@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)
|
||||
override fun write(outputStream: OutputStream, token: Long) {
|
||||
val metadata = BackupMetadata(token = token)
|
||||
override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
|
||||
outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
|
||||
crypto.encryptMultipleSegments(outputStream, encode(metadata))
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun encode(metadata: BackupMetadata): ByteArray {
|
||||
override fun encode(metadata: BackupMetadata): ByteArray {
|
||||
val json = JSONObject().apply {
|
||||
put(JSON_METADATA, JSONObject().apply {
|
||||
put(JSON_METADATA_VERSION, metadata.version.toInt())
|
||||
|
|
|
@ -6,7 +6,7 @@ import org.koin.android.ext.koin.androidContext
|
|||
import org.koin.dsl.module
|
||||
|
||||
val documentsProviderModule = module {
|
||||
single { DocumentsStorage(androidContext(), get()) }
|
||||
single { DocumentsStorage(androidContext(), get(), get()) }
|
||||
single<BackupPlugin> { DocumentsProviderBackupPlugin(get(), androidContext().packageManager) }
|
||||
single<RestorePlugin> { DocumentsProviderRestorePlugin(androidContext(), get()) }
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.provider.DocumentsContract.*
|
|||
import android.provider.DocumentsContract.Document.*
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
|
@ -28,10 +29,10 @@ private val TAG = DocumentsStorage::class.java.simpleName
|
|||
|
||||
internal class DocumentsStorage(
|
||||
private val context: Context,
|
||||
private val settingsManager: SettingsManager) {
|
||||
private val metadataManager: MetadataManager,
|
||||
settingsManager: SettingsManager) {
|
||||
|
||||
private val storage: Storage? = settingsManager.getStorage()
|
||||
private val token: Long = settingsManager.getBackupToken()
|
||||
|
||||
internal val rootBackupDir: DocumentFile? by lazy {
|
||||
val parent = storage?.getDocumentFile(context) ?: return@lazy null
|
||||
|
@ -47,10 +48,7 @@ internal class DocumentsStorage(
|
|||
}
|
||||
|
||||
private val currentToken: Long by lazy {
|
||||
if (token != 0L) token
|
||||
else settingsManager.getAndSaveNewBackupToken().apply {
|
||||
Log.d(TAG, "Using a fresh backup token: $this")
|
||||
}
|
||||
metadataManager.getBackupToken()
|
||||
}
|
||||
|
||||
private val currentSetDir: DocumentFile? by lazy {
|
||||
|
|
|
@ -18,6 +18,7 @@ import android.util.Log
|
|||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
|
@ -28,7 +29,6 @@ import com.stevesoltys.seedvault.isMassStorage
|
|||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import java.util.*
|
||||
|
||||
private val TAG = SettingsFragment::class.java.name
|
||||
|
||||
|
@ -94,6 +94,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@OnPreferenceChangeListener false
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.lastBackupTime.observe(this, Observer { time -> setBackupLocationSummary(time) })
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -105,8 +107,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
storage = settingsManager.getStorage()
|
||||
setBackupState()
|
||||
setAutoRestoreState()
|
||||
setBackupLocationSummary()
|
||||
setMenuItemStates()
|
||||
viewModel.updateLastBackupTime()
|
||||
|
||||
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
|
||||
val storageName = storage?.name ?: getString(R.string.settings_backup_location_none)
|
||||
|
||||
// get time of last backup
|
||||
val lastBackupInMillis = settingsManager.getBackupTime()
|
||||
// set time of last backup
|
||||
val lastBackup = if (lastBackupInMillis == 0L) {
|
||||
getString(R.string.settings_backup_last_backup_never)
|
||||
} 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)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.hardware.usb.UsbDevice
|
|||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.util.*
|
||||
|
||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
||||
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_PRODUCT_ID = "flashDriveProductId"
|
||||
|
||||
private const val PREF_KEY_BACKUP_TOKEN = "backupToken"
|
||||
private const val PREF_KEY_BACKUP_TIME = "backupTime"
|
||||
|
||||
class SettingsManager(context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
@ -66,48 +62,6 @@ class SettingsManager(context: Context) {
|
|||
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(
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.transport.requestBackup
|
||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||
|
||||
class SettingsViewModel(
|
||||
app: Application,
|
||||
settingsManager: SettingsManager,
|
||||
keyManager: KeyManager
|
||||
keyManager: KeyManager,
|
||||
private val metadataManager: MetadataManager
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import android.os.ParcelFileDescriptor
|
|||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
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 java.io.IOException
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
|
@ -23,7 +23,7 @@ internal class BackupCoordinator(
|
|||
private val plugin: BackupPlugin,
|
||||
private val kv: KVBackup,
|
||||
private val full: FullBackup,
|
||||
private val metadataWriter: MetadataWriter,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val nm: BackupNotificationManager) {
|
||||
|
||||
|
@ -56,7 +56,7 @@ internal class BackupCoordinator(
|
|||
Log.i(TAG, "Initialize Device!")
|
||||
return try {
|
||||
plugin.initializeDevice()
|
||||
writeBackupMetadata(settingsManager.getBackupToken())
|
||||
metadataManager.onDeviceInitialization(plugin.getMetadataOutputStream())
|
||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||
// so we remember that we initialized successfully
|
||||
calledInitialize = true
|
||||
|
@ -102,15 +102,15 @@ internal class BackupCoordinator(
|
|||
}
|
||||
|
||||
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
||||
val packageName = packageInfo.packageName
|
||||
// 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
|
||||
if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
|
||||
return TRANSPORT_PACKAGE_REJECTED
|
||||
}
|
||||
|
||||
val result = kv.performBackup(packageInfo, data, flags)
|
||||
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
||||
return result
|
||||
return onPackageBackedUp(result, packageInfo)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
@ -138,8 +138,7 @@ internal class BackupCoordinator(
|
|||
|
||||
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
||||
val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
||||
return result
|
||||
return onPackageBackedUp(result, targetPackage)
|
||||
}
|
||||
|
||||
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
||||
|
@ -193,10 +192,17 @@ internal class BackupCoordinator(
|
|||
else -> throw IllegalStateException("Unexpected state in finishBackup()")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeBackupMetadata(token: Long) {
|
||||
val outputStream = plugin.getMetadataOutputStream()
|
||||
metadataWriter.write(outputStream, token)
|
||||
private fun onPackageBackedUp(result: Int, packageInfo: PackageInfo): Int {
|
||||
if (result != TRANSPORT_OK) return result
|
||||
val packageName = packageInfo.packageName
|
||||
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 {
|
||||
|
|
|
@ -10,8 +10,8 @@ import android.os.ParcelFileDescriptor
|
|||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
|
||||
|
@ -22,7 +22,7 @@ private class RestoreCoordinatorState(
|
|||
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||
|
||||
internal class RestoreCoordinator(
|
||||
private val settingsManager: SettingsManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val plugin: RestorePlugin,
|
||||
private val kv: KVRestore,
|
||||
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.
|
||||
*/
|
||||
fun getCurrentRestoreSet(): Long {
|
||||
return settingsManager.getBackupToken()
|
||||
return metadataManager.getBackupToken()
|
||||
.apply { Log.i(TAG, "Got current restore set token: $this") }
|
||||
}
|
||||
|
||||
|
|
|
@ -25,10 +25,7 @@ internal class BackupStorageViewModel(
|
|||
override fun onLocationSet(uri: Uri) {
|
||||
val isUsb = saveStorage(uri)
|
||||
|
||||
// use a new backup token
|
||||
settingsManager.getAndSaveNewBackupToken()
|
||||
|
||||
// initialize the new location
|
||||
// initialize the new location, will also generate a new backup token
|
||||
val observer = InitializationObserver()
|
||||
backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer)
|
||||
|
||||
|
|
|
@ -96,9 +96,6 @@ internal abstract class StorageViewModel(
|
|||
val storage = Storage(uri, name, root.isUsb)
|
||||
settingsManager.setStorage(storage)
|
||||
|
||||
// reset time of last backup to "Never"
|
||||
settingsManager.resetBackupTime()
|
||||
|
||||
if (storage.isUsb) {
|
||||
Log.d(TAG, "Selected storage is a removable USB device.")
|
||||
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)
|
||||
}
|
||||
|
||||
private fun getMetadata(packageMetadata: Map<String, PackageMetadata> = HashMap()): BackupMetadata {
|
||||
private fun getMetadata(packageMetadata: HashMap<String, PackageMetadata> = HashMap()): BackupMetadata {
|
||||
return BackupMetadata(
|
||||
version = 1.toByte(),
|
||||
token = Random.nextLong(),
|
||||
|
|
|
@ -66,7 +66,7 @@ internal class MetadataWriterDecoderTest {
|
|||
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(
|
||||
version = Random.nextBytes(1)[0],
|
||||
token = Random.nextLong(),
|
||||
|
|
|
@ -16,7 +16,6 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
|
|||
import com.stevesoltys.seedvault.header.HeaderWriterImpl
|
||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||
import com.stevesoltys.seedvault.metadata.MetadataWriterImpl
|
||||
import com.stevesoltys.seedvault.transport.backup.*
|
||||
import com.stevesoltys.seedvault.transport.restore.*
|
||||
import io.mockk.*
|
||||
|
@ -35,7 +34,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val headerWriter = HeaderWriterImpl()
|
||||
private val headerReader = HeaderReaderImpl()
|
||||
private val cryptoImpl = CryptoImpl(cipherFactory, headerWriter, headerReader)
|
||||
private val metadataWriter = MetadataWriterImpl(cryptoImpl)
|
||||
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||
|
||||
private val backupPlugin = mockk<BackupPlugin>()
|
||||
|
@ -44,20 +42,21 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val backup = BackupCoordinator(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 kvRestorePlugin = mockk<KVRestorePlugin>()
|
||||
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
||||
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val restore = RestoreCoordinator(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
private val restore = RestoreCoordinator(metadataManager, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
|
||||
private val backupDataInput = mockk<BackupDataInput>()
|
||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
private val token = Random.nextLong()
|
||||
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
|
||||
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
|
||||
private val metadataOutputStream = ByteArrayOutputStream()
|
||||
private val key = "RestoreKey"
|
||||
private val key64 = key.encodeBase64()
|
||||
private val key2 = "RestoreKey2"
|
||||
|
@ -92,7 +91,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
appData2.size
|
||||
}
|
||||
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
|
||||
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
|
||||
|
@ -179,7 +179,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
||||
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
||||
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
|
||||
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.pm.PackageInfo
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -16,6 +17,7 @@ abstract class TransportTest {
|
|||
|
||||
protected val crypto = mockk<Crypto>()
|
||||
protected val settingsManager = mockk<SettingsManager>()
|
||||
protected val metadataManager = mockk<MetadataManager>()
|
||||
protected val context = mockk<Context>(relaxed = true)
|
||||
|
||||
protected val packageInfo = PackageInfo().apply { packageName = "org.example" }
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.net.Uri
|
|||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.MetadataWriter
|
||||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
|
@ -24,18 +23,17 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
private val plugin = mockk<BackupPlugin>()
|
||||
private val kv = mockk<KVBackup>()
|
||||
private val full = mockk<FullBackup>()
|
||||
private val metadataWriter = mockk<MetadataWriter>()
|
||||
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>()
|
||||
|
||||
@Test
|
||||
fun `device initialization succeeds and delegates to plugin`() {
|
||||
every { plugin.initializeDevice() } just Runs
|
||||
every { settingsManager.getBackupToken() } returns token
|
||||
expectWritingMetadata(token)
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onDeviceInitialization(metadataOutputStream) } just Runs
|
||||
every { kv.hasState() } returns false
|
||||
every { full.hasState() } returns false
|
||||
|
||||
|
@ -145,9 +143,4 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
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 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 inputStream = mockk<InputStream>()
|
||||
|
@ -57,7 +57,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `getCurrentRestoreSet() delegates to plugin`() {
|
||||
every { settingsManager.getBackupToken() } returns token
|
||||
every { metadataManager.getBackupToken() } returns token
|
||||
assertEquals(token, restore.getCurrentRestoreSet())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue