Refactor and repackage Seedvault

This commit is contained in:
Steve Soltys 2024-01-02 19:33:18 -05:00
parent fa4c52fb83
commit 59a51d21b2
151 changed files with 1907 additions and 1512 deletions

View file

@ -1,11 +1,6 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="ALLOW_TRAILING_COMMA" value="true" /> <option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>

View file

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="SPDX-FileCopyrightText: &amp;#36;today.year The Calyx Institute&#10;SPDX-License-Identifier: Apache-2.0" />
<option name="myName" value="Apache-2.0" />
</copyright>
</component>

View file

@ -1,12 +1,12 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault
import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.transport.backup.InputFactory import com.stevesoltys.seedvault.service.app.backup.InputFactory
import com.stevesoltys.seedvault.transport.backup.KVBackup import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.transport.restore.FullRestore import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import com.stevesoltys.seedvault.transport.restore.OutputFactory import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
@ -26,8 +26,8 @@ class KoinInstrumentationTestApp : App() {
val context = this@KoinInstrumentationTestApp val context = this@KoinInstrumentationTestApp
single { spyk(BackupNotificationManager(context)) } single { spyk(BackupNotificationManager(context)) }
single { spyk(FullBackup(get(), get(), get(), get())) } single { spyk(FullBackupService(get(), get(), get(), get())) }
single { spyk(KVBackup(get(), get(), get(), get(), get())) } single { spyk(KVBackupService(get(), get(), get(), get(), get())) }
single { spyk(InputFactory()) } single { spyk(InputFactory()) }
single { spyk(FullRestore(get(), get(), get(), get(), get())) } single { spyk(FullRestore(get(), get(), get(), get(), get())) }

View file

@ -4,14 +4,14 @@ import androidx.test.core.content.pm.PackageInfoBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin import com.stevesoltys.seedvault.service.storage.saf.DocumentsProviderStoragePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA import com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.plugins.saf.deleteContents import com.stevesoltys.seedvault.service.storage.saf.deleteContents
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -33,9 +33,9 @@ import org.koin.core.component.inject
class PluginTest : KoinComponent { class PluginTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val settingsManager: SettingsManager by inject() private val settingsService: SettingsService by inject()
private val mockedSettingsManager: SettingsManager = mockk() private val mockedSettingsService: SettingsService = mockk()
private val storage = DocumentsStorage(context, mockedSettingsManager) private val storage = DocumentsStorage(context, mockedSettingsService)
private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage) private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage)
@ -49,7 +49,7 @@ class PluginTest : KoinComponent {
@Before @Before
fun setup() = runBlocking { fun setup() = runBlocking {
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage() every { mockedSettingsService.getStorage() } returns settingsService.getStorage()
storage.rootBackupDir?.deleteContents(context) storage.rootBackupDir?.deleteContents(context)
?: error("Select a storage location in the app first!") ?: error("Select a storage location in the app first!")
} }
@ -76,11 +76,11 @@ class PluginTest : KoinComponent {
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) { fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
// no backups available initially // no backups available initially
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size) assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
val s = settingsManager.getStorage() ?: error("no storage") val s = settingsService.getStorage() ?: error("no storage")
assertFalse(storagePlugin.hasBackup(s)) assertFalse(storagePlugin.hasBackup(s))
// prepare returned tokens requested when initializing device // prepare returned tokens requested when initializing device
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1) every { mockedSettingsService.getToken() } returnsMany listOf(token, token + 1, token + 1)
// start new restore set and initialize device afterwards // start new restore set and initialize device afterwards
storagePlugin.startNewRestoreSet(token) storagePlugin.startNewRestoreSet(token)
@ -114,7 +114,7 @@ class PluginTest : KoinComponent {
@Test @Test
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) { fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
every { mockedSettingsManager.getToken() } returns token every { mockedSettingsService.getToken() } returns token
storagePlugin.startNewRestoreSet(token) storagePlugin.startNewRestoreSet(token)
storagePlugin.initializeDevice() storagePlugin.initializeDevice()
@ -216,7 +216,7 @@ class PluginTest : KoinComponent {
} }
private fun initStorage(token: Long) = runBlocking { private fun initStorage(token: Long) = runBlocking {
every { mockedSettingsManager.getToken() } returns token every { mockedSettingsService.getToken() } returns token
storagePlugin.initializeDevice() storagePlugin.initializeDevice()
} }

View file

@ -5,9 +5,9 @@ import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.service.app.backup.InputFactory
import com.stevesoltys.seedvault.transport.backup.InputFactory import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.transport.backup.KVBackup import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery import io.mockk.coEvery
@ -27,9 +27,9 @@ internal interface LargeBackupTestBase : LargeTestBase {
val spyBackupNotificationManager: BackupNotificationManager get() = get() val spyBackupNotificationManager: BackupNotificationManager get() = get()
val spyFullBackup: FullBackup get() = get() val spyFullBackupService: FullBackupService get() = get()
val spyKVBackup: KVBackup get() = get() val spyKVBackupService: KVBackupService get() = get()
val spyInputFactory: InputFactory get() = get() val spyInputFactory: InputFactory get() = get()
@ -72,7 +72,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
return backupResult.copy( return backupResult.copy(
backupResults = backupResult.allUserApps().associate { backupResults = backupResult.allUserApps().associate {
it.packageName to spyMetadataManager.getPackageMetadata(it.packageName) it.packageName to spyMetadataService.getPackageMetadata(it.packageName)
}.toMutableMap() }.toMutableMap()
) )
} }
@ -88,7 +88,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
} }
private fun spyOnBackup(backupResult: SeedvaultLargeTestResult): AtomicBoolean { private fun spyOnBackup(backupResult: SeedvaultLargeTestResult): AtomicBoolean {
clearMocks(spyInputFactory, spyKVBackup, spyFullBackup) clearMocks(spyInputFactory, spyKVBackupService, spyFullBackupService)
spyOnFullBackupData(backupResult) spyOnFullBackupData(backupResult)
spyOnKVBackupData(backupResult) spyOnKVBackupData(backupResult)
@ -100,7 +100,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
var data = mutableMapOf<String, ByteArray>() var data = mutableMapOf<String, ByteArray>()
coEvery { coEvery {
spyKVBackup.performBackup(any(), any(), any(), any(), any()) spyKVBackupService.performBackup(any(), any(), any(), any(), any())
} answers { } answers {
packageName = firstArg<PackageInfo>().packageName packageName = firstArg<PackageInfo>().packageName
callOriginal() callOriginal()
@ -117,7 +117,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
} }
coEvery { coEvery {
spyKVBackup.finishBackup() spyKVBackupService.finishBackup()
} answers { } answers {
backupResult.kv[packageName!!] = data backupResult.kv[packageName!!] = data
.mapValues { entry -> entry.value.sha256() } .mapValues { entry -> entry.value.sha256() }
@ -134,7 +134,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
var dataIntercept = ByteArrayOutputStream() var dataIntercept = ByteArrayOutputStream()
coEvery { coEvery {
spyFullBackup.performFullBackup(any(), any(), any(), any(), any()) spyFullBackupService.performFullBackup(any(), any(), any(), any(), any())
} answers { } answers {
packageName = firstArg<PackageInfo>().packageName packageName = firstArg<PackageInfo>().packageName
callOriginal() callOriginal()
@ -150,7 +150,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
} }
every { every {
spyFullBackup.finishBackup() spyFullBackupService.finishBackup()
} answers { } answers {
val result = callOriginal() val result = callOriginal()
backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256() backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256()

View file

@ -6,9 +6,9 @@ import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
import com.stevesoltys.seedvault.transport.restore.FullRestore import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import com.stevesoltys.seedvault.transport.restore.OutputFactory import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every

View file

@ -11,21 +11,21 @@ import androidx.preference.PreferenceManager
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import com.stevesoltys.seedvault.crypto.ANDROID_KEY_STORE import com.stevesoltys.seedvault.service.crypto.ANDROID_KEY_STORE
import com.stevesoltys.seedvault.crypto.KEY_ALIAS_BACKUP import com.stevesoltys.seedvault.service.crypto.KEY_ALIAS_BACKUP
import com.stevesoltys.seedvault.crypto.KEY_ALIAS_MAIN import com.stevesoltys.seedvault.service.crypto.KEY_ALIAS_MAIN
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.currentRestoreStorageViewModel import com.stevesoltys.seedvault.currentRestoreStorageViewModel
import com.stevesoltys.seedvault.currentRestoreViewModel import com.stevesoltys.seedvault.currentRestoreViewModel
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.service.app.PackageService
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -65,13 +65,13 @@ internal interface LargeTestBase : KoinComponent {
val packageService: PackageService get() = get() val packageService: PackageService get() = get()
val settingsManager: SettingsManager get() = get() val settingsService: SettingsService get() = get()
val keyManager: KeyManager get() = get() val keyManager: KeyManager get() = get()
val documentsStorage: DocumentsStorage get() = get() val documentsStorage: DocumentsStorage get() = get()
val spyMetadataManager: MetadataManager get() = get() val spyMetadataService: MetadataService get() = get()
val backupManager: IBackupManager get() = get() val backupManager: IBackupManager get() = get()
@ -83,7 +83,7 @@ internal interface LargeTestBase : KoinComponent {
fun resetApplicationState() { fun resetApplicationState() {
backupManager.setAutoRestore(false) backupManager.setAutoRestore(false)
settingsManager.setNewToken(null) settingsService.setNewToken(null)
documentsStorage.reset(null) documentsStorage.reset(null)
val sharedPreferences = permitDiskReads { val sharedPreferences = permitDiskReads {

View file

@ -47,7 +47,7 @@ internal abstract class SeedvaultLargeTest :
if (arguments.getString("d2d_backup_test") == "true") { if (arguments.getString("d2d_backup_test") == "true") {
println("Enabling D2D backups for test") println("Enabling D2D backups for test")
settingsManager.setD2dBackupsEnabled(true) settingsService.setD2dBackupsEnabled(true)
} }
} }

View file

@ -1,8 +1,8 @@
package com.stevesoltys.seedvault.e2e package com.stevesoltys.seedvault.e2e
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.service.metadata.PackageMetadata
import com.stevesoltys.seedvault.restore.AppRestoreResult import com.stevesoltys.seedvault.ui.restore.AppRestoreResult
/** /**
* Contains maps of (package name -> SHA-256 hashes) of application data. * Contains maps of (package name -> SHA-256 hashes) of application data.

View file

@ -3,7 +3,7 @@ package com.stevesoltys.seedvault.e2e.impl
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
import com.stevesoltys.seedvault.metadata.PackageState import com.stevesoltys.seedvault.service.metadata.PackageState
import org.junit.Test import org.junit.Test
@LargeTest @LargeTest
@ -17,7 +17,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
confirmCode() confirmCode()
} }
if (settingsManager.getStorage() == null) { if (settingsService.getStorage() == null) {
chooseStorageLocation() chooseStorageLocation()
} else { } else {
changeBackupLocation() changeBackupLocation()

View file

@ -13,7 +13,11 @@ import com.stevesoltys.seedvault.assertReadEquals
import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
import com.stevesoltys.seedvault.service.storage.saf.createOrGetFile
import com.stevesoltys.seedvault.service.storage.saf.findFileBlocking
import com.stevesoltys.seedvault.service.storage.saf.getLoadedCursor
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.writeAndClose import com.stevesoltys.seedvault.writeAndClose
import io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
@ -44,8 +48,8 @@ import kotlin.random.Random
class DocumentsStorageTest : KoinComponent { class DocumentsStorageTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val settingsManager by inject<SettingsManager>() private val settingsService by inject<SettingsService>()
private val storage = DocumentsStorage(context, settingsManager) private val storage = DocumentsStorage(context, settingsService)
private val filename = getRandomBase64() private val filename = getRandomBase64()
private lateinit var file: DocumentFile private lateinit var file: DocumentFile

View file

@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.transport.backup
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import com.stevesoltys.seedvault.service.app.PackageService
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent

View file

@ -5,11 +5,11 @@
<application> <application>
<!-- Remove permission requirements only for debug versions to make development easier --> <!-- Remove permission requirements only for debug versions to make development easier -->
<activity <activity
android:name="com.stevesoltys.seedvault.settings.SettingsActivity" android:name="com.stevesoltys.seedvault.ui.settings.SettingsActivity"
android:exported="true" android:exported="true"
tools:remove="android:permission" /> tools:remove="android:permission" />
<activity <activity
android:name="com.stevesoltys.seedvault.restore.RestoreActivity" android:name="com.stevesoltys.seedvault.ui.restore.RestoreActivity"
android:exported="true" android:exported="true"
tools:remove="android:permission" /> tools:remove="android:permission" />
</application> </application>

View file

@ -87,7 +87,7 @@
tools:ignore="GoogleAppIndexingWarning"> tools:ignore="GoogleAppIndexingWarning">
<activity <activity
android:name=".settings.SettingsActivity" android:name=".ui.settings.SettingsActivity"
android:exported="true" android:exported="true"
android:permission="com.stevesoltys.seedvault.OPEN_SETTINGS" /> android:permission="com.stevesoltys.seedvault.OPEN_SETTINGS" />
@ -106,7 +106,7 @@
android:theme="@style/AppTheme.NoActionBar" /> android:theme="@style/AppTheme.NoActionBar" />
<activity <activity
android:name=".restore.RestoreActivity" android:name=".ui.restore.RestoreActivity"
android:exported="true" android:exported="true"
android:label="@string/restore_title" android:label="@string/restore_title"
android:permission="com.stevesoltys.seedvault.RESTORE_BACKUP" android:permission="com.stevesoltys.seedvault.RESTORE_BACKUP"
@ -137,7 +137,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".restore.RestoreErrorBroadcastReceiver" android:name=".ui.restore.RestoreErrorBroadcastReceiver"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.stevesoltys.seedvault.action.UNINSTALL" /> <action android:name="com.stevesoltys.seedvault.action.UNINSTALL" />
@ -145,7 +145,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".SecretCodeReceiver" android:name=".ui.restore.SecretCodeReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.telephony.action.SECRET_CODE" /> <action android:name="android.telephony.action.SECRET_CODE" />
@ -158,19 +158,19 @@
<!-- Used to start actual BackupService depending on scheduling criteria --> <!-- Used to start actual BackupService depending on scheduling criteria -->
<service <service
android:name=".storage.StorageBackupJobService" android:name=".service.file.backup.FileBackupJobService"
android:exported="false" android:exported="false"
android:label="BackupJobService" android:label="BackupJobService"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<!-- Does the actual backup work as a foreground service --> <!-- Does the actual backup work as a foreground service -->
<service <service
android:name=".storage.StorageBackupService" android:name=".service.file.backup.FileBackupService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
android:label="BackupService" /> android:label="BackupService" />
<!-- Does restore as a foreground service --> <!-- Does restore as a foreground service -->
<service <service
android:name=".storage.StorageRestoreService" android:name=".service.file.restore.FileRestoreService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
android:label="RestoreService" /> android:label="RestoreService" />

View file

@ -10,24 +10,25 @@ import android.os.Build
import android.os.ServiceManager.getService import android.os.ServiceManager.getService
import android.os.StrictMode import android.os.StrictMode
import android.os.UserManager import android.os.UserManager
import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.service.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.service.header.headerModule
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.metadata.metadataModule import com.stevesoltys.seedvault.service.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule import com.stevesoltys.seedvault.service.storage.saf.documentsProviderModule
import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
import com.stevesoltys.seedvault.restore.install.installModule import com.stevesoltys.seedvault.ui.restore.apk.installModule
import com.stevesoltys.seedvault.settings.AppListRetriever import com.stevesoltys.seedvault.ui.settings.AppListRetriever
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.settings.SettingsViewModel import com.stevesoltys.seedvault.ui.settings.SettingsViewModel
import com.stevesoltys.seedvault.storage.storageModule import com.stevesoltys.seedvault.service.file.filesModule
import com.stevesoltys.seedvault.transport.backup.backupModule import com.stevesoltys.seedvault.service.app.backup.backupModule
import com.stevesoltys.seedvault.transport.restore.restoreModule import com.stevesoltys.seedvault.service.app.restore.restoreModule
import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
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
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
import com.stevesoltys.seedvault.util.TimeSource
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@ -43,9 +44,9 @@ import org.koin.dsl.module
open class App : Application() { open class App : Application() {
private val appModule = module { private val appModule = module {
single { SettingsManager(this@App) } single { SettingsService(this@App) }
single { BackupNotificationManager(this@App) } single { BackupNotificationManager(this@App) }
single { Clock() } single { TimeSource() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
factory { AppListRetriever(this@App, get(), get(), get()) } factory { AppListRetriever(this@App, get(), get(), get()) }
@ -94,24 +95,24 @@ open class App : Application() {
backupModule, backupModule,
restoreModule, restoreModule,
installModule, installModule,
storageModule, filesModule,
appModule appModule
) )
private val settingsManager: SettingsManager by inject() private val settingsService: SettingsService by inject()
private val metadataManager: MetadataManager by inject() private val metadataService: MetadataService by inject()
/** /**
* The responsibility for the current token was moved to the [SettingsManager] * The responsibility for the current token was moved to the [SettingsService]
* in the end of 2020. * in the end of 2020.
* This method migrates the token for existing installs and can be removed * This method migrates the token for existing installs and can be removed
* after sufficient time has passed. * after sufficient time has passed.
*/ */
private fun migrateTokenFromMetadataToSettingsManager() { private fun migrateTokenFromMetadataToSettingsManager() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val token = metadataManager.getBackupToken() val token = metadataService.getBackupToken()
if (token != 0L && settingsManager.getToken() == null) { if (token != 0L && settingsService.getToken() == null) {
settingsManager.setNewToken(token) settingsService.setNewToken(token)
} }
} }

View file

@ -15,12 +15,12 @@ import android.os.Looper
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat.startForegroundService import androidx.core.content.ContextCompat.startForegroundService
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.app.backup.requestBackup
import com.stevesoltys.seedvault.settings.FlashDrive import com.stevesoltys.seedvault.service.file.backup.FileBackupService
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.file.backup.FileBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP import com.stevesoltys.seedvault.service.settings.FlashDrive
import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
import org.koin.core.context.GlobalContext.get import org.koin.core.context.GlobalContext.get
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
@ -32,17 +32,17 @@ private const val HOURS_AUTO_BACKUP: Long = 24
class UsbIntentReceiver : UsbMonitor() { class UsbIntentReceiver : UsbMonitor() {
// using KoinComponent would crash robolectric tests :( // using KoinComponent would crash robolectric tests :(
private val settingsManager: SettingsManager by lazy { get().get() } private val settingsService: SettingsService by lazy { get().get() }
private val metadataManager: MetadataManager by lazy { get().get() } private val metadataService: MetadataService by lazy { get().get() }
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
Log.d(TAG, "Checking if this is the current backup drive.") Log.d(TAG, "Checking if this is the current backup drive.")
val savedFlashDrive = settingsManager.getFlashDrive() ?: return false val savedFlashDrive = settingsService.getFlashDrive() ?: return false
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...")
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime() val backupMillis = System.currentTimeMillis() - metadataService.getLastBackupTime()
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) { if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
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
@ -57,8 +57,8 @@ class UsbIntentReceiver : UsbMonitor() {
} }
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) { override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
if (settingsManager.isStorageBackupEnabled()) { if (settingsService.isStorageBackupEnabled()) {
val i = Intent(context, StorageBackupService::class.java) val i = Intent(context, FileBackupService::class.java)
// this starts an app backup afterwards // this starts an app backup afterwards
i.putExtra(EXTRA_START_APP_BACKUP, true) i.putExtra(EXTRA_START_APP_BACKUP, true)
startForegroundService(context, i) startForegroundService(context, i)

View file

@ -1,7 +0,0 @@
package com.stevesoltys.seedvault.header
import org.koin.dsl.module
val headerModule = module {
single<HeaderReader> { HeaderReaderImpl() }
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault.service.app
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID
@ -8,9 +8,9 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.util.Log.DEBUG import android.util.Log.DEBUG
private val TAG = BackupMonitor::class.java.name private val TAG = BackupManagerOperationMonitor::class.java.name
class BackupMonitor : IBackupManagerMonitor.Stub() { class BackupManagerOperationMonitor : IBackupManagerMonitor.Stub() {
override fun onEvent(bundle: Bundle) { override fun onEvent(bundle: Bundle) {
if (!Log.isLoggable(TAG, DEBUG)) return if (!Log.isLoggable(TAG, DEBUG)) return
@ -18,5 +18,4 @@ class BackupMonitor : IBackupManagerMonitor.Stub() {
Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1)) Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?")) Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"))
} }
} }

View file

@ -1,6 +1,5 @@
package com.stevesoltys.seedvault.transport.backup package com.stevesoltys.seedvault.service.app
import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
import android.content.pm.ApplicationInfo.FLAG_STOPPED import android.content.pm.ApplicationInfo.FLAG_STOPPED
@ -12,17 +11,12 @@ import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_INSTRUMENTATION import android.content.pm.PackageManager.GET_INSTRUMENTATION
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.os.RemoteException import android.os.RemoteException
import android.os.UserHandle
import android.util.Log import android.util.Log
import android.util.Log.INFO import android.util.Log.INFO
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
private val TAG = PackageService::class.java.simpleName
private const val LOG_MAX_PACKAGES = 100
/** /**
* @author Steve Soltys * @author Steve Soltys
@ -30,13 +24,17 @@ private const val LOG_MAX_PACKAGES = 100
*/ */
internal class PackageService( internal class PackageService(
private val context: Context, private val context: Context,
private val backupManager: IBackupManager, private val settingsService: SettingsService,
private val settingsManager: SettingsManager,
private val plugin: StoragePlugin, private val plugin: StoragePlugin,
) { ) {
companion object {
private val TAG = PackageService::class.java.simpleName
private const val LOG_MAX_PACKAGES = 100
}
private val packageManager: PackageManager = context.packageManager private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId()
val eligiblePackages: Array<String> val eligiblePackages: Array<String>
@WorkerThread @WorkerThread
@ -96,7 +94,7 @@ internal class PackageService(
@WorkerThread @WorkerThread
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo -> get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
packageInfo.isUserVisible(context) && packageInfo.isUserVisible(context) &&
packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) packageInfo.allowsBackup(settingsService.d2dBackupsEnabled())
} }
/** /**
@ -105,7 +103,7 @@ internal class PackageService(
val userNotAllowedApps: List<PackageInfo> val userNotAllowedApps: List<PackageInfo>
@WorkerThread @WorkerThread
get() = packageManager.getInstalledPackages(0).filter { packageInfo -> get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
!packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) && !packageInfo.allowsBackup(settingsService.d2dBackupsEnabled()) &&
!packageInfo.isSystemApp() !packageInfo.isSystemApp()
} }
@ -140,7 +138,7 @@ internal class PackageService(
// apps that we, or the user, want to exclude. // apps that we, or the user, want to exclude.
// Check that the app is not excluded by user preference // Check that the app is not excluded by user preference
val enabled = settingsManager.isBackupEnabled(packageName) val enabled = settingsService.isBackupEnabled(packageName)
// We need to explicitly exclude DocumentsProvider and Seedvault. // We need to explicitly exclude DocumentsProvider and Seedvault.
// Otherwise, they get killed while backing them up, terminating our backup. // Otherwise, they get killed while backing them up, terminating our backup.

View file

@ -0,0 +1,54 @@
package com.stevesoltys.seedvault.service.app.backup
import android.app.backup.BackupManager
import android.app.backup.IBackupManager
import android.content.Context
import android.os.RemoteException
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.service.app.BackupManagerOperationMonitor
import com.stevesoltys.seedvault.service.app.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
import org.koin.core.context.GlobalContext
private val TAG = AppBackupService::class.java.simpleName
internal class AppBackupService(
private val context: Context,
) {
fun initiateBackup() {
requestBackup(context)
}
}
/**
* TODO: Move to above service class.
*/
@WorkerThread
fun requestBackup(context: Context) {
val backupManager: IBackupManager = GlobalContext.get().get()
if (backupManager.isBackupEnabled) {
val packageService: PackageService = GlobalContext.get().get()
val packages = packageService.eligiblePackages
val appTotals = packageService.expectedAppTotals
val result = try {
Log.d(TAG, "Backup is enabled, request backup...")
val observer = NotificationBackupObserver(context, packages.size, appTotals)
backupManager.requestBackup(packages, observer, BackupManagerOperationMonitor(), 0)
} catch (e: RemoteException) {
Log.e(TAG, "Error during backup: ", e)
val nm: BackupNotificationManager = GlobalContext.get().get()
nm.onBackupError()
}
if (result == BackupManager.SUCCESS) {
Log.i(TAG, "Backup succeeded ")
} else {
Log.e(TAG, "Backup failed: $result")
}
} else {
Log.i(TAG, "Backup is not enabled")
}
}

View file

@ -0,0 +1,62 @@
package com.stevesoltys.seedvault.service.app.backup
import com.stevesoltys.seedvault.service.app.PackageService
import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManagerImpl
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val backupModule = module {
single { InputFactory() }
single {
PackageService(
context = androidContext(),
settingsService = get(),
plugin = get()
)
}
single {
ApkBackupService(
pm = androidContext().packageManager,
cryptoService = get(),
settingsService = get(),
metadataService = get()
)
}
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
single {
KVBackupService(
plugin = get(),
settingsService = get(),
inputFactory = get(),
cryptoService = get(),
dbManager = get()
)
}
single {
FullBackupService(
plugin = get(),
settingsService = get(),
inputFactory = get(),
cryptoService = get()
)
}
single {
BackupCoordinatorService(
context = androidContext(),
plugin = get(),
kv = get(),
full = get(),
apkBackupService = get(),
timeSource = get(),
packageService = get(),
metadataService = get(),
settingsService = get(),
nm = get()
)
}
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.transport.backup package com.stevesoltys.seedvault.service.app.backup
import android.app.backup.BackupDataInput import android.app.backup.BackupDataInput
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor

View file

@ -1,4 +1,12 @@
package com.stevesoltys.seedvault.transport.backup /*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.backup.apk
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
@ -8,13 +16,15 @@ import android.content.pm.SigningInfo
import android.util.Log import android.util.Log
import android.util.PackageUtils.computeSha256DigestBytes import android.util.PackageUtils.computeSha256DigestBytes
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.util.encodeBase64
import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.service.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.service.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState import com.stevesoltys.seedvault.service.metadata.PackageState
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.app.isNotUpdatedSystemApp
import com.stevesoltys.seedvault.service.app.isTestOnly
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -23,14 +33,14 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.security.MessageDigest import java.security.MessageDigest
private val TAG = ApkBackup::class.java.simpleName private val TAG = ApkBackupService::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class ApkBackup( internal class ApkBackupService(
private val pm: PackageManager, private val pm: PackageManager,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val settingsManager: SettingsManager, private val settingsService: SettingsService,
private val metadataManager: MetadataManager, private val metadataService: MetadataService,
) { ) {
/** /**
@ -53,7 +63,7 @@ internal class ApkBackup(
if (packageName == MAGIC_PACKAGE_MANAGER) return null if (packageName == MAGIC_PACKAGE_MANAGER) return null
// do not back up when setting is not enabled // do not back up when setting is not enabled
if (!settingsManager.backupApks()) return null if (!settingsService.backupApks()) return null
// do not back up test-only apps as we can't re-install them anyway // do not back up test-only apps as we can't re-install them anyway
// see: https://commonsware.com/blog/2017/10/31/android-studio-3p0-flag-test-only.html // see: https://commonsware.com/blog/2017/10/31/android-studio-3p0-flag-test-only.html
@ -82,7 +92,7 @@ internal class ApkBackup(
} }
// get cached metadata about package // get cached metadata about package
val packageMetadata = metadataManager.getPackageMetadata(packageName) val packageMetadata = metadataService.getPackageMetadata(packageName)
?: PackageMetadata() ?: PackageMetadata()
// get version codes // get version codes
@ -104,7 +114,7 @@ internal class ApkBackup(
// get an InputStream for the APK // get an InputStream for the APK
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir) val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
// copy the APK to the storage's output and calculate SHA-256 hash while at it // copy the APK to the storage's output and calculate SHA-256 hash while at it
val name = crypto.getNameForApk(metadataManager.salt, packageName) val name = cryptoService.getNameForApk(metadataService.salt, packageName)
val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(name)) val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(name))
// back up splits if they exist // back up splits if they exist
@ -195,7 +205,7 @@ internal class ApkBackup(
} }
} }
val sha256 = messageDigest.digest().encodeBase64() val sha256 = messageDigest.digest().encodeBase64()
val name = crypto.getNameForApk(metadataManager.salt, packageName, splitName) val name = cryptoService.getNameForApk(metadataService.salt, packageName, splitName)
// copy the split APK to the storage stream // copy the split APK to the storage stream
getApkInputStream(sourceDir).use { inputStream -> getApkInputStream(sourceDir).use { inputStream ->
streamGetter(name).use { outputStream -> streamGetter(name).use { outputStream ->

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.transport.backup package com.stevesoltys.seedvault.service.app.backup.coordinator
import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED
import android.app.backup.BackupTransport.FLAG_INCREMENTAL import android.app.backup.BackupTransport.FLAG_INCREMENTAL
@ -18,62 +18,52 @@ import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.app.PackageService
import com.stevesoltys.seedvault.metadata.PackageState import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.service.app.isStopped
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.service.app.isSystemApp
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.service.metadata.BackupType
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA import com.stevesoltys.seedvault.service.metadata.PackageState
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.util.TimeSource
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.util.concurrent.TimeUnit.DAYS import java.util.concurrent.TimeUnit.DAYS
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
private val TAG = BackupCoordinator::class.java.simpleName
private class CoordinatorState(
var calledInitialize: Boolean,
var calledClearBackupData: Boolean,
var cancelReason: PackageState,
) {
val expectFinish: Boolean
get() = calledInitialize || calledClearBackupData
fun onFinish() {
calledInitialize = false
calledClearBackupData = false
cancelReason = UNKNOWN_ERROR
}
}
/** /**
* @author Steve Soltys * @author Steve Soltys
* @author Torsten Grote * @author Torsten Grote
*/ */
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok @WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class BackupCoordinator( internal class BackupCoordinatorService(
private val context: Context, private val context: Context,
private val plugin: StoragePlugin, private val plugin: StoragePlugin,
private val kv: KVBackup, private val kv: KVBackupService,
private val full: FullBackup, private val full: FullBackupService,
private val apkBackup: ApkBackup, private val apkBackupService: ApkBackupService,
private val clock: Clock, private val timeSource: TimeSource,
private val packageService: PackageService, private val packageService: PackageService,
private val metadataManager: MetadataManager, private val metadataService: MetadataService,
private val settingsManager: SettingsManager, private val settingsService: SettingsService,
private val nm: BackupNotificationManager, private val nm: BackupNotificationManager,
) { ) {
private val state = CoordinatorState( private val TAG = BackupCoordinatorService::class.java.simpleName
private val state = BackupCoordinatorState(
calledInitialize = false, calledInitialize = false,
calledClearBackupData = false, calledClearBackupData = false,
cancelReason = UNKNOWN_ERROR cancelReason = UNKNOWN_ERROR
@ -92,9 +82,9 @@ internal class BackupCoordinator(
*/ */
@Throws(IOException::class) @Throws(IOException::class)
private suspend fun startNewRestoreSet(): Long { private suspend fun startNewRestoreSet(): Long {
val token = clock.time() val token = timeSource.time()
Log.i(TAG, "Starting new RestoreSet with token $token...") Log.i(TAG, "Starting new RestoreSet with token $token...")
settingsManager.setNewToken(token) settingsService.setNewToken(token)
plugin.startNewRestoreSet(token) plugin.startNewRestoreSet(token)
return token return token
} }
@ -125,7 +115,7 @@ internal class BackupCoordinator(
plugin.initializeDevice() plugin.initializeDevice()
Log.d(TAG, "Resetting backup metadata for token $token...") Log.d(TAG, "Resetting backup metadata for token $token...")
plugin.getMetadataOutputStream(token).use { plugin.getMetadataOutputStream(token).use {
metadataManager.onDeviceInitialization(token, it) metadataService.onDeviceInitialization(token, it)
} }
// [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
@ -134,7 +124,7 @@ internal class BackupCoordinator(
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error initializing device", e) Log.e(TAG, "Error initializing device", e)
// Show error notification if we needed init or were ready for backups // Show error notification if we needed init or were ready for backups
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError() if (metadataService.requiresInit || settingsService.canDoBackupNow()) nm.onBackupError()
TRANSPORT_ERROR TRANSPORT_ERROR
} }
@ -230,7 +220,7 @@ internal class BackupCoordinator(
flags: Int, flags: Int,
): Int { ): Int {
state.cancelReason = UNKNOWN_ERROR state.cancelReason = UNKNOWN_ERROR
if (metadataManager.requiresInit) { if (metadataService.requiresInit) {
// start a new restore set to upgrade from legacy format // start a new restore set to upgrade from legacy format
// by starting a clean backup with all files using the new version // by starting a clean backup with all files using the new version
try { try {
@ -241,8 +231,8 @@ internal class BackupCoordinator(
// this causes a backup error, but things should go back to normal afterwards // this causes a backup error, but things should go back to normal afterwards
return TRANSPORT_NOT_INITIALIZED return TRANSPORT_NOT_INITIALIZED
} }
val token = settingsManager.getToken() ?: error("no token in performFullBackup") val token = settingsService.getToken() ?: error("no token in performFullBackup")
val salt = metadataManager.salt val salt = metadataService.salt
return kv.performBackup(packageInfo, data, flags, token, salt) return kv.performBackup(packageInfo, data, flags, token, salt)
} }
@ -280,8 +270,8 @@ internal class BackupCoordinator(
flags: Int, flags: Int,
): Int { ): Int {
state.cancelReason = UNKNOWN_ERROR state.cancelReason = UNKNOWN_ERROR
val token = settingsManager.getToken() ?: error("no token in performFullBackup") val token = settingsService.getToken() ?: error("no token in performFullBackup")
val salt = metadataManager.salt val salt = metadataService.salt
return full.performFullBackup(targetPackage, fileDescriptor, flags, token, salt) return full.performFullBackup(targetPackage, fileDescriptor, flags, token, salt)
} }
@ -310,8 +300,8 @@ internal class BackupCoordinator(
// don't bother with system apps that have no data // don't bother with system apps that have no data
val ignoreApp = state.cancelReason == NO_DATA && packageInfo.isSystemApp() val ignoreApp = state.cancelReason == NO_DATA && packageInfo.isSystemApp()
if (!ignoreApp) onPackageBackupError(packageInfo, BackupType.FULL) if (!ignoreApp) onPackageBackupError(packageInfo, BackupType.FULL)
val token = settingsManager.getToken() ?: error("no token in cancelFullBackup") val token = settingsService.getToken() ?: error("no token in cancelFullBackup")
val salt = metadataManager.salt val salt = metadataService.salt
full.cancelFullBackup(token, salt, ignoreApp) full.cancelFullBackup(token, salt, ignoreApp)
} }
@ -329,8 +319,8 @@ internal class BackupCoordinator(
suspend fun clearBackupData(packageInfo: PackageInfo): Int { suspend fun clearBackupData(packageInfo: PackageInfo): Int {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
Log.i(TAG, "Clear Backup Data of $packageName.") Log.i(TAG, "Clear Backup Data of $packageName.")
val token = settingsManager.getToken() ?: error("no token in clearBackupData") val token = settingsService.getToken() ?: error("no token in clearBackupData")
val salt = metadataManager.salt val salt = metadataService.salt
try { try {
kv.clearBackupData(packageInfo, token, salt) kv.clearBackupData(packageInfo, token, salt)
} catch (e: IOException) { } catch (e: IOException) {
@ -368,7 +358,7 @@ internal class BackupCoordinator(
if (result == TRANSPORT_OK) { if (result == TRANSPORT_OK) {
val isPmBackup = packageName == MAGIC_PACKAGE_MANAGER val isPmBackup = packageName == MAGIC_PACKAGE_MANAGER
// call onPackageBackedUp for @pm@ only if we can do backups right now // call onPackageBackedUp for @pm@ only if we can do backups right now
if (!isPmBackup || settingsManager.canDoBackupNow()) { if (!isPmBackup || settingsService.canDoBackupNow()) {
try { try {
onPackageBackedUp(packageInfo, BackupType.KV) onPackageBackedUp(packageInfo, BackupType.KV)
} catch (e: Exception) { } catch (e: Exception) {
@ -377,7 +367,7 @@ internal class BackupCoordinator(
} }
} }
// hook in here to back up APKs of apps that are otherwise not allowed for backup // hook in here to back up APKs of apps that are otherwise not allowed for backup
if (isPmBackup && settingsManager.canDoBackupNow()) { if (isPmBackup && settingsService.canDoBackupNow()) {
try { try {
backUpApksOfNotBackedUpPackages() backUpApksOfNotBackedUpPackages()
} catch (e: Exception) { } catch (e: Exception) {
@ -426,7 +416,7 @@ internal class BackupCoordinator(
val wasBackedUp = backUpApk(packageInfo, packageState) val wasBackedUp = backUpApk(packageInfo, packageState)
if (!wasBackedUp) { if (!wasBackedUp) {
val packageMetadata = val packageMetadata =
metadataManager.getPackageMetadata(packageName) metadataService.getPackageMetadata(packageName)
val oldPackageState = packageMetadata?.state val oldPackageState = packageMetadata?.state
if (oldPackageState != packageState) { if (oldPackageState != packageState) {
Log.i( Log.i(
@ -434,7 +424,7 @@ internal class BackupCoordinator(
", update to $packageState" ", update to $packageState"
) )
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackupError(packageInfo, packageState, it) metadataService.onPackageBackupError(packageInfo, packageState, it)
} }
} }
} }
@ -455,12 +445,12 @@ internal class BackupCoordinator(
): Boolean { ): Boolean {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
return try { return try {
apkBackup.backupApkIfNecessary(packageInfo, packageState) { name -> apkBackupService.backupApkIfNecessary(packageInfo, packageState) { name ->
val token = settingsManager.getToken() ?: throw IOException("no current token") val token = settingsService.getToken() ?: throw IOException("no current token")
plugin.getOutputStream(token, name) plugin.getOutputStream(token, name)
}?.let { packageMetadata -> }?.let { packageMetadata ->
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onApkBackedUp(packageInfo, packageMetadata, it) metadataService.onApkBackedUp(packageInfo, packageMetadata, it)
} }
true true
} ?: false } ?: false
@ -472,7 +462,7 @@ internal class BackupCoordinator(
private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) { private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackedUp(packageInfo, type, it) metadataService.onPackageBackedUp(packageInfo, type, it)
} }
} }
@ -480,7 +470,7 @@ internal class BackupCoordinator(
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
try { try {
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackupError(packageInfo, state.cancelReason, it, type) metadataService.onPackageBackupError(packageInfo, state.cancelReason, it, type)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error while writing metadata for $packageName", e) Log.e(TAG, "Error while writing metadata for $packageName", e)
@ -491,7 +481,7 @@ internal class BackupCoordinator(
val longBackoff = DAYS.toMillis(30) val longBackoff = DAYS.toMillis(30)
// back off if there's no storage set // back off if there's no storage set
val storage = settingsManager.getStorage() ?: return longBackoff val storage = settingsService.getStorage() ?: return longBackoff
return when { return when {
// back off if storage is removable and not available right now // back off if storage is removable and not available right now
storage.isUnavailableUsb(context) -> longBackoff storage.isUnavailableUsb(context) -> longBackoff
@ -503,8 +493,10 @@ internal class BackupCoordinator(
} }
private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream { private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream {
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token") val t = token ?: settingsService.getToken() ?: throw IOException("no current token")
return getOutputStream(t, FILE_BACKUP_METADATA) return getOutputStream(t,
com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
)
} }
} }

View file

@ -0,0 +1,22 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.backup.coordinator
import com.stevesoltys.seedvault.service.metadata.PackageState
class BackupCoordinatorState(
var calledInitialize: Boolean,
var calledClearBackupData: Boolean,
var cancelReason: PackageState,
) {
val expectFinish: Boolean
get() = calledInitialize || calledClearBackupData
fun onFinish() {
calledInitialize = false
calledClearBackupData = false
cancelReason = PackageState.UNKNOWN_ERROR
}
}

View file

@ -1,4 +1,8 @@
package com.stevesoltys.seedvault.transport.backup /*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.backup.full
import android.app.backup.BackupTransport.FLAG_USER_INITIATED import android.app.backup.BackupTransport.FLAG_USER_INITIATED
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_ERROR
@ -8,41 +12,26 @@ import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.app.backup.InputFactory
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.header.getADForFull import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.header.getADForFull
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.EOFException import java.io.EOFException
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
private class FullBackupState(
val packageInfo: PackageInfo,
val inputFileDescriptor: ParcelFileDescriptor,
val inputStream: InputStream,
var outputStreamInit: (suspend () -> OutputStream)?,
) {
/**
* This is an encrypted stream that can be written to directly.
*/
var outputStream: OutputStream? = null
val packageName: String = packageInfo.packageName
var size: Long = 0
}
const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong() const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
private val TAG = FullBackup::class.java.simpleName private val TAG = FullBackupService::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup( internal class FullBackupService(
private val plugin: StoragePlugin, private val plugin: StoragePlugin,
private val settingsManager: SettingsManager, private val settingsService: SettingsService,
private val inputFactory: InputFactory, private val inputFactory: InputFactory,
private val crypto: Crypto, private val cryptoService: CryptoService,
) { ) {
private var state: FullBackupState? = null private var state: FullBackupState? = null
@ -52,7 +41,7 @@ internal class FullBackup(
fun getCurrentPackage() = state?.packageInfo fun getCurrentPackage() = state?.packageInfo
fun getQuota(): Long { fun getQuota(): Long {
return if (settingsManager.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP return if (settingsService.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP
} }
fun checkFullBackupSize(size: Long): Int { fun checkFullBackupSize(size: Long): Int {
@ -114,7 +103,7 @@ internal class FullBackup(
val inputStream = inputFactory.getInputStream(socket) val inputStream = inputFactory.getInputStream(socket)
state = FullBackupState(targetPackage, socket, inputStream) { state = FullBackupState(targetPackage, socket, inputStream) {
Log.d(TAG, "Initializing OutputStream for $packageName.") Log.d(TAG, "Initializing OutputStream for $packageName.")
val name = crypto.getNameForPackage(salt, packageName) val name = cryptoService.getNameForPackage(salt, packageName)
// get OutputStream to write backup data into // get OutputStream to write backup data into
val outputStream = try { val outputStream = try {
plugin.getOutputStream(token, name) plugin.getOutputStream(token, name)
@ -127,7 +116,10 @@ internal class FullBackup(
// store version header // store version header
val state = this.state ?: throw AssertionError() val state = this.state ?: throw AssertionError()
outputStream.write(ByteArray(1) { VERSION }) outputStream.write(ByteArray(1) { VERSION })
crypto.newEncryptingStream(outputStream, getADForFull(VERSION, state.packageName)) cryptoService.newEncryptingStream(
outputStream,
getADForFull(VERSION, state.packageName)
)
} // this lambda is only called before we actually write backup data the first time } // this lambda is only called before we actually write backup data the first time
return TRANSPORT_OK return TRANSPORT_OK
} }
@ -173,7 +165,7 @@ internal class FullBackup(
@Throws(IOException::class) @Throws(IOException::class)
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) { suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
val name = crypto.getNameForPackage(salt, packageInfo.packageName) val name = cryptoService.getNameForPackage(salt, packageInfo.packageName)
plugin.removeData(token, name) plugin.removeData(token, name)
} }

View file

@ -0,0 +1,20 @@
package com.stevesoltys.seedvault.service.app.backup.full
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import java.io.InputStream
import java.io.OutputStream
class FullBackupState(
val packageInfo: PackageInfo,
val inputFileDescriptor: ParcelFileDescriptor,
val inputStream: InputStream,
var outputStreamInit: (suspend () -> OutputStream)?,
) {
/**
* This is an encrypted stream that can be written to directly.
*/
var outputStream: OutputStream? = null
val packageName: String = packageInfo.packageName
var size: Long = 0
}

View file

@ -1,4 +1,8 @@
package com.stevesoltys.seedvault.transport.backup /*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.backup.kv
import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED
import android.app.backup.BackupTransport.FLAG_INCREMENTAL import android.app.backup.BackupTransport.FLAG_INCREMENTAL
@ -10,33 +14,25 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV import com.stevesoltys.seedvault.service.header.getADForKV
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.app.backup.InputFactory
import java.io.IOException import java.io.IOException
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
class KVBackupState(
internal val packageInfo: PackageInfo,
val token: Long,
val name: String,
val db: KVDb,
) {
var needsUpload: Boolean = false
}
const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong() const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
private val TAG = KVBackup::class.java.simpleName private val TAG = KVBackupService::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackup( internal class KVBackupService(
private val plugin: StoragePlugin, private val plugin: StoragePlugin,
private val settingsManager: SettingsManager, private val settingsService: SettingsService,
private val inputFactory: InputFactory, private val inputFactory: InputFactory,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val dbManager: KvDbManager, private val dbManager: KvDbManager,
) { ) {
@ -46,7 +42,7 @@ internal class KVBackup(
fun getCurrentPackage() = state?.packageInfo fun getCurrentPackage() = state?.packageInfo
fun getQuota(): Long = if (settingsManager.isQuotaUnlimited()) { fun getQuota(): Long = if (settingsService.isQuotaUnlimited()) {
Long.MAX_VALUE Long.MAX_VALUE
} else { } else {
DEFAULT_QUOTA_KEY_VALUE_BACKUP DEFAULT_QUOTA_KEY_VALUE_BACKUP
@ -84,7 +80,7 @@ internal class KVBackup(
if (state != null) { if (state != null) {
throw AssertionError("Have state for ${state.packageInfo.packageName}") throw AssertionError("Have state for ${state.packageInfo.packageName}")
} }
val name = crypto.getNameForPackage(salt, packageName) val name = cryptoService.getNameForPackage(salt, packageName)
val db = dbManager.getDb(packageName) val db = dbManager.getDb(packageName)
this.state = KVBackupState(packageInfo, token, name, db) this.state = KVBackupState(packageInfo, token, name, db)
@ -134,7 +130,7 @@ internal class KVBackup(
// K/V backups (typically starting with package manager metadata - @pm@) // K/V backups (typically starting with package manager metadata - @pm@)
// are scheduled with JobInfo.Builder#setOverrideDeadline() // are scheduled with JobInfo.Builder#setOverrideDeadline()
// and thus do not respect backoff. // and thus do not respect backoff.
settingsManager.canDoBackupNow() settingsService.canDoBackupNow()
} else { } else {
// all other packages always need upload // all other packages always need upload
true true
@ -194,7 +190,7 @@ internal class KVBackup(
@Throws(IOException::class) @Throws(IOException::class)
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) { suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
Log.i(TAG, "Clearing K/V data of ${packageInfo.packageName}") Log.i(TAG, "Clearing K/V data of ${packageInfo.packageName}")
val name = state?.name ?: crypto.getNameForPackage(salt, packageInfo.packageName) val name = state?.name ?: cryptoService.getNameForPackage(salt, packageInfo.packageName)
plugin.removeData(token, name) plugin.removeData(token, name)
if (!dbManager.deleteDb(packageInfo.packageName)) throw IOException() if (!dbManager.deleteDb(packageInfo.packageName)) throw IOException()
} }
@ -244,7 +240,7 @@ internal class KVBackup(
plugin.getOutputStream(token, name).use { outputStream -> plugin.getOutputStream(token, name).use { outputStream ->
outputStream.write(ByteArray(1) { VERSION }) outputStream.write(ByteArray(1) { VERSION })
val ad = getADForKV(VERSION, packageName) val ad = getADForKV(VERSION, packageName)
crypto.newEncryptingStream(outputStream, ad).use { encryptedStream -> cryptoService.newEncryptingStream(outputStream, ad).use { encryptedStream ->
GZIPOutputStream(encryptedStream).use { gZipStream -> GZIPOutputStream(encryptedStream).use { gZipStream ->
dbManager.getDbInputStream(packageName).use { inputStream -> dbManager.getDbInputStream(packageName).use { inputStream ->
inputStream.copyTo(gZipStream) inputStream.copyTo(gZipStream)

View file

@ -0,0 +1,16 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.backup.kv
import android.content.pm.PackageInfo
class KVBackupState(
internal val packageInfo: PackageInfo,
val token: Long,
val name: String,
val db: KVDb,
) {
var needsUpload: Boolean = false
}

View file

@ -1,4 +1,8 @@
package com.stevesoltys.seedvault.transport.backup /*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.backup.kv
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context

View file

@ -0,0 +1,10 @@
package com.stevesoltys.seedvault.service.app.restore
import com.stevesoltys.seedvault.ui.restore.RestorableBackup
class AppRestoreService {
fun initiateRestore(restorableBackup: RestorableBackup) {
}
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.transport.restore package com.stevesoltys.seedvault.service.app.restore
import android.app.backup.BackupDataOutput import android.app.backup.BackupDataOutput
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
@ -17,5 +17,4 @@ internal class OutputFactory {
fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream { fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
return FileOutputStream(outputFileDescriptor.fileDescriptor) return FileOutputStream(outputFileDescriptor.fileDescriptor)
} }
} }

View file

@ -1,5 +1,8 @@
package com.stevesoltys.seedvault.transport.restore package com.stevesoltys.seedvault.service.app.restore
import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.transport.restore package com.stevesoltys.seedvault.service.app.restore.coordinator
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_OK
@ -13,15 +13,17 @@ import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import com.stevesoltys.seedvault.metadata.BackupMetadata import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.metadata.DecryptionFailedException import com.stevesoltys.seedvault.service.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.service.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.metadata.MetadataReader
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -35,26 +37,14 @@ import java.io.IOException
*/ */
internal const val D2D_DEVICE_NAME = "D2D" internal const val D2D_DEVICE_NAME = "D2D"
private data class RestoreCoordinatorState(
val token: Long,
val packages: Iterator<PackageInfo>,
/**
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/
val autoRestorePackageInfo: PackageInfo?,
val backupMetadata: BackupMetadata,
) {
var currentPackage: String? = null
}
private val TAG = RestoreCoordinator::class.java.simpleName private val TAG = RestoreCoordinator::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreCoordinator( internal class RestoreCoordinator(
private val context: Context, private val context: Context,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val settingsManager: SettingsManager, private val settingsService: SettingsService,
private val metadataManager: MetadataManager, private val metadataService: MetadataService,
private val notificationManager: BackupNotificationManager, private val notificationManager: BackupNotificationManager,
private val plugin: StoragePlugin, private val plugin: StoragePlugin,
private val kv: KVRestore, private val kv: KVRestore,
@ -126,7 +116,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.getToken() ?: 0L).apply { return (settingsService.getToken() ?: 0L).apply {
Log.i(TAG, "Got current restore set token: $this") Log.i(TAG, "Got current restore set token: $this")
} }
} }
@ -138,7 +128,7 @@ internal class RestoreCoordinator(
this.backupMetadata = backupMetadata this.backupMetadata = backupMetadata
if (backupMetadata.d2dBackup) { if (backupMetadata.d2dBackup) {
settingsManager.setD2dBackupsEnabled(true) settingsService.setD2dBackupsEnabled(true)
} }
} }
@ -167,9 +157,9 @@ internal class RestoreCoordinator(
// check if the backup is on removable storage that is not plugged in // check if the backup is on removable storage that is not plugged in
if (isStorageRemovableAndNotAvailable()) { if (isStorageRemovableAndNotAvailable()) {
// check if we even have a backup of that app // check if we even have a backup of that app
if (metadataManager.getPackageMetadata(pmPackageName) != null) { if (metadataService.getPackageMetadata(pmPackageName) != null) {
// remind user to plug in storage device // remind user to plug in storage device
val storageName = settingsManager.getStorage()?.name val storageName = settingsService.getStorage()?.name
?: context.getString(R.string.settings_backup_location_none) ?: context.getString(R.string.settings_backup_location_none)
notificationManager.onRemovableStorageNotAvailableForRestore( notificationManager.onRemovableStorageNotAvailableForRestore(
pmPackageName, pmPackageName,
@ -231,7 +221,8 @@ internal class RestoreCoordinator(
val type = try { val type = try {
when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) { when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
BackupType.KV -> { BackupType.KV -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) val name =
cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
if (plugin.hasData(state.token, name)) { if (plugin.hasData(state.token, name)) {
Log.i(TAG, "Found K/V data for $packageName.") Log.i(TAG, "Found K/V data for $packageName.")
kv.initializeState( kv.initializeState(
@ -247,7 +238,8 @@ internal class RestoreCoordinator(
} }
BackupType.FULL -> { BackupType.FULL -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) val name =
cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
if (plugin.hasData(state.token, name)) { if (plugin.hasData(state.token, name)) {
Log.i(TAG, "Found full backup data for $packageName.") Log.i(TAG, "Found full backup data for $packageName.")
full.initializeState(version, state.token, name, packageInfo) full.initializeState(version, state.token, name, packageInfo)
@ -365,7 +357,7 @@ internal class RestoreCoordinator(
// TODO this is plugin specific, needs to be factored out when supporting different plugins // TODO this is plugin specific, needs to be factored out when supporting different plugins
private fun isStorageRemovableAndNotAvailable(): Boolean { private fun isStorageRemovableAndNotAvailable(): Boolean {
val storage = settingsManager.getStorage() ?: return false val storage = settingsService.getStorage() ?: return false
return storage.isUnavailableUsb(context) return storage.isUnavailableUsb(context)
} }

View file

@ -0,0 +1,20 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.restore.coordinator
import android.content.pm.PackageInfo
import com.stevesoltys.seedvault.service.metadata.BackupMetadata
data class RestoreCoordinatorState(
val token: Long,
val packages: Iterator<PackageInfo>,
/**
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/
val autoRestorePackageInfo: PackageInfo?,
val backupMetadata: BackupMetadata,
) {
var currentPackage: String? = null
}

View file

@ -1,4 +1,9 @@
package com.stevesoltys.seedvault.transport.restore /*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.restore.full
import android.app.backup.BackupTransport.NO_MORE_DATA import android.app.backup.BackupTransport.NO_MORE_DATA
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_ERROR
@ -7,29 +12,20 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.header.getADForFull import com.stevesoltys.seedvault.service.header.HeaderDecodeService
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.service.header.getADForFull
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.EOFException import java.io.EOFException
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
private class FullRestoreState(
val version: Byte,
val token: Long,
val name: String,
val packageInfo: PackageInfo,
) {
var inputStream: InputStream? = null
}
private val TAG = FullRestore::class.java.simpleName private val TAG = FullRestore::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
@ -38,8 +34,8 @@ internal class FullRestore(
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin, private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory, private val outputFactory: OutputFactory,
private val headerReader: HeaderReader, private val headerDecodeService: HeaderDecodeService,
private val crypto: Crypto, private val cryptoService: CryptoService,
) { ) {
private var state: FullRestoreState? = null private var state: FullRestoreState? = null
@ -104,15 +100,15 @@ internal class FullRestore(
if (state.version == 0.toByte()) { if (state.version == 0.toByte()) {
val inputStream = val inputStream =
legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo) legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo)
val version = headerReader.readVersion(inputStream, state.version) val version = headerDecodeService.readVersion(inputStream, state.version)
@Suppress("deprecation") @Suppress("deprecation")
crypto.decryptHeader(inputStream, version, packageName) cryptoService.decryptHeader(inputStream, version, packageName)
state.inputStream = inputStream state.inputStream = inputStream
} else { } else {
val inputStream = plugin.getInputStream(state.token, state.name) val inputStream = plugin.getInputStream(state.token, state.name)
val version = headerReader.readVersion(inputStream, state.version) val version = headerDecodeService.readVersion(inputStream, state.version)
val ad = getADForFull(version, packageName) val ad = getADForFull(version, packageName)
state.inputStream = crypto.newDecryptingStream(inputStream, ad) state.inputStream = cryptoService.newDecryptingStream(inputStream, ad)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.w(TAG, "Error getting input stream for $packageName", e) Log.w(TAG, "Error getting input stream for $packageName", e)
@ -148,7 +144,7 @@ internal class FullRestore(
// read segment from input stream and decrypt it // read segment from input stream and decrypt it
val decrypted = try { val decrypted = try {
@Suppress("deprecation") @Suppress("deprecation")
crypto.decryptSegment(inputStream) cryptoService.decryptSegment(inputStream)
} catch (e: EOFException) { } catch (e: EOFException) {
Log.i(TAG, " EOF") Log.i(TAG, " EOF")
// close input stream here as we won't need it anymore // close input stream here as we won't need it anymore

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.restore.full
import android.content.pm.PackageInfo
import java.io.InputStream
class FullRestoreState(
val version: Byte,
val token: Long,
val name: String,
val packageInfo: PackageInfo,
) {
var inputStream: InputStream? = null
}

View file

@ -1,4 +1,9 @@
package com.stevesoltys.seedvault.transport.restore /*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.restore.kv
import android.app.backup.BackupDataOutput import android.app.backup.BackupDataOutput
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_ERROR
@ -9,34 +14,23 @@ import android.util.Log
import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY
import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
import com.stevesoltys.seedvault.decodeBase64 import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.service.header.HeaderDecodeService
import com.stevesoltys.seedvault.header.getADForKV import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.header.getADForKV
import com.stevesoltys.seedvault.transport.backup.KVDb import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.transport.backup.KvDbManager import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.util.decodeBase64
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.util.ArrayList
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import javax.crypto.AEADBadTagException import javax.crypto.AEADBadTagException
private class KVRestoreState(
val version: Byte,
val token: Long,
val name: String,
val packageInfo: PackageInfo,
/**
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
*/
val autoRestorePackageInfo: PackageInfo?,
)
private val TAG = KVRestore::class.java.simpleName private val TAG = KVRestore::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
@ -45,8 +39,8 @@ internal class KVRestore(
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin, private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory, private val outputFactory: OutputFactory,
private val headerReader: HeaderReader, private val headerDecodeService: HeaderDecodeService,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val dbManager: KvDbManager, private val dbManager: KvDbManager,
) { ) {
@ -153,9 +147,9 @@ internal class KVRestore(
private suspend fun downloadRestoreDb(state: KVRestoreState): KVDb { private suspend fun downloadRestoreDb(state: KVRestoreState): KVDb {
val packageName = state.packageInfo.packageName val packageName = state.packageInfo.packageName
plugin.getInputStream(state.token, state.name).use { inputStream -> plugin.getInputStream(state.token, state.name).use { inputStream ->
headerReader.readVersion(inputStream, state.version) headerDecodeService.readVersion(inputStream, state.version)
val ad = getADForKV(VERSION, packageName) val ad = getADForKV(VERSION, packageName)
crypto.newDecryptingStream(inputStream, ad).use { decryptedStream -> cryptoService.newDecryptingStream(inputStream, ad).use { decryptedStream ->
GZIPInputStream(decryptedStream).use { gzipStream -> GZIPInputStream(decryptedStream).use { gzipStream ->
dbManager.getDbOutputStream(packageName).use { outputStream -> dbManager.getDbOutputStream(packageName).use { outputStream ->
gzipStream.copyTo(outputStream) gzipStream.copyTo(outputStream)
@ -241,10 +235,10 @@ internal class KVRestore(
out: BackupDataOutput, out: BackupDataOutput,
) = legacyPlugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key) ) = legacyPlugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
.use { inputStream -> .use { inputStream ->
val version = headerReader.readVersion(inputStream, state.version) val version = headerDecodeService.readVersion(inputStream, state.version)
val packageName = state.packageInfo.packageName val packageName = state.packageInfo.packageName
crypto.decryptHeader(inputStream, version, packageName, dKey.key) cryptoService.decryptHeader(inputStream, version, packageName, dKey.key)
val value = crypto.decryptMultipleSegments(inputStream) val value = cryptoService.decryptMultipleSegments(inputStream)
val size = value.size val size = value.size
Log.v(TAG, " ... key=${dKey.key} size=$size") Log.v(TAG, " ... key=${dKey.key} size=$size")

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.app.restore.kv
import android.content.pm.PackageInfo
class KVRestoreState(
val version: Byte,
val token: Long,
val name: String,
val packageInfo: PackageInfo,
/**
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
*/
val autoRestorePackageInfo: PackageInfo?,
)

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.service.crypto
import java.security.Key import java.security.Key
import javax.crypto.Cipher import javax.crypto.Cipher

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.service.crypto
import org.koin.dsl.module import org.koin.dsl.module
import java.security.KeyStore import java.security.KeyStore
@ -15,5 +15,5 @@ val cryptoModule = module {
} }
KeyManagerImpl(keyStore) KeyManagerImpl(keyStore)
} }
single<Crypto> { CryptoImpl(get(), get(), get()) } single<CryptoService> { CryptoServiceImpl(get(), get(), get()) }
} }

View file

@ -0,0 +1,108 @@
package com.stevesoltys.seedvault.service.crypto
import com.google.crypto.tink.subtle.AesGcmHkdfStreaming
import com.stevesoltys.seedvault.service.header.SegmentHeader
import com.stevesoltys.seedvault.service.header.VersionHeader
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.GeneralSecurityException
import java.security.SecureRandom
/**
* A version 1 backup stream uses [AesGcmHkdfStreaming] from the tink library.
*
* A version 0 backup stream starts with a version byte followed by an encrypted [VersionHeader].
*
* The header will be encrypted with AES/GCM to provide authentication.
* It can be read using [decryptHeader] which throws a [SecurityException],
* if the expected version and package name do not match the encrypted header.
*
* After the header, follows one or more data segments.
* Each segment begins with a clear-text [SegmentHeader]
* that contains the length of the segment
* and a nonce acting as the initialization vector for the encryption.
* The segment can be read using [decryptSegment] which throws a [SecurityException],
* if the length of the segment is specified larger than allowed.
*/
internal interface CryptoService {
/**
* Returns a ByteArray with bytes retrieved from [SecureRandom].
*/
fun getRandomBytes(size: Int): ByteArray
fun getNameForPackage(salt: String, packageName: String): String
/**
* Returns the name that identifies an APK in the backup storage plugin.
* @param suffix empty string for normal APKs and the name of the split in case of an APK split
*/
fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
/**
* Returns a [AesGcmHkdfStreaming] encrypting stream
* that gets encrypted and authenticated the given associated data.
*/
@Throws(IOException::class, GeneralSecurityException::class)
fun newEncryptingStream(
outputStream: OutputStream,
associatedData: ByteArray,
): OutputStream
/**
* Returns a [AesGcmHkdfStreaming] decrypting stream
* that gets decrypted and authenticated the given associated data.
*/
@Throws(IOException::class, GeneralSecurityException::class)
fun newDecryptingStream(
inputStream: InputStream,
associatedData: ByteArray,
): InputStream
/**
* Reads and decrypts a [VersionHeader] from the given [InputStream]
* and ensures that the expected version, package name and key match
* what is found in the header.
* If a mismatch is found, a [SecurityException] is thrown.
*
* @return The read [VersionHeader] present in the beginning of the given [InputStream].
*/
@Suppress("Deprecation")
@Deprecated("Use newDecryptingStream instead")
@Throws(IOException::class, SecurityException::class)
fun decryptHeader(
inputStream: InputStream,
expectedVersion: Byte,
expectedPackageName: String,
expectedKey: String? = null,
): VersionHeader
/**
* Reads and decrypts a segment from the given [InputStream].
*
* @return The decrypted segment payload.
*/
@Deprecated("Use newDecryptingStream instead")
@Throws(EOFException::class, IOException::class, SecurityException::class)
fun decryptSegment(inputStream: InputStream): ByteArray
/**
* Like [decryptSegment], but decrypts multiple segments and does not throw [EOFException].
*/
@Deprecated("Use newDecryptingStream instead")
@Throws(IOException::class, SecurityException::class)
fun decryptMultipleSegments(inputStream: InputStream): ByteArray
/**
* Verify that the stored backup key was created from the given seed.
*
* @return true if the key was created from given seed, false otherwise.
*/
fun verifyBackupKey(seed: ByteArray): Boolean
}
internal const val TYPE_METADATA: Byte = 0x00
internal const val TYPE_BACKUP_KV: Byte = 0x01
internal const val TYPE_BACKUP_FULL: Byte = 0x02

View file

@ -1,14 +1,11 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.service.crypto
import com.google.crypto.tink.subtle.AesGcmHkdfStreaming import com.stevesoltys.seedvault.service.header.HeaderDecodeService
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.service.header.MAX_VERSION_HEADER_SIZE
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH import com.stevesoltys.seedvault.service.header.VersionHeader
import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE import com.stevesoltys.seedvault.util.encodeBase64
import com.stevesoltys.seedvault.header.SegmentHeader
import com.stevesoltys.seedvault.header.VersionHeader
import org.calyxos.backup.storage.crypto.StreamCrypto import org.calyxos.backup.storage.crypto.StreamCrypto
import org.calyxos.backup.storage.crypto.StreamCrypto.deriveStreamKey
import java.io.EOFException import java.io.EOFException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -19,111 +16,14 @@ import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
/** internal class CryptoServiceImpl(
* A version 1 backup stream uses [AesGcmHkdfStreaming] from the tink library.
*
* A version 0 backup stream starts with a version byte followed by an encrypted [VersionHeader].
*
* The header will be encrypted with AES/GCM to provide authentication.
* It can be read using [decryptHeader] which throws a [SecurityException],
* if the expected version and package name do not match the encrypted header.
*
* After the header, follows one or more data segments.
* Each segment begins with a clear-text [SegmentHeader]
* that contains the length of the segment
* and a nonce acting as the initialization vector for the encryption.
* The segment can be read using [decryptSegment] which throws a [SecurityException],
* if the length of the segment is specified larger than allowed.
*/
internal interface Crypto {
/**
* Returns a ByteArray with bytes retrieved from [SecureRandom].
*/
fun getRandomBytes(size: Int): ByteArray
fun getNameForPackage(salt: String, packageName: String): String
/**
* Returns the name that identifies an APK in the backup storage plugin.
* @param suffix empty string for normal APKs and the name of the split in case of an APK split
*/
fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
/**
* Returns a [AesGcmHkdfStreaming] encrypting stream
* that gets encrypted and authenticated the given associated data.
*/
@Throws(IOException::class, GeneralSecurityException::class)
fun newEncryptingStream(
outputStream: OutputStream,
associatedData: ByteArray,
): OutputStream
/**
* Returns a [AesGcmHkdfStreaming] decrypting stream
* that gets decrypted and authenticated the given associated data.
*/
@Throws(IOException::class, GeneralSecurityException::class)
fun newDecryptingStream(
inputStream: InputStream,
associatedData: ByteArray,
): InputStream
/**
* Reads and decrypts a [VersionHeader] from the given [InputStream]
* and ensures that the expected version, package name and key match
* what is found in the header.
* If a mismatch is found, a [SecurityException] is thrown.
*
* @return The read [VersionHeader] present in the beginning of the given [InputStream].
*/
@Suppress("Deprecation")
@Deprecated("Use newDecryptingStream instead")
@Throws(IOException::class, SecurityException::class)
fun decryptHeader(
inputStream: InputStream,
expectedVersion: Byte,
expectedPackageName: String,
expectedKey: String? = null,
): VersionHeader
/**
* Reads and decrypts a segment from the given [InputStream].
*
* @return The decrypted segment payload.
*/
@Deprecated("Use newDecryptingStream instead")
@Throws(EOFException::class, IOException::class, SecurityException::class)
fun decryptSegment(inputStream: InputStream): ByteArray
/**
* Like [decryptSegment], but decrypts multiple segments and does not throw [EOFException].
*/
@Deprecated("Use newDecryptingStream instead")
@Throws(IOException::class, SecurityException::class)
fun decryptMultipleSegments(inputStream: InputStream): ByteArray
/**
* Verify that the stored backup key was created from the given seed.
*
* @return true if the key was created from given seed, false otherwise.
*/
fun verifyBackupKey(seed: ByteArray): Boolean
}
internal const val TYPE_METADATA: Byte = 0x00
internal const val TYPE_BACKUP_KV: Byte = 0x01
internal const val TYPE_BACKUP_FULL: Byte = 0x02
internal class CryptoImpl(
private val keyManager: KeyManager, private val keyManager: KeyManager,
private val cipherFactory: CipherFactory, private val cipherFactory: CipherFactory,
private val headerReader: HeaderReader, private val headerDecodeService: HeaderDecodeService,
) : Crypto { ) : CryptoService {
private val key: ByteArray by lazy { private val key: ByteArray by lazy {
deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray()) StreamCrypto.deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray())
} }
private val secureRandom: SecureRandom by lazy { SecureRandom() } private val secureRandom: SecureRandom by lazy { SecureRandom() }
@ -175,7 +75,7 @@ internal class CryptoImpl(
expectedKey: String?, expectedKey: String?,
): VersionHeader { ): VersionHeader {
val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE) val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
val header = headerReader.getVersionHeader(decrypted) val header = headerDecodeService.getVersionHeader(decrypted)
if (header.version != expectedVersion) { if (header.version != expectedVersion) {
throw SecurityException( throw SecurityException(
@ -219,7 +119,7 @@ internal class CryptoImpl(
@Suppress("Deprecation") @Suppress("Deprecation")
@Throws(EOFException::class, IOException::class, SecurityException::class) @Throws(EOFException::class, IOException::class, SecurityException::class)
private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray { private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
val segmentHeader = headerReader.readSegmentHeader(inputStream) val segmentHeader = headerDecodeService.readSegmentHeader(inputStream)
if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException( if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException(
"Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength" "Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength"
) )

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.service.crypto
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE

View file

@ -0,0 +1,14 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.file
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.api.StoragePlugin
import org.koin.dsl.module
val filesModule = module {
single<StoragePlugin> { FileBackupStoragePlugin(get(), get(), get()) }
single { StorageBackup(get(), get()) }
}

View file

@ -1,14 +1,17 @@
package com.stevesoltys.seedvault.storage package com.stevesoltys.seedvault.service.file
import android.content.Context import android.content.Context
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.getStorageContext import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
import javax.crypto.SecretKey import javax.crypto.SecretKey
internal class SeedvaultStoragePlugin( /**
* [SafStoragePlugin] for backing up files.
*/
internal class FileBackupStoragePlugin(
private val appContext: Context, private val appContext: Context,
private val storage: DocumentsStorage, private val storage: DocumentsStorage,
private val keyManager: KeyManager, private val keyManager: KeyManager,

View file

@ -0,0 +1,19 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.file.backup
import org.calyxos.backup.storage.backup.BackupJobService
/*
test and debug with
adb shell dumpsys jobscheduler |
grep -A 23 -B 4 "Service: com.stevesoltys.seedvault/.storage.StorageBackupJobService"
force running with:
adb shell cmd jobscheduler run -f com.stevesoltys.seedvault 0
*/
internal class FileBackupJobService : BackupJobService(FileBackupService::class.java)

View file

@ -0,0 +1,32 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.file.backup
import android.content.Intent
import com.stevesoltys.seedvault.service.app.backup.requestBackup
import org.calyxos.backup.storage.api.BackupObserver
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupService
import org.calyxos.backup.storage.backup.NotificationBackupObserver
import org.koin.android.ext.android.inject
internal class FileBackupService : BackupService() {
companion object {
internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
}
override val storageBackup: StorageBackup by inject()
// use lazy delegate because context isn't available during construction time
override val backupObserver: BackupObserver by lazy {
NotificationBackupObserver(applicationContext)
}
override fun onBackupFinished(intent: Intent, success: Boolean) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
requestBackup(applicationContext)
}
}
}

View file

@ -0,0 +1,19 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.service.file.restore
import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
import org.calyxos.backup.storage.restore.RestoreService
import org.koin.android.ext.android.inject
internal class FileRestoreService : RestoreService() {
override val storageBackup: StorageBackup by inject()
// use lazy delegate because context isn't available during construction time
override val restoreObserver: RestoreObserver by lazy {
NotificationRestoreObserver(applicationContext)
}
}

View file

@ -1,8 +1,8 @@
package com.stevesoltys.seedvault.header package com.stevesoltys.seedvault.service.header
import com.stevesoltys.seedvault.crypto.GCM_AUTHENTICATION_TAG_LENGTH import com.stevesoltys.seedvault.service.crypto.GCM_AUTHENTICATION_TAG_LENGTH
import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_FULL import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_FULL
import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_KV import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_KV
import java.nio.ByteBuffer import java.nio.ByteBuffer
internal const val VERSION: Byte = 1 internal const val VERSION: Byte = 1

View file

@ -0,0 +1,22 @@
package com.stevesoltys.seedvault.service.header
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
internal interface HeaderDecodeService {
@Throws(IOException::class, UnsupportedVersionException::class)
fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte
@Suppress("Deprecation")
@Deprecated("For restoring v0 backups only")
@Throws(SecurityException::class)
fun getVersionHeader(byteArray: ByteArray): VersionHeader
@Suppress("Deprecation")
@Deprecated("For restoring v0 backups only")
@Throws(EOFException::class, IOException::class)
fun readSegmentHeader(inputStream: InputStream): SegmentHeader
}
class UnsupportedVersionException(val version: Byte) : IOException()

View file

@ -1,28 +1,13 @@
package com.stevesoltys.seedvault.header package com.stevesoltys.seedvault.service.header
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.util.Utf8
import java.io.EOFException import java.io.EOFException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
internal interface HeaderReader { internal class HeaderDecodeServiceImpl : HeaderDecodeService {
@Throws(IOException::class, UnsupportedVersionException::class)
fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte
@Suppress("Deprecation")
@Deprecated("For restoring v0 backups only")
@Throws(SecurityException::class)
fun getVersionHeader(byteArray: ByteArray): VersionHeader
@Suppress("Deprecation")
@Deprecated("For restoring v0 backups only")
@Throws(EOFException::class, IOException::class)
fun readSegmentHeader(inputStream: InputStream): SegmentHeader
}
internal class HeaderReaderImpl : HeaderReader {
@Throws(IOException::class, UnsupportedVersionException::class, GeneralSecurityException::class) @Throws(IOException::class, UnsupportedVersionException::class, GeneralSecurityException::class)
override fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte { override fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte {
@ -85,5 +70,3 @@ internal class HeaderReaderImpl : HeaderReader {
} }
} }
class UnsupportedVersionException(val version: Byte) : IOException()

View file

@ -0,0 +1,7 @@
package com.stevesoltys.seedvault.service.header
import org.koin.dsl.module
val headerModule = module {
single<HeaderDecodeService> { HeaderDecodeServiceImpl() }
}

View file

@ -1,10 +1,10 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.service.metadata
import android.content.pm.ApplicationInfo.FLAG_STOPPED import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.os.Build import android.os.Build
import com.stevesoltys.seedvault.crypto.TYPE_METADATA import com.stevesoltys.seedvault.service.crypto.TYPE_METADATA
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
import java.nio.ByteBuffer import java.nio.ByteBuffer

View file

@ -1,10 +1,10 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.service.metadata
import org.koin.android.ext.koin.androidContext 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(), get(), get()) } single { MetadataService(androidContext(), get(), get(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) } single<MetadataWriter> { MetadataWriterImpl(get()) }
single<MetadataReader> { MetadataReaderImpl(get()) } single<MetadataReader> { MetadataReaderImpl(get()) }
} }

View file

@ -1,15 +1,15 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.service.metadata
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.util.Utf8
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -36,7 +36,7 @@ interface MetadataReader {
} }
internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { internal class MetadataReaderImpl(private val cryptoService: CryptoService) : MetadataReader {
@Throws( @Throws(
SecurityException::class, SecurityException::class,
@ -51,7 +51,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken) if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken)
val metadataBytes = try { val metadataBytes = try {
crypto.newDecryptingStream(inputStream, getAD(version, expectedToken)).readBytes() cryptoService.newDecryptingStream(inputStream, getAD(version, expectedToken))
.readBytes()
} catch (e: GeneralSecurityException) { } catch (e: GeneralSecurityException) {
throw DecryptionFailedException(e) throw DecryptionFailedException(e)
} }
@ -67,7 +68,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
@Suppress("Deprecation") @Suppress("Deprecation")
private fun readMetadataV0(inputStream: InputStream, expectedToken: Long): BackupMetadata { private fun readMetadataV0(inputStream: InputStream, expectedToken: Long): BackupMetadata {
val metadataBytes = try { val metadataBytes = try {
crypto.decryptMultipleSegments(inputStream) cryptoService.decryptMultipleSegments(inputStream)
} catch (e: AEADBadTagException) { } catch (e: AEADBadTagException) {
throw DecryptionFailedException(e) throw DecryptionFailedException(e)
} }

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.service.metadata
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
@ -9,34 +9,34 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.service.app.isSystemApp
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.util.TimeSource
import com.stevesoltys.seedvault.transport.backup.isSystemApp import com.stevesoltys.seedvault.util.encodeBase64
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
private val TAG = MetadataManager::class.java.simpleName private val TAG = MetadataService::class.java.simpleName
@VisibleForTesting @VisibleForTesting
internal const val METADATA_CACHE_FILE = "metadata.cache" internal const val METADATA_CACHE_FILE = "metadata.cache"
internal const val METADATA_SALT_SIZE = 32 internal const val METADATA_SALT_SIZE = 32
@WorkerThread @WorkerThread
internal class MetadataManager( internal class MetadataService(
private val context: Context, private val context: Context,
private val clock: Clock, private val timeSource: TimeSource,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val metadataWriter: MetadataWriter, private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader, private val metadataReader: MetadataReader,
private val settingsManager: SettingsManager private val settingsService: SettingsService
) { ) {
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "") private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
@ -63,7 +63,7 @@ internal class MetadataManager(
@Synchronized @Synchronized
@Throws(IOException::class) @Throws(IOException::class)
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) { fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64() val salt = cryptoService.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
modifyMetadata(metadataOutputStream) { modifyMetadata(metadataOutputStream) {
metadata = BackupMetadata(token = token, salt = salt) metadata = BackupMetadata(token = token, salt = salt)
} }
@ -135,9 +135,9 @@ internal class MetadataManager(
) { ) {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
modifyMetadata(metadataOutputStream) { modifyMetadata(metadataOutputStream) {
val now = clock.time() val now = timeSource.time()
metadata.time = now metadata.time = now
metadata.d2dBackup = settingsManager.d2dBackupsEnabled() metadata.d2dBackup = settingsService.d2dBackupsEnabled()
if (metadata.packageMetadataMap.containsKey(packageName)) { if (metadata.packageMetadataMap.containsKey(packageName)) {
metadata.packageMetadataMap[packageName]!!.time = now metadata.packageMetadataMap[packageName]!!.time = now

View file

@ -1,8 +1,8 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.service.metadata
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.util.Utf8
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -15,12 +15,13 @@ interface MetadataWriter {
fun encode(metadata: BackupMetadata): ByteArray fun encode(metadata: BackupMetadata): ByteArray
} }
internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { internal class MetadataWriterImpl(private val cryptoService: CryptoService) : MetadataWriter {
@Throws(IOException::class) @Throws(IOException::class)
override fun write(metadata: BackupMetadata, outputStream: OutputStream) { override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
outputStream.write(ByteArray(1).apply { this[0] = metadata.version }) outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
crypto.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token)).use { cryptoService.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token))
.use {
it.write(encode(metadata)) it.write(encode(metadata))
} }
} }

View file

@ -0,0 +1,19 @@
package com.stevesoltys.seedvault.service.settings
import android.hardware.usb.UsbDevice
data class FlashDrive(
val name: String,
val serialNumber: String?,
val vendorId: Int,
val productId: Int,
) {
companion object {
fun from(device: UsbDevice) = FlashDrive(
name = "${device.manufacturerName} ${device.productName}",
serialNumber = device.serialNumber,
vendorId = device.vendorId,
productId = device.productId
)
}
}

View file

@ -1,17 +1,14 @@
package com.stevesoltys.seedvault.settings package com.stevesoltys.seedvault.service.settings
import android.content.Context import android.content.Context
import android.hardware.usb.UsbDevice
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri import android.net.Uri
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.stevesoltys.seedvault.getStorageContext import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
import com.stevesoltys.seedvault.ui.settings.AppStatus
import java.util.concurrent.ConcurrentSkipListSet import java.util.concurrent.ConcurrentSkipListSet
internal const val PREF_KEY_TOKEN = "token" internal const val PREF_KEY_TOKEN = "token"
@ -34,7 +31,9 @@ private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota" private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups" internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
class SettingsManager(private val context: Context) { class SettingsService(
private val context: Context,
) {
private val prefs = permitDiskReads { private val prefs = permitDiskReads {
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
@ -59,7 +58,7 @@ class SettingsManager(private val context: Context) {
/** /**
* Sets a new RestoreSet token. * Sets a new RestoreSet token.
* Should only be called by the [BackupCoordinator] * Should only be called by the [BackupCoordinatorService]
* to ensure that related work is performed after moving to a new token. * to ensure that related work is performed after moving to a new token.
*/ */
fun setNewToken(newToken: Long?) { fun setNewToken(newToken: Long?) {
@ -162,54 +161,3 @@ class SettingsManager(private val context: Context) {
.apply() .apply()
} }
} }
data class Storage(
val uri: Uri,
val name: String,
val isUsb: Boolean,
val requiresNetwork: Boolean,
) {
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
?: throw AssertionError("Should only happen on API < 21.")
/**
* Returns true if this is USB storage that is not available, false otherwise.
*
* Must be run off UI thread (ideally I/O).
*/
@WorkerThread
fun isUnavailableUsb(context: Context): Boolean {
return isUsb && !getDocumentFile(context).isDirectory
}
/**
* Returns true if this is storage that requires network access,
* but it isn't available right now.
*/
fun isUnavailableNetwork(context: Context): Boolean {
return requiresNetwork && !hasUnmeteredInternet(context)
}
private fun hasUnmeteredInternet(context: Context): Boolean {
val cm = context.getSystemService(ConnectivityManager::class.java)
val isMetered = cm.isActiveNetworkMetered
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered
}
}
data class FlashDrive(
val name: String,
val serialNumber: String?,
val vendorId: Int,
val productId: Int,
) {
companion object {
fun from(device: UsbDevice) = FlashDrive(
name = "${device.manufacturerName} ${device.productName}",
serialNumber = device.serialNumber,
vendorId = device.vendorId,
productId = device.productId
)
}
}

View file

@ -0,0 +1,43 @@
package com.stevesoltys.seedvault.service.settings
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
data class Storage(
val uri: Uri,
val name: String,
val isUsb: Boolean,
val requiresNetwork: Boolean,
) {
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
?: throw AssertionError("Should only happen on API < 21.")
/**
* Returns true if this is USB storage that is not available, false otherwise.
*
* Must be run off UI thread (ideally I/O).
*/
@WorkerThread
fun isUnavailableUsb(context: Context): Boolean {
return isUsb && !getDocumentFile(context).isDirectory
}
/**
* Returns true if this is storage that requires network access,
* but it isn't available right now.
*/
fun isUnavailableNetwork(context: Context): Boolean {
return requiresNetwork && !hasUnmeteredInternet(context)
}
private fun hasUnmeteredInternet(context: Context): Boolean {
val cm = context.getSystemService(ConnectivityManager::class.java)
val isMetered = cm.isActiveNetworkMetered
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered
}
}

View file

@ -0,0 +1,8 @@
package com.stevesoltys.seedvault.service.storage
import java.io.InputStream
class EncryptedBackupMetadata(
val token: Long,
val inputStreamRetriever: () -> InputStream,
)

View file

@ -1,8 +1,8 @@
package com.stevesoltys.seedvault.plugins package com.stevesoltys.seedvault.service.storage
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.service.settings.Storage
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -61,7 +61,7 @@ interface StoragePlugin {
* @return metadata for the set of restore images available, * @return metadata for the set of restore images available,
* or null if an error occurred (the attempt should be rescheduled). * or null if an error occurred (the attempt should be rescheduled).
**/ **/
suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
/** /**
* Returns the package name of the app that provides the backend storage * Returns the package name of the app that provides the backend storage
@ -74,5 +74,3 @@ interface StoragePlugin {
val providerPackageName: String? val providerPackageName: String?
} }
class EncryptedMetadata(val token: Long, val inputStreamRetriever: () -> InputStream)

View file

@ -1,7 +1,8 @@
package com.stevesoltys.seedvault.plugins.saf package com.stevesoltys.seedvault.service.storage.saf
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module

View file

@ -1,13 +1,13 @@
package com.stevesoltys.seedvault.plugins.saf package com.stevesoltys.seedvault.service.storage.saf
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.getStorageContext import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.plugins.EncryptedMetadata import com.stevesoltys.seedvault.service.storage.EncryptedBackupMetadata
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.service.settings.Storage
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -90,14 +90,14 @@ internal class DocumentsProviderStoragePlugin(
return backupSets.isNotEmpty() return backupSets.isNotEmpty()
} }
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? { override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
val rootDir = storage.rootBackupDir ?: return null val rootDir = storage.rootBackupDir ?: return null
val backupSets = getBackups(context, rootDir) val backupSets = getBackups(context, rootDir)
val iterator = backupSets.iterator() val iterator = backupSets.iterator()
return generateSequence { return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence if (!iterator.hasNext()) return@generateSequence null // end sequence
val backupSet = iterator.next() val backupSet = iterator.next()
EncryptedMetadata(backupSet.token) { EncryptedBackupMetadata(backupSet.token) {
storage.getInputStream(backupSet.metadataFile) storage.getInputStream(backupSet.metadataFile)
} }
} }

View file

@ -1,6 +1,6 @@
@file:Suppress("BlockingMethodInNonBlockingContext") @file:Suppress("BlockingMethodInNonBlockingContext")
package com.stevesoltys.seedvault.plugins.saf package com.stevesoltys.seedvault.service.storage.saf
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
@ -17,8 +17,8 @@ import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.getStorageContext import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.service.settings.Storage
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -43,11 +43,11 @@ private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage( internal class DocumentsStorage(
private val appContext: Context, private val appContext: Context,
private val settingsManager: SettingsManager, private val settingsService: SettingsService,
) { ) {
internal var storage: Storage? = null internal var storage: Storage? = null
get() { get() {
if (field == null) field = settingsManager.getStorage() if (field == null) field = settingsService.getStorage()
return field return field
} }
@ -81,7 +81,7 @@ internal class DocumentsStorage(
private var currentToken: Long? = null private var currentToken: Long? = null
get() { get() {
if (field == null) field = settingsManager.getToken() if (field == null) field = settingsService.getToken()
return field return field
} }

View file

@ -1,16 +1,20 @@
package com.stevesoltys.seedvault.plugins.saf package com.stevesoltys.seedvault.service.storage.saf.legacy
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
import com.stevesoltys.seedvault.service.storage.saf.assertRightFile
import com.stevesoltys.seedvault.service.storage.saf.findFileBlocking
import com.stevesoltys.seedvault.service.storage.saf.listFilesBlocking
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@WorkerThread @WorkerThread
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O @Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
@Deprecated("Only for old v0 backup format")
internal class DocumentsProviderLegacyPlugin( internal class DocumentsProviderLegacyPlugin(
private val context: Context, private val context: Context,
private val storage: DocumentsStorage, private val storage: DocumentsStorage,

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.plugins package com.stevesoltys.seedvault.service.storage.saf.legacy
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import java.io.IOException import java.io.IOException

View file

@ -1,55 +0,0 @@
package com.stevesoltys.seedvault.storage
import android.content.Intent
import com.stevesoltys.seedvault.transport.requestBackup
import org.calyxos.backup.storage.api.BackupObserver
import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.backup.storage.backup.BackupService
import org.calyxos.backup.storage.backup.NotificationBackupObserver
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
import org.calyxos.backup.storage.restore.RestoreService
import org.koin.android.ext.android.inject
/*
test and debug with
adb shell dumpsys jobscheduler |
grep -A 23 -B 4 "Service: com.stevesoltys.seedvault/.storage.StorageBackupJobService"
force running with:
adb shell cmd jobscheduler run -f com.stevesoltys.seedvault 0
*/
internal class StorageBackupJobService : BackupJobService(StorageBackupService::class.java)
internal class StorageBackupService : BackupService() {
companion object {
internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
}
override val storageBackup: StorageBackup by inject()
// use lazy delegate because context isn't available during construction time
override val backupObserver: BackupObserver by lazy {
NotificationBackupObserver(applicationContext)
}
override fun onBackupFinished(intent: Intent, success: Boolean) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
requestBackup(applicationContext)
}
}
}
internal class StorageRestoreService : RestoreService() {
override val storageBackup: StorageBackup by inject()
// use lazy delegate because context isn't available during construction time
override val restoreObserver: RestoreObserver by lazy {
NotificationRestoreObserver(applicationContext)
}
}

View file

@ -1,10 +0,0 @@
package com.stevesoltys.seedvault.storage
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.api.StoragePlugin
import org.koin.dsl.module
val storageModule = module {
single<StoragePlugin> { SeedvaultStoragePlugin(get(), get(), get()) }
single { StorageBackup(get(), get()) }
}

View file

@ -11,10 +11,10 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsActivity import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.ui.settings.SettingsActivity
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -36,9 +36,9 @@ private val TAG = ConfigurableBackupTransport::class.java.simpleName
class ConfigurableBackupTransport internal constructor(private val context: Context) : class ConfigurableBackupTransport internal constructor(private val context: Context) :
BackupTransport(), KoinComponent { BackupTransport(), KoinComponent {
private val backupCoordinator by inject<BackupCoordinator>() private val backupCoordinatorService by inject<BackupCoordinatorService>()
private val restoreCoordinator by inject<RestoreCoordinator>() private val restoreCoordinator by inject<RestoreCoordinator>()
private val settingsManager by inject<SettingsManager>() private val settingsService by inject<SettingsService>()
override fun transportDirName(): String { override fun transportDirName(): String {
return TRANSPORT_DIRECTORY_NAME return TRANSPORT_DIRECTORY_NAME
@ -58,7 +58,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
* This allows the agent to decide what to do based on properties of the transport. * This allows the agent to decide what to do based on properties of the transport.
*/ */
override fun getTransportFlags(): Int { override fun getTransportFlags(): Int {
return if (settingsManager.d2dBackupsEnabled()) { return if (settingsService.d2dBackupsEnabled()) {
D2D_TRANSPORT_FLAGS D2D_TRANSPORT_FLAGS
} else { } else {
DEFAULT_TRANSPORT_FLAGS DEFAULT_TRANSPORT_FLAGS
@ -120,26 +120,26 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
// //
override fun initializeDevice(): Int = runBlocking { override fun initializeDevice(): Int = runBlocking {
backupCoordinator.initializeDevice() backupCoordinatorService.initializeDevice()
} }
override fun isAppEligibleForBackup( override fun isAppEligibleForBackup(
targetPackage: PackageInfo, targetPackage: PackageInfo,
isFullBackup: Boolean, isFullBackup: Boolean,
): Boolean { ): Boolean {
return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup) return backupCoordinatorService.isAppEligibleForBackup(targetPackage, isFullBackup)
} }
override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking { override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
backupCoordinator.getBackupQuota(packageName, isFullBackup) backupCoordinatorService.getBackupQuota(packageName, isFullBackup)
} }
override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking { override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
backupCoordinator.clearBackupData(packageInfo) backupCoordinatorService.clearBackupData(packageInfo)
} }
override fun finishBackup(): Int = runBlocking { override fun finishBackup(): Int = runBlocking {
backupCoordinator.finishBackup() backupCoordinatorService.finishBackup()
} }
// ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------
@ -147,7 +147,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
// //
override fun requestBackupTime(): Long { override fun requestBackupTime(): Long {
return backupCoordinator.requestBackupTime() return backupCoordinatorService.requestBackupTime()
} }
override fun performBackup( override fun performBackup(
@ -155,7 +155,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
inFd: ParcelFileDescriptor, inFd: ParcelFileDescriptor,
flags: Int, flags: Int,
): Int = runBlocking { ): Int = runBlocking {
backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags) backupCoordinatorService.performIncrementalBackup(packageInfo, inFd, flags)
} }
override fun performBackup( override fun performBackup(
@ -171,11 +171,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
// //
override fun requestFullBackupTime(): Long { override fun requestFullBackupTime(): Long {
return backupCoordinator.requestFullBackupTime() return backupCoordinatorService.requestFullBackupTime()
} }
override fun checkFullBackupSize(size: Long): Int { override fun checkFullBackupSize(size: Long): Int {
return backupCoordinator.checkFullBackupSize(size) return backupCoordinatorService.checkFullBackupSize(size)
} }
override fun performFullBackup( override fun performFullBackup(
@ -183,7 +183,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
socket: ParcelFileDescriptor, socket: ParcelFileDescriptor,
flags: Int, flags: Int,
): Int = runBlocking { ): Int = runBlocking {
backupCoordinator.performFullBackup(targetPackage, socket, flags) backupCoordinatorService.performFullBackup(targetPackage, socket, flags)
} }
override fun performFullBackup( override fun performFullBackup(
@ -191,15 +191,15 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
fileDescriptor: ParcelFileDescriptor, fileDescriptor: ParcelFileDescriptor,
): Int = runBlocking { ): Int = runBlocking {
Log.w(TAG, "Warning: Legacy performFullBackup() method called.") Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0) backupCoordinatorService.performFullBackup(targetPackage, fileDescriptor, 0)
} }
override fun sendBackupData(numBytes: Int): Int = runBlocking { override fun sendBackupData(numBytes: Int): Int = runBlocking {
backupCoordinator.sendBackupData(numBytes) backupCoordinatorService.sendBackupData(numBytes)
} }
override fun cancelFullBackup() = runBlocking { override fun cancelFullBackup() = runBlocking {
backupCoordinator.cancelFullBackup() backupCoordinatorService.cancelFullBackup()
} }
// ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------

View file

@ -1,22 +1,14 @@
package com.stevesoltys.seedvault.transport package com.stevesoltys.seedvault.transport
import android.app.Service import android.app.Service
import android.app.backup.BackupManager
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.get
private val TAG = ConfigurableBackupTransportService::class.java.simpleName private val TAG = ConfigurableBackupTransportService::class.java.simpleName
@ -59,30 +51,3 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
} }
} }
@WorkerThread
fun requestBackup(context: Context) {
val backupManager: IBackupManager = get().get()
if (backupManager.isBackupEnabled) {
val packageService: PackageService = get().get()
val packages = packageService.eligiblePackages
val appTotals = packageService.expectedAppTotals
val result = try {
Log.d(TAG, "Backup is enabled, request backup...")
val observer = NotificationBackupObserver(context, packages.size, appTotals)
backupManager.requestBackup(packages, observer, BackupMonitor(), 0)
} catch (e: RemoteException) {
Log.e(TAG, "Error during backup: ", e)
val nm: BackupNotificationManager = get().get()
nm.onBackupError()
}
if (result == BackupManager.SUCCESS) {
Log.i(TAG, "Backup succeeded ")
} else {
Log.e(TAG, "Backup failed: $result")
}
} else {
Log.i(TAG, "Backup is not enabled")
}
}

View file

@ -1,56 +0,0 @@
package com.stevesoltys.seedvault.transport.backup
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val backupModule = module {
single { InputFactory() }
single {
PackageService(
context = androidContext(),
backupManager = get(),
settingsManager = get(),
plugin = get()
)
}
single {
ApkBackup(
pm = androidContext().packageManager,
crypto = get(),
settingsManager = get(),
metadataManager = get()
)
}
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
single {
KVBackup(
plugin = get(),
settingsManager = get(),
inputFactory = get(),
crypto = get(),
dbManager = get()
)
}
single {
FullBackup(
plugin = get(),
settingsManager = get(),
inputFactory = get(),
crypto = get()
)
}
single {
BackupCoordinator(
context = androidContext(),
plugin = get(),
kv = get(),
full = get(),
apkBackup = get(),
clock = get(),
packageService = get(),
metadataManager = get(),
settingsManager = get(),
nm = get()
)
}
}

View file

@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
abstract class BackupActivity : AppCompatActivity() { abstract class BackupActivityBase : AppCompatActivity() {
@CallSuper @CallSuper
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {

View file

@ -5,7 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsViewModel import com.stevesoltys.seedvault.ui.settings.SettingsViewModel
import org.calyxos.backup.storage.ui.backup.BackupContentFragment import org.calyxos.backup.storage.ui.backup.BackupContentFragment
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel

View file

@ -1,9 +1,9 @@
package com.stevesoltys.seedvault.ui package com.stevesoltys.seedvault.ui.liveevent
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.stevesoltys.seedvault.ui.LiveEvent.ConsumableEvent import com.stevesoltys.seedvault.ui.liveevent.LiveEvent.ConsumableEvent
open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() { open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() {

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.ui; package com.stevesoltys.seedvault.ui.liveevent;
public interface LiveEventHandler<T> { public interface LiveEventHandler<T> {
void onEvent(T t); void onEvent(T t);

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.ui package com.stevesoltys.seedvault.ui.liveevent
class MutableLiveEvent<T> : LiveEvent<T>() { class MutableLiveEvent<T> : LiveEvent<T>() {

View file

@ -20,12 +20,12 @@ import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_HIGH import androidx.core.app.NotificationCompat.PRIORITY_HIGH
import androidx.core.app.NotificationCompat.PRIORITY_LOW import androidx.core.app.NotificationCompat.PRIORITY_LOW
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL import com.stevesoltys.seedvault.ui.restore.ACTION_RESTORE_ERROR_UNINSTALL
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME import com.stevesoltys.seedvault.ui.restore.EXTRA_PACKAGE_NAME
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL import com.stevesoltys.seedvault.ui.restore.REQUEST_CODE_UNINSTALL
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST import com.stevesoltys.seedvault.ui.settings.ACTION_APP_STATUS_LIST
import com.stevesoltys.seedvault.settings.SettingsActivity import com.stevesoltys.seedvault.ui.settings.SettingsActivity
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
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"

View file

@ -9,8 +9,8 @@ import android.util.Log.INFO
import android.util.Log.isLoggable import android.util.Log.isLoggable
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals import com.stevesoltys.seedvault.service.metadata.MetadataService
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -23,7 +23,7 @@ internal class NotificationBackupObserver(
) : IBackupObserver.Stub(), KoinComponent { ) : IBackupObserver.Stub(), KoinComponent {
private val nm: BackupNotificationManager by inject() private val nm: BackupNotificationManager by inject()
private val metadataManager: MetadataManager by inject() private val metadataService: MetadataService by inject()
private var currentPackage: String? = null private var currentPackage: String? = null
private var numPackages: Int = 0 private var numPackages: Int = 0
@ -77,7 +77,7 @@ internal class NotificationBackupObserver(
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status") Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
} }
val success = status == 0 val success = status == 0
val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null val numBackedUp = if (success) metadataService.getPackagesNumBackedUp() else null
nm.onBackupFinished(success, numBackedUp) nm.onBackupFinished(success, numBackedUp)
} }

View file

@ -1,10 +1,11 @@
package com.stevesoltys.seedvault.ui package com.stevesoltys.seedvault.ui.provision
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import com.stevesoltys.seedvault.ui.BackupActivityBase
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeActivity import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeActivity
import com.stevesoltys.seedvault.ui.storage.StorageActivity import com.stevesoltys.seedvault.ui.storage.StorageActivity
@ -19,7 +20,7 @@ private val TAG = RequireProvisioningActivity::class.java.name
* An Activity that requires the recovery code and the backup location to be set up * An Activity that requires the recovery code and the backup location to be set up
* before starting. * before starting.
*/ */
abstract class RequireProvisioningActivity : BackupActivity() { abstract class RequireProvisioningActivity : BackupActivityBase() {
private val recoveryCodeRequest = private val recoveryCodeRequest =
registerForActivityResult(StartActivityForResult()) { result -> registerForActivityResult(StartActivityForResult()) { result ->

View file

@ -1,14 +1,16 @@
package com.stevesoltys.seedvault.ui package com.stevesoltys.seedvault.ui.provision
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
import com.stevesoltys.seedvault.ui.storage.StorageViewModel import com.stevesoltys.seedvault.ui.storage.StorageViewModel
abstract class RequireProvisioningViewModel( abstract class RequireProvisioningViewModel(
protected val app: Application, protected val app: Application,
protected val settingsManager: SettingsManager, protected val settingsService: SettingsService,
protected val keyManager: KeyManager, protected val keyManager: KeyManager,
) : AndroidViewModel(app) { ) : AndroidViewModel(app) {
@ -18,7 +20,7 @@ abstract class RequireProvisioningViewModel(
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true) internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsManager) internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsService)
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey() internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()

View file

@ -5,11 +5,11 @@ import android.view.MenuItem
import android.view.WindowManager.LayoutParams.FLAG_SECURE import android.view.WindowManager.LayoutParams.FLAG_SECURE
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.isDebugBuild import com.stevesoltys.seedvault.isDebugBuild
import com.stevesoltys.seedvault.ui.BackupActivity import com.stevesoltys.seedvault.ui.BackupActivityBase
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_RESTORE
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class RecoveryCodeActivity : BackupActivity() { class RecoveryCodeActivity : BackupActivityBase() {
private val viewModel: RecoveryCodeViewModel by viewModel() private val viewModel: RecoveryCodeViewModel by viewModel()

View file

@ -10,12 +10,12 @@ import cash.z.ecc.android.bip39.Mnemonics.InvalidWordException
import cash.z.ecc.android.bip39.Mnemonics.WordCountException import cash.z.ecc.android.bip39.Mnemonics.WordCountException
import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.bip39.toSeed
import com.stevesoltys.seedvault.App import com.stevesoltys.seedvault.App
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -29,17 +29,17 @@ private val TAG = RecoveryCodeViewModel::class.java.simpleName
internal class RecoveryCodeViewModel( internal class RecoveryCodeViewModel(
app: App, app: App,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val keyManager: KeyManager, private val keyManager: KeyManager,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val backupCoordinator: BackupCoordinator, private val backupCoordinatorService: BackupCoordinatorService,
private val notificationManager: BackupNotificationManager, private val notificationManager: BackupNotificationManager,
private val storageBackup: StorageBackup, private val storageBackup: StorageBackup,
) : AndroidViewModel(app) { ) : AndroidViewModel(app) {
internal val wordList: List<CharArray> by lazy { internal val wordList: List<CharArray> by lazy {
// we use our own entropy to not having to trust the library to use SecureRandom // we use our own entropy to not having to trust the library to use SecureRandom
val entropy = crypto.getRandomBytes(Mnemonics.WordCount.COUNT_12.bitLength / 8) val entropy = cryptoService.getRandomBytes(Mnemonics.WordCount.COUNT_12.bitLength / 8)
// create the words from the entropy // create the words from the entropy
Mnemonics.MnemonicCode(entropy).words Mnemonics.MnemonicCode(entropy).words
} }
@ -73,7 +73,7 @@ internal class RecoveryCodeViewModel(
fun verifyExistingCode(input: List<CharSequence>) { fun verifyExistingCode(input: List<CharSequence>) {
// we validate the code again, just in case // we validate the code again, just in case
val seed = validateCode(input).toSeed() val seed = validateCode(input).toSeed()
val verified = crypto.verifyBackupKey(seed) val verified = cryptoService.verifyBackupKey(seed)
// store main key at this opportunity if it is still missing // store main key at this opportunity if it is still missing
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed) if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
mExistingCodeChecked.setEvent(verified) mExistingCodeChecked.setEvent(verified)

View file

@ -1,7 +1,7 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import com.stevesoltys.seedvault.metadata.BackupMetadata import com.stevesoltys.seedvault.service.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
data class RestorableBackup(val backupMetadata: BackupMetadata) { data class RestorableBackup(val backupMetadata: BackupMetadata) {

View file

@ -1,15 +1,15 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.os.Bundle import android.os.Bundle
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment import com.stevesoltys.seedvault.ui.restore.apk.InstallProgressFragment
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.provision.RequireProvisioningActivity
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class RestoreActivity : RequireProvisioningActivity() { class RestoreActivity : RequireProvisioningActivity() {

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.os.Bundle import android.os.Bundle

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import android.view.LayoutInflater import android.view.LayoutInflater
@ -8,7 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.Adapter
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder import com.stevesoltys.seedvault.ui.restore.RestoreProgressAdapter.PackageViewHolder
import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppViewHolder import com.stevesoltys.seedvault.ui.AppViewHolder
import java.util.LinkedList import java.util.LinkedList

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
import android.text.format.DateUtils.HOUR_IN_MILLIS import android.text.format.DateUtils.HOUR_IN_MILLIS
@ -10,7 +10,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder import com.stevesoltys.seedvault.ui.restore.RestoreSetAdapter.RestoreSetViewHolder
internal class RestoreSetAdapter( internal class RestoreSetAdapter(
private val listener: RestorableBackupClickListener, private val listener: RestorableBackupClickListener,

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.ui.restore
import android.app.Application import android.app.Application
import android.app.backup.BackupManager import android.app.backup.BackupManager
@ -18,28 +18,28 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.switchMap import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.service.app.BackupManagerOperationMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
import com.stevesoltys.seedvault.restore.install.ApkRestore import com.stevesoltys.seedvault.ui.restore.apk.ApkRestore
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator import com.stevesoltys.seedvault.ui.restore.apk.InstallIntentCreator
import com.stevesoltys.seedvault.restore.install.InstallResult import com.stevesoltys.seedvault.ui.restore.apk.InstallResult
import com.stevesoltys.seedvault.restore.install.isInstalled import com.stevesoltys.seedvault.ui.restore.apk.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.service.file.restore.FileRestoreService
import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
@ -49,9 +49,9 @@ import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS
import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.getAppName import com.stevesoltys.seedvault.ui.notification.getAppName
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -74,20 +74,20 @@ internal const val PACKAGES_PER_CHUNK = 100
internal class RestoreViewModel( internal class RestoreViewModel(
app: Application, app: Application,
settingsManager: SettingsManager, settingsService: SettingsService,
keyManager: KeyManager, keyManager: KeyManager,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val restoreCoordinator: RestoreCoordinator, private val restoreCoordinator: RestoreCoordinator,
private val apkRestore: ApkRestore, private val apkRestore: ApkRestore,
storageBackup: StorageBackup, storageBackup: StorageBackup,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : RequireProvisioningViewModel(app, settingsManager, keyManager), ) : RequireProvisioningViewModel(app, settingsService, keyManager),
RestorableBackupClickListener, SnapshotViewModel { RestorableBackupClickListener, SnapshotViewModel {
override val isRestoreOperation = true override val isRestoreOperation = true
private var session: IRestoreSession? = null private var session: IRestoreSession? = null
private val monitor = BackupMonitor() private val monitor = BackupManagerOperationMonitor()
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>() private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
@ -193,8 +193,8 @@ internal class RestoreViewModel(
// if we had no token before (i.e. restore from setup wizard), // if we had no token before (i.e. restore from setup wizard),
// use the token of the current restore set from now on // use the token of the current restore set from now on
if (settingsManager.getToken() == null) { if (settingsService.getToken() == null) {
settingsManager.setNewToken(token) settingsService.setNewToken(token)
} }
// start a new restore session // start a new restore session
@ -316,7 +316,7 @@ internal class RestoreViewModel(
private val restorableBackup: RestorableBackup, private val restorableBackup: RestorableBackup,
private val session: IRestoreSession, private val session: IRestoreSession,
private val packages: List<String>, private val packages: List<String>,
private val monitor: BackupMonitor, private val monitor: BackupManagerOperationMonitor,
) : IRestoreObserver.Stub() { ) : IRestoreObserver.Stub() {
/** /**
@ -441,7 +441,7 @@ internal class RestoreViewModel(
@UiThread @UiThread
internal fun startFilesRestore(item: SnapshotItem) { internal fun startFilesRestore(item: SnapshotItem) {
val i = Intent(app, StorageRestoreService::class.java) val i = Intent(app, FileRestoreService::class.java)
i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId) i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId)
i.putExtra(EXTRA_TIMESTAMP_START, item.time) i.putExtra(EXTRA_TIMESTAMP_START, item.time)
app.startForegroundService(i) app.startForegroundService(i)

View file

@ -1,22 +1,23 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault.ui.restore
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.restore.RestoreActivity
private val TAG = BroadcastReceiver::class.java.simpleName
private const val RESTORE_SECRET_CODE = "7378673"
class SecretCodeReceiver : BroadcastReceiver() { class SecretCodeReceiver : BroadcastReceiver() {
companion object {
private val TAG = BroadcastReceiver::class.java.simpleName
private const val RESTORE_SECRET_CODE = "7378673"
}
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.data?.host != RESTORE_SECRET_CODE) return if (intent.data?.host != RESTORE_SECRET_CODE) return
Log.d(TAG, "Restore secret code received.") Log.d(TAG, "Restore secret code received.")
val i = Intent(context, RestoreActivity::class.java).apply { val i = Intent(context, RestoreActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
context.startActivity(i) context.startActivity(i)
} }

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.ui.restore.apk
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
@ -20,8 +20,8 @@ import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -1,23 +1,23 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.ui.restore.apk
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_SIGNATURES import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.service.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.service.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.restore.RestorableBackup import com.stevesoltys.seedvault.ui.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED_SYSTEM_APP
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash import com.stevesoltys.seedvault.service.app.backup.apk.copyStreamsAndGetHash
import com.stevesoltys.seedvault.transport.backup.getSignatures import com.stevesoltys.seedvault.service.app.backup.apk.getSignatures
import com.stevesoltys.seedvault.transport.backup.isSystemApp import com.stevesoltys.seedvault.service.app.isSystemApp
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -31,7 +31,7 @@ internal class ApkRestore(
private val storagePlugin: StoragePlugin, private val storagePlugin: StoragePlugin,
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin, private val legacyStoragePlugin: LegacyStoragePlugin,
private val crypto: Crypto, private val cryptoService: CryptoService,
private val splitCompatChecker: ApkSplitCompatibilityChecker, private val splitCompatChecker: ApkSplitCompatibilityChecker,
private val apkInstaller: ApkInstaller, private val apkInstaller: ApkInstaller,
) { ) {
@ -222,7 +222,7 @@ internal class ApkRestore(
@Suppress("Deprecation") @Suppress("Deprecation")
legacyStoragePlugin.getApkInputStream(token, packageName, suffix) legacyStoragePlugin.getApkInputStream(token, packageName, suffix)
} else { } else {
val name = crypto.getNameForApk(salt, packageName, suffix) val name = cryptoService.getNameForApk(salt, packageName, suffix)
storagePlugin.getInputStream(token, name) storagePlugin.getInputStream(token, name)
} }
val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream()) val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream())

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.ui.restore.apk
import android.util.Log import android.util.Log

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.ui.restore.apk
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.ui.restore.apk
import android.content.Intent import android.content.Intent
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.ui.restore.apk
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module

Some files were not shown because too many files have changed in this diff Show more