1
0
Fork 0

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
.idea
codeStyles
copyright
app/src
androidTest/java/com/stevesoltys/seedvault
debug
main
AndroidManifest.xml
java/com/stevesoltys/seedvault
App.ktUsbIntentReceiver.kt
header
service
storage
transport
ui

View file

@ -1,11 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<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="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</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
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory
import com.stevesoltys.seedvault.transport.backup.KVBackup
import com.stevesoltys.seedvault.transport.restore.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore
import com.stevesoltys.seedvault.transport.restore.OutputFactory
import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.service.app.backup.InputFactory
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
@ -26,8 +26,8 @@ class KoinInstrumentationTestApp : App() {
val context = this@KoinInstrumentationTestApp
single { spyk(BackupNotificationManager(context)) }
single { spyk(FullBackup(get(), get(), get(), get())) }
single { spyk(KVBackup(get(), get(), get(), get(), get())) }
single { spyk(FullBackupService(get(), get(), get(), get())) }
single { spyk(KVBackupService(get(), get(), get(), get(), get())) }
single { spyk(InputFactory()) }
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.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.plugins.saf.deleteContents
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.service.storage.saf.DocumentsProviderStoragePlugin
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
import com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.service.storage.saf.deleteContents
import com.stevesoltys.seedvault.service.settings.SettingsService
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
@ -33,9 +33,9 @@ import org.koin.core.component.inject
class PluginTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val settingsManager: SettingsManager by inject()
private val mockedSettingsManager: SettingsManager = mockk()
private val storage = DocumentsStorage(context, mockedSettingsManager)
private val settingsService: SettingsService by inject()
private val mockedSettingsService: SettingsService = mockk()
private val storage = DocumentsStorage(context, mockedSettingsService)
private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage)
@ -49,7 +49,7 @@ class PluginTest : KoinComponent {
@Before
fun setup() = runBlocking {
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
every { mockedSettingsService.getStorage() } returns settingsService.getStorage()
storage.rootBackupDir?.deleteContents(context)
?: error("Select a storage location in the app first!")
}
@ -76,11 +76,11 @@ class PluginTest : KoinComponent {
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
// no backups available initially
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
val s = settingsManager.getStorage() ?: error("no storage")
val s = settingsService.getStorage() ?: error("no storage")
assertFalse(storagePlugin.hasBackup(s))
// 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
storagePlugin.startNewRestoreSet(token)
@ -114,7 +114,7 @@ class PluginTest : KoinComponent {
@Test
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
every { mockedSettingsManager.getToken() } returns token
every { mockedSettingsService.getToken() } returns token
storagePlugin.startNewRestoreSet(token)
storagePlugin.initializeDevice()
@ -216,7 +216,7 @@ class PluginTest : KoinComponent {
}
private fun initStorage(token: Long) = runBlocking {
every { mockedSettingsManager.getToken() } returns token
every { mockedSettingsService.getToken() } returns token
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.InputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory
import com.stevesoltys.seedvault.transport.backup.KVBackup
import com.stevesoltys.seedvault.service.app.backup.InputFactory
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.clearMocks
import io.mockk.coEvery
@ -27,9 +27,9 @@ internal interface LargeBackupTestBase : LargeTestBase {
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()
@ -72,7 +72,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
return backupResult.copy(
backupResults = backupResult.allUserApps().associate {
it.packageName to spyMetadataManager.getPackageMetadata(it.packageName)
it.packageName to spyMetadataService.getPackageMetadata(it.packageName)
}.toMutableMap()
)
}
@ -88,7 +88,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
}
private fun spyOnBackup(backupResult: SeedvaultLargeTestResult): AtomicBoolean {
clearMocks(spyInputFactory, spyKVBackup, spyFullBackup)
clearMocks(spyInputFactory, spyKVBackupService, spyFullBackupService)
spyOnFullBackupData(backupResult)
spyOnKVBackupData(backupResult)
@ -100,7 +100,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
var data = mutableMapOf<String, ByteArray>()
coEvery {
spyKVBackup.performBackup(any(), any(), any(), any(), any())
spyKVBackupService.performBackup(any(), any(), any(), any(), any())
} answers {
packageName = firstArg<PackageInfo>().packageName
callOriginal()
@ -117,7 +117,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
}
coEvery {
spyKVBackup.finishBackup()
spyKVBackupService.finishBackup()
} answers {
backupResult.kv[packageName!!] = data
.mapValues { entry -> entry.value.sha256() }
@ -134,7 +134,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
var dataIntercept = ByteArrayOutputStream()
coEvery {
spyFullBackup.performFullBackup(any(), any(), any(), any(), any())
spyFullBackupService.performFullBackup(any(), any(), any(), any(), any())
} answers {
packageName = firstArg<PackageInfo>().packageName
callOriginal()
@ -150,7 +150,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
}
every {
spyFullBackup.finishBackup()
spyFullBackupService.finishBackup()
} answers {
val result = callOriginal()
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.screen.impl.RecoveryCodeScreen
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
import com.stevesoltys.seedvault.transport.restore.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore
import com.stevesoltys.seedvault.transport.restore.OutputFactory
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every

View file

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

View file

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

View file

@ -1,8 +1,8 @@
package com.stevesoltys.seedvault.e2e
import android.content.pm.PackageInfo
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.restore.AppRestoreResult
import com.stevesoltys.seedvault.service.metadata.PackageMetadata
import com.stevesoltys.seedvault.ui.restore.AppRestoreResult
/**
* 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 com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.service.metadata.PackageState
import org.junit.Test
@LargeTest
@ -17,7 +17,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
confirmCode()
}
if (settingsManager.getStorage() == null) {
if (settingsService.getStorage() == null) {
chooseStorageLocation()
} else {
changeBackupLocation()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,12 +15,12 @@ import android.os.Looper
import android.provider.DocumentsContract
import android.util.Log
import androidx.core.content.ContextCompat.startForegroundService
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.service.app.backup.requestBackup
import com.stevesoltys.seedvault.service.file.backup.FileBackupService
import com.stevesoltys.seedvault.service.file.backup.FileBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.service.settings.FlashDrive
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
import org.koin.core.context.GlobalContext.get
import java.util.concurrent.TimeUnit.HOURS
@ -32,17 +32,17 @@ private const val HOURS_AUTO_BACKUP: Long = 24
class UsbIntentReceiver : UsbMonitor() {
// using KoinComponent would crash robolectric tests :(
private val settingsManager: SettingsManager by lazy { get().get() }
private val metadataManager: MetadataManager by lazy { get().get() }
private val settingsService: SettingsService by lazy { get().get() }
private val metadataService: MetadataService by lazy { get().get() }
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
if (action != ACTION_USB_DEVICE_ATTACHED) return false
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)
return if (savedFlashDrive == attachedFlashDrive) {
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)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
true
@ -57,8 +57,8 @@ class UsbIntentReceiver : UsbMonitor() {
}
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
if (settingsManager.isStorageBackupEnabled()) {
val i = Intent(context, StorageBackupService::class.java)
if (settingsService.isStorageBackupEnabled()) {
val i = Intent(context, FileBackupService::class.java)
// this starts an app backup afterwards
i.putExtra(EXTRA_START_APP_BACKUP, true)
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_ID
@ -8,9 +8,9 @@ import android.os.Bundle
import android.util.Log
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) {
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, "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.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
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_SIGNING_CERTIFICATES
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log
import android.util.Log.INFO
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager
private val TAG = PackageService::class.java.simpleName
private const val LOG_MAX_PACKAGES = 100
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.settings.SettingsService
/**
* @author Steve Soltys
@ -30,13 +24,17 @@ private const val LOG_MAX_PACKAGES = 100
*/
internal class PackageService(
private val context: Context,
private val backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val settingsService: SettingsService,
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 myUserId = UserHandle.myUserId()
val eligiblePackages: Array<String>
@WorkerThread
@ -96,7 +94,7 @@ internal class PackageService(
@WorkerThread
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
packageInfo.isUserVisible(context) &&
packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled())
packageInfo.allowsBackup(settingsService.d2dBackupsEnabled())
}
/**
@ -105,7 +103,7 @@ internal class PackageService(
val userNotAllowedApps: List<PackageInfo>
@WorkerThread
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
!packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) &&
!packageInfo.allowsBackup(settingsService.d2dBackupsEnabled()) &&
!packageInfo.isSystemApp()
}
@ -140,7 +138,7 @@ internal class PackageService(
// apps that we, or the user, want to exclude.
// 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.
// 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.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.content.pm.PackageInfo
@ -8,13 +16,15 @@ import android.content.pm.SigningInfo
import android.util.Log
import android.util.PackageUtils.computeSha256DigestBytes
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.util.encodeBase64
import com.stevesoltys.seedvault.service.metadata.ApkSplit
import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.service.metadata.PackageMetadata
import com.stevesoltys.seedvault.service.metadata.PackageState
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.FileInputStream
import java.io.FileNotFoundException
@ -23,14 +33,14 @@ import java.io.InputStream
import java.io.OutputStream
import java.security.MessageDigest
private val TAG = ApkBackup::class.java.simpleName
private val TAG = ApkBackupService::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class ApkBackup(
internal class ApkBackupService(
private val pm: PackageManager,
private val crypto: Crypto,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
private val cryptoService: CryptoService,
private val settingsService: SettingsService,
private val metadataService: MetadataService,
) {
/**
@ -53,7 +63,7 @@ internal class ApkBackup(
if (packageName == MAGIC_PACKAGE_MANAGER) return null
// 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
// 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
val packageMetadata = metadataManager.getPackageMetadata(packageName)
val packageMetadata = metadataService.getPackageMetadata(packageName)
?: PackageMetadata()
// get version codes
@ -104,7 +114,7 @@ internal class ApkBackup(
// get an InputStream for the APK
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
// 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))
// back up splits if they exist
@ -195,7 +205,7 @@ internal class ApkBackup(
}
}
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
getApkInputStream(sourceDir).use { inputStream ->
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_INCREMENTAL
@ -18,62 +18,52 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.app.PackageService
import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
import com.stevesoltys.seedvault.service.app.isStopped
import com.stevesoltys.seedvault.service.app.isSystemApp
import com.stevesoltys.seedvault.service.metadata.BackupType
import com.stevesoltys.seedvault.service.metadata.MetadataService
import com.stevesoltys.seedvault.service.metadata.PackageState
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.util.TimeSource
import java.io.IOException
import java.io.OutputStream
import java.util.concurrent.TimeUnit.DAYS
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 Torsten Grote
*/
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
@Suppress("BlockingMethodInNonBlockingContext")
internal class BackupCoordinator(
internal class BackupCoordinatorService(
private val context: Context,
private val plugin: StoragePlugin,
private val kv: KVBackup,
private val full: FullBackup,
private val apkBackup: ApkBackup,
private val clock: Clock,
private val kv: KVBackupService,
private val full: FullBackupService,
private val apkBackupService: ApkBackupService,
private val timeSource: TimeSource,
private val packageService: PackageService,
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager,
private val metadataService: MetadataService,
private val settingsService: SettingsService,
private val nm: BackupNotificationManager,
) {
private val state = CoordinatorState(
private val TAG = BackupCoordinatorService::class.java.simpleName
private val state = BackupCoordinatorState(
calledInitialize = false,
calledClearBackupData = false,
cancelReason = UNKNOWN_ERROR
@ -92,9 +82,9 @@ internal class BackupCoordinator(
*/
@Throws(IOException::class)
private suspend fun startNewRestoreSet(): Long {
val token = clock.time()
val token = timeSource.time()
Log.i(TAG, "Starting new RestoreSet with token $token...")
settingsManager.setNewToken(token)
settingsService.setNewToken(token)
plugin.startNewRestoreSet(token)
return token
}
@ -125,7 +115,7 @@ internal class BackupCoordinator(
plugin.initializeDevice()
Log.d(TAG, "Resetting backup metadata for token $token...")
plugin.getMetadataOutputStream(token).use {
metadataManager.onDeviceInitialization(token, it)
metadataService.onDeviceInitialization(token, it)
}
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
// so we remember that we initialized successfully
@ -134,7 +124,7 @@ internal class BackupCoordinator(
} catch (e: IOException) {
Log.e(TAG, "Error initializing device", e)
// 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
}
@ -230,7 +220,7 @@ internal class BackupCoordinator(
flags: Int,
): Int {
state.cancelReason = UNKNOWN_ERROR
if (metadataManager.requiresInit) {
if (metadataService.requiresInit) {
// start a new restore set to upgrade from legacy format
// by starting a clean backup with all files using the new version
try {
@ -241,8 +231,8 @@ internal class BackupCoordinator(
// this causes a backup error, but things should go back to normal afterwards
return TRANSPORT_NOT_INITIALIZED
}
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
val salt = metadataManager.salt
val token = settingsService.getToken() ?: error("no token in performFullBackup")
val salt = metadataService.salt
return kv.performBackup(packageInfo, data, flags, token, salt)
}
@ -280,8 +270,8 @@ internal class BackupCoordinator(
flags: Int,
): Int {
state.cancelReason = UNKNOWN_ERROR
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
val salt = metadataManager.salt
val token = settingsService.getToken() ?: error("no token in performFullBackup")
val salt = metadataService.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
val ignoreApp = state.cancelReason == NO_DATA && packageInfo.isSystemApp()
if (!ignoreApp) onPackageBackupError(packageInfo, BackupType.FULL)
val token = settingsManager.getToken() ?: error("no token in cancelFullBackup")
val salt = metadataManager.salt
val token = settingsService.getToken() ?: error("no token in cancelFullBackup")
val salt = metadataService.salt
full.cancelFullBackup(token, salt, ignoreApp)
}
@ -329,8 +319,8 @@ internal class BackupCoordinator(
suspend fun clearBackupData(packageInfo: PackageInfo): Int {
val packageName = packageInfo.packageName
Log.i(TAG, "Clear Backup Data of $packageName.")
val token = settingsManager.getToken() ?: error("no token in clearBackupData")
val salt = metadataManager.salt
val token = settingsService.getToken() ?: error("no token in clearBackupData")
val salt = metadataService.salt
try {
kv.clearBackupData(packageInfo, token, salt)
} catch (e: IOException) {
@ -368,7 +358,7 @@ internal class BackupCoordinator(
if (result == TRANSPORT_OK) {
val isPmBackup = packageName == MAGIC_PACKAGE_MANAGER
// call onPackageBackedUp for @pm@ only if we can do backups right now
if (!isPmBackup || settingsManager.canDoBackupNow()) {
if (!isPmBackup || settingsService.canDoBackupNow()) {
try {
onPackageBackedUp(packageInfo, BackupType.KV)
} 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
if (isPmBackup && settingsManager.canDoBackupNow()) {
if (isPmBackup && settingsService.canDoBackupNow()) {
try {
backUpApksOfNotBackedUpPackages()
} catch (e: Exception) {
@ -426,7 +416,7 @@ internal class BackupCoordinator(
val wasBackedUp = backUpApk(packageInfo, packageState)
if (!wasBackedUp) {
val packageMetadata =
metadataManager.getPackageMetadata(packageName)
metadataService.getPackageMetadata(packageName)
val oldPackageState = packageMetadata?.state
if (oldPackageState != packageState) {
Log.i(
@ -434,7 +424,7 @@ internal class BackupCoordinator(
", update to $packageState"
)
plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackupError(packageInfo, packageState, it)
metadataService.onPackageBackupError(packageInfo, packageState, it)
}
}
}
@ -455,12 +445,12 @@ internal class BackupCoordinator(
): Boolean {
val packageName = packageInfo.packageName
return try {
apkBackup.backupApkIfNecessary(packageInfo, packageState) { name ->
val token = settingsManager.getToken() ?: throw IOException("no current token")
apkBackupService.backupApkIfNecessary(packageInfo, packageState) { name ->
val token = settingsService.getToken() ?: throw IOException("no current token")
plugin.getOutputStream(token, name)
}?.let { packageMetadata ->
plugin.getMetadataOutputStream().use {
metadataManager.onApkBackedUp(packageInfo, packageMetadata, it)
metadataService.onApkBackedUp(packageInfo, packageMetadata, it)
}
true
} ?: false
@ -472,7 +462,7 @@ internal class BackupCoordinator(
private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackedUp(packageInfo, type, it)
metadataService.onPackageBackedUp(packageInfo, type, it)
}
}
@ -480,7 +470,7 @@ internal class BackupCoordinator(
val packageName = packageInfo.packageName
try {
plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackupError(packageInfo, state.cancelReason, it, type)
metadataService.onPackageBackupError(packageInfo, state.cancelReason, it, type)
}
} catch (e: IOException) {
Log.e(TAG, "Error while writing metadata for $packageName", e)
@ -491,7 +481,7 @@ internal class BackupCoordinator(
val longBackoff = DAYS.toMillis(30)
// back off if there's no storage set
val storage = settingsManager.getStorage() ?: return longBackoff
val storage = settingsService.getStorage() ?: return longBackoff
return when {
// back off if storage is removable and not available right now
storage.isUnavailableUsb(context) -> longBackoff
@ -503,8 +493,10 @@ internal class BackupCoordinator(
}
private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream {
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
return getOutputStream(t, FILE_BACKUP_METADATA)
val t = token ?: settingsService.getToken() ?: throw IOException("no current token")
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.TRANSPORT_ERROR
@ -8,41 +12,26 @@ import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.app.backup.InputFactory
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.service.header.getADForFull
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import libcore.io.IoUtils.closeQuietly
import java.io.EOFException
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()
private val TAG = FullBackup::class.java.simpleName
private val TAG = FullBackupService::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup(
internal class FullBackupService(
private val plugin: StoragePlugin,
private val settingsManager: SettingsManager,
private val settingsService: SettingsService,
private val inputFactory: InputFactory,
private val crypto: Crypto,
private val cryptoService: CryptoService,
) {
private var state: FullBackupState? = null
@ -52,7 +41,7 @@ internal class FullBackup(
fun getCurrentPackage() = state?.packageInfo
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 {
@ -114,7 +103,7 @@ internal class FullBackup(
val inputStream = inputFactory.getInputStream(socket)
state = FullBackupState(targetPackage, socket, inputStream) {
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
val outputStream = try {
plugin.getOutputStream(token, name)
@ -122,12 +111,15 @@ internal class FullBackup(
"Error getting OutputStream for full backup of $packageName".let {
Log.e(TAG, it, e)
}
throw(e)
throw (e)
}
// store version header
val state = this.state ?: throw AssertionError()
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
return TRANSPORT_OK
}
@ -173,7 +165,7 @@ internal class FullBackup(
@Throws(IOException::class)
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)
}

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_INCREMENTAL
@ -10,33 +14,25 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.service.header.getADForKV
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.app.backup.InputFactory
import java.io.IOException
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()
private val TAG = KVBackup::class.java.simpleName
private val TAG = KVBackupService::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackup(
internal class KVBackupService(
private val plugin: StoragePlugin,
private val settingsManager: SettingsManager,
private val settingsService: SettingsService,
private val inputFactory: InputFactory,
private val crypto: Crypto,
private val cryptoService: CryptoService,
private val dbManager: KvDbManager,
) {
@ -46,7 +42,7 @@ internal class KVBackup(
fun getCurrentPackage() = state?.packageInfo
fun getQuota(): Long = if (settingsManager.isQuotaUnlimited()) {
fun getQuota(): Long = if (settingsService.isQuotaUnlimited()) {
Long.MAX_VALUE
} else {
DEFAULT_QUOTA_KEY_VALUE_BACKUP
@ -84,7 +80,7 @@ internal class KVBackup(
if (state != null) {
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)
this.state = KVBackupState(packageInfo, token, name, db)
@ -134,7 +130,7 @@ internal class KVBackup(
// K/V backups (typically starting with package manager metadata - @pm@)
// are scheduled with JobInfo.Builder#setOverrideDeadline()
// and thus do not respect backoff.
settingsManager.canDoBackupNow()
settingsService.canDoBackupNow()
} else {
// all other packages always need upload
true
@ -194,7 +190,7 @@ internal class KVBackup(
@Throws(IOException::class)
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
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)
if (!dbManager.deleteDb(packageInfo.packageName)) throw IOException()
}
@ -244,7 +240,7 @@ internal class KVBackup(
plugin.getOutputStream(token, name).use { outputStream ->
outputStream.write(ByteArray(1) { VERSION })
val ad = getADForKV(VERSION, packageName)
crypto.newEncryptingStream(outputStream, ad).use { encryptedStream ->
cryptoService.newEncryptingStream(outputStream, ad).use { encryptedStream ->
GZIPOutputStream(encryptedStream).use { gZipStream ->
dbManager.getDbInputStream(packageName).use { inputStream ->
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.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.os.ParcelFileDescriptor
@ -17,5 +17,4 @@ internal class OutputFactory {
fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
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.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_OK
@ -13,15 +13,17 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.service.metadata.BackupMetadata
import com.stevesoltys.seedvault.service.metadata.BackupType
import com.stevesoltys.seedvault.service.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.service.metadata.MetadataReader
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.DEFAULT_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -35,26 +37,14 @@ import java.io.IOException
*/
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
@Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreCoordinator(
private val context: Context,
private val crypto: Crypto,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
private val cryptoService: CryptoService,
private val settingsService: SettingsService,
private val metadataService: MetadataService,
private val notificationManager: BackupNotificationManager,
private val plugin: StoragePlugin,
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.
*/
fun getCurrentRestoreSet(): Long {
return (settingsManager.getToken() ?: 0L).apply {
return (settingsService.getToken() ?: 0L).apply {
Log.i(TAG, "Got current restore set token: $this")
}
}
@ -138,7 +128,7 @@ internal class RestoreCoordinator(
this.backupMetadata = backupMetadata
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
if (isStorageRemovableAndNotAvailable()) {
// 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
val storageName = settingsManager.getStorage()?.name
val storageName = settingsService.getStorage()?.name
?: context.getString(R.string.settings_backup_location_none)
notificationManager.onRemovableStorageNotAvailableForRestore(
pmPackageName,
@ -231,7 +221,8 @@ internal class RestoreCoordinator(
val type = try {
when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
BackupType.KV -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
val name =
cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
if (plugin.hasData(state.token, name)) {
Log.i(TAG, "Found K/V data for $packageName.")
kv.initializeState(
@ -247,7 +238,8 @@ internal class RestoreCoordinator(
}
BackupType.FULL -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
val name =
cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
if (plugin.hasData(state.token, name)) {
Log.i(TAG, "Found full backup data for $packageName.")
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
private fun isStorageRemovableAndNotAvailable(): Boolean {
val storage = settingsManager.getStorage() ?: return false
val storage = settingsService.getStorage() ?: return false
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.TRANSPORT_ERROR
@ -7,29 +12,20 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.HeaderDecodeService
import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.service.header.getADForFull
import libcore.io.IoUtils.closeQuietly
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
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
@Suppress("BlockingMethodInNonBlockingContext")
@ -38,8 +34,8 @@ internal class FullRestore(
@Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory,
private val headerReader: HeaderReader,
private val crypto: Crypto,
private val headerDecodeService: HeaderDecodeService,
private val cryptoService: CryptoService,
) {
private var state: FullRestoreState? = null
@ -104,15 +100,15 @@ internal class FullRestore(
if (state.version == 0.toByte()) {
val inputStream =
legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo)
val version = headerReader.readVersion(inputStream, state.version)
val version = headerDecodeService.readVersion(inputStream, state.version)
@Suppress("deprecation")
crypto.decryptHeader(inputStream, version, packageName)
cryptoService.decryptHeader(inputStream, version, packageName)
state.inputStream = inputStream
} else {
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)
state.inputStream = crypto.newDecryptingStream(inputStream, ad)
state.inputStream = cryptoService.newDecryptingStream(inputStream, ad)
}
} catch (e: IOException) {
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
val decrypted = try {
@Suppress("deprecation")
crypto.decryptSegment(inputStream)
cryptoService.decryptSegment(inputStream)
} catch (e: EOFException) {
Log.i(TAG, " EOF")
// 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.BackupTransport.TRANSPORT_ERROR
@ -9,34 +14,23 @@ import android.util.Log
import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY
import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.decodeBase64
import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.transport.backup.KVDb
import com.stevesoltys.seedvault.transport.backup.KvDbManager
import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.HeaderDecodeService
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.service.header.getADForKV
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.util.decodeBase64
import libcore.io.IoUtils.closeQuietly
import java.io.IOException
import java.security.GeneralSecurityException
import java.util.ArrayList
import java.util.zip.GZIPInputStream
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
@Suppress("BlockingMethodInNonBlockingContext")
@ -45,8 +39,8 @@ internal class KVRestore(
@Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory,
private val headerReader: HeaderReader,
private val crypto: Crypto,
private val headerDecodeService: HeaderDecodeService,
private val cryptoService: CryptoService,
private val dbManager: KvDbManager,
) {
@ -153,9 +147,9 @@ internal class KVRestore(
private suspend fun downloadRestoreDb(state: KVRestoreState): KVDb {
val packageName = state.packageInfo.packageName
plugin.getInputStream(state.token, state.name).use { inputStream ->
headerReader.readVersion(inputStream, state.version)
headerDecodeService.readVersion(inputStream, state.version)
val ad = getADForKV(VERSION, packageName)
crypto.newDecryptingStream(inputStream, ad).use { decryptedStream ->
cryptoService.newDecryptingStream(inputStream, ad).use { decryptedStream ->
GZIPInputStream(decryptedStream).use { gzipStream ->
dbManager.getDbOutputStream(packageName).use { outputStream ->
gzipStream.copyTo(outputStream)
@ -241,10 +235,10 @@ internal class KVRestore(
out: BackupDataOutput,
) = legacyPlugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
.use { inputStream ->
val version = headerReader.readVersion(inputStream, state.version)
val version = headerDecodeService.readVersion(inputStream, state.version)
val packageName = state.packageInfo.packageName
crypto.decryptHeader(inputStream, version, packageName, dKey.key)
val value = crypto.decryptMultipleSegments(inputStream)
cryptoService.decryptHeader(inputStream, version, packageName, dKey.key)
val value = cryptoService.decryptMultipleSegments(inputStream)
val size = value.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 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 java.security.KeyStore
@ -15,5 +15,5 @@ val cryptoModule = module {
}
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.encodeBase64
import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
import com.stevesoltys.seedvault.header.SegmentHeader
import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.service.header.HeaderDecodeService
import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.service.header.MAX_VERSION_HEADER_SIZE
import com.stevesoltys.seedvault.service.header.VersionHeader
import com.stevesoltys.seedvault.util.encodeBase64
import org.calyxos.backup.storage.crypto.StreamCrypto
import org.calyxos.backup.storage.crypto.StreamCrypto.deriveStreamKey
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
@ -19,111 +16,14 @@ import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import javax.crypto.spec.SecretKeySpec
/**
* 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(
internal class CryptoServiceImpl(
private val keyManager: KeyManager,
private val cipherFactory: CipherFactory,
private val headerReader: HeaderReader,
) : Crypto {
private val headerDecodeService: HeaderDecodeService,
) : CryptoService {
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() }
@ -175,7 +75,7 @@ internal class CryptoImpl(
expectedKey: String?,
): VersionHeader {
val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
val header = headerReader.getVersionHeader(decrypted)
val header = headerDecodeService.getVersionHeader(decrypted)
if (header.version != expectedVersion) {
throw SecurityException(
@ -219,7 +119,7 @@ internal class CryptoImpl(
@Suppress("Deprecation")
@Throws(EOFException::class, IOException::class, SecurityException::class)
private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
val segmentHeader = headerReader.readSegmentHeader(inputStream)
val segmentHeader = headerDecodeService.readSegmentHeader(inputStream)
if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException(
"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.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 androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.crypto.KeyManager
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 javax.crypto.SecretKey
internal class SeedvaultStoragePlugin(
/**
* [SafStoragePlugin] for backing up files.
*/
internal class FileBackupStoragePlugin(
private val appContext: Context,
private val storage: DocumentsStorage,
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.crypto.TYPE_BACKUP_FULL
import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_KV
import com.stevesoltys.seedvault.service.crypto.GCM_AUTHENTICATION_TAG_LENGTH
import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_FULL
import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_KV
import java.nio.ByteBuffer
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.IOException
import java.io.InputStream
import java.nio.ByteBuffer
import java.security.GeneralSecurityException
internal interface HeaderReader {
@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 {
internal class HeaderDecodeServiceImpl : HeaderDecodeService {
@Throws(IOException::class, UnsupportedVersionException::class, GeneralSecurityException::class)
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.os.Build
import com.stevesoltys.seedvault.crypto.TYPE_METADATA
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.service.crypto.TYPE_METADATA
import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
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.dsl.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<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.crypto.Crypto
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
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.util.Utf8
import org.json.JSONException
import org.json.JSONObject
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(
SecurityException::class,
@ -51,7 +51,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken)
val metadataBytes = try {
crypto.newDecryptingStream(inputStream, getAD(version, expectedToken)).readBytes()
cryptoService.newDecryptingStream(inputStream, getAD(version, expectedToken))
.readBytes()
} catch (e: GeneralSecurityException) {
throw DecryptionFailedException(e)
}
@ -67,7 +68,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
@Suppress("Deprecation")
private fun readMetadataV0(inputStream: InputStream, expectedToken: Long): BackupMetadata {
val metadataBytes = try {
crypto.decryptMultipleSegments(inputStream)
cryptoService.decryptMultipleSegments(inputStream)
} catch (e: AEADBadTagException) {
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.MODE_PRIVATE
@ -9,34 +9,34 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.isSystemApp
import com.stevesoltys.seedvault.service.app.isSystemApp
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.header.VERSION
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
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.WAS_STOPPED
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.util.TimeSource
import com.stevesoltys.seedvault.util.encodeBase64
import java.io.FileNotFoundException
import java.io.IOException
import java.io.OutputStream
private val TAG = MetadataManager::class.java.simpleName
private val TAG = MetadataService::class.java.simpleName
@VisibleForTesting
internal const val METADATA_CACHE_FILE = "metadata.cache"
internal const val METADATA_SALT_SIZE = 32
@WorkerThread
internal class MetadataManager(
internal class MetadataService(
private val context: Context,
private val clock: Clock,
private val crypto: Crypto,
private val timeSource: TimeSource,
private val cryptoService: CryptoService,
private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader,
private val settingsManager: SettingsManager
private val settingsService: SettingsService
) {
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
@ -63,7 +63,7 @@ internal class MetadataManager(
@Synchronized
@Throws(IOException::class)
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
val salt = cryptoService.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
modifyMetadata(metadataOutputStream) {
metadata = BackupMetadata(token = token, salt = salt)
}
@ -135,9 +135,9 @@ internal class MetadataManager(
) {
val packageName = packageInfo.packageName
modifyMetadata(metadataOutputStream) {
val now = clock.time()
val now = timeSource.time()
metadata.time = now
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
metadata.d2dBackup = settingsService.d2dBackupsEnabled()
if (metadata.packageMetadataMap.containsKey(packageName)) {
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.crypto.Crypto
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.util.Utf8
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
@ -15,14 +15,15 @@ interface MetadataWriter {
fun encode(metadata: BackupMetadata): ByteArray
}
internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
internal class MetadataWriterImpl(private val cryptoService: CryptoService) : MetadataWriter {
@Throws(IOException::class)
override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
crypto.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token)).use {
it.write(encode(metadata))
}
cryptoService.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token))
.use {
it.write(encode(metadata))
}
}
override fun encode(metadata: BackupMetadata): ByteArray {

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.hardware.usb.UsbDevice
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.stevesoltys.seedvault.getStorageContext
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
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"
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 {
PreferenceManager.getDefaultSharedPreferences(context)
@ -59,7 +58,7 @@ class SettingsManager(private val context: Context) {
/**
* 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.
*/
fun setNewToken(newToken: Long?) {
@ -162,54 +161,3 @@ class SettingsManager(private val context: Context) {
.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 androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.settings.Storage
import com.stevesoltys.seedvault.service.settings.Storage
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@ -61,7 +61,7 @@ interface StoragePlugin {
* @return metadata for the set of restore images available,
* 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
@ -74,5 +74,3 @@ interface StoragePlugin {
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.plugins.StoragePlugin
import com.stevesoltys.seedvault.service.storage.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.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.pm.PackageManager
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.Storage
import com.stevesoltys.seedvault.service.storage.EncryptedBackupMetadata
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.service.settings.Storage
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
@ -90,14 +90,14 @@ internal class DocumentsProviderStoragePlugin(
return backupSets.isNotEmpty()
}
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
val rootDir = storage.rootBackupDir ?: return null
val backupSets = getBackups(context, rootDir)
val iterator = backupSets.iterator()
return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence
val backupSet = iterator.next()
EncryptedMetadata(backupSet.token) {
EncryptedBackupMetadata(backupSet.token) {
storage.getInputStream(backupSet.metadataFile)
}
}

View file

@ -1,6 +1,6 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.service.storage.saf
import android.content.ContentResolver
import android.content.Context
@ -17,8 +17,8 @@ import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.Storage
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.settings.Storage
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
@ -43,11 +43,11 @@ private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage(
private val appContext: Context,
private val settingsManager: SettingsManager,
private val settingsService: SettingsService,
) {
internal var storage: Storage? = null
get() {
if (field == null) field = settingsManager.getStorage()
if (field == null) field = settingsService.getStorage()
return field
}
@ -81,7 +81,7 @@ internal class DocumentsStorage(
private var currentToken: Long? = null
get() {
if (field == null) field = settingsManager.getToken()
if (field == null) field = settingsService.getToken()
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.pm.PackageInfo
import androidx.annotation.WorkerThread
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.IOException
import java.io.InputStream
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
@Deprecated("Only for old v0 backup format")
internal class DocumentsProviderLegacyPlugin(
private val context: Context,
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 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.util.Log
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsActivity
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.ui.settings.SettingsActivity
import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent
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) :
BackupTransport(), KoinComponent {
private val backupCoordinator by inject<BackupCoordinator>()
private val backupCoordinatorService by inject<BackupCoordinatorService>()
private val restoreCoordinator by inject<RestoreCoordinator>()
private val settingsManager by inject<SettingsManager>()
private val settingsService by inject<SettingsService>()
override fun transportDirName(): String {
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.
*/
override fun getTransportFlags(): Int {
return if (settingsManager.d2dBackupsEnabled()) {
return if (settingsService.d2dBackupsEnabled()) {
D2D_TRANSPORT_FLAGS
} else {
DEFAULT_TRANSPORT_FLAGS
@ -120,26 +120,26 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
//
override fun initializeDevice(): Int = runBlocking {
backupCoordinator.initializeDevice()
backupCoordinatorService.initializeDevice()
}
override fun isAppEligibleForBackup(
targetPackage: PackageInfo,
isFullBackup: Boolean,
): Boolean {
return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
return backupCoordinatorService.isAppEligibleForBackup(targetPackage, isFullBackup)
}
override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
backupCoordinator.getBackupQuota(packageName, isFullBackup)
backupCoordinatorService.getBackupQuota(packageName, isFullBackup)
}
override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
backupCoordinator.clearBackupData(packageInfo)
backupCoordinatorService.clearBackupData(packageInfo)
}
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 {
return backupCoordinator.requestBackupTime()
return backupCoordinatorService.requestBackupTime()
}
override fun performBackup(
@ -155,7 +155,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
inFd: ParcelFileDescriptor,
flags: Int,
): Int = runBlocking {
backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags)
backupCoordinatorService.performIncrementalBackup(packageInfo, inFd, flags)
}
override fun performBackup(
@ -171,11 +171,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
//
override fun requestFullBackupTime(): Long {
return backupCoordinator.requestFullBackupTime()
return backupCoordinatorService.requestFullBackupTime()
}
override fun checkFullBackupSize(size: Long): Int {
return backupCoordinator.checkFullBackupSize(size)
return backupCoordinatorService.checkFullBackupSize(size)
}
override fun performFullBackup(
@ -183,7 +183,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
socket: ParcelFileDescriptor,
flags: Int,
): Int = runBlocking {
backupCoordinator.performFullBackup(targetPackage, socket, flags)
backupCoordinatorService.performFullBackup(targetPackage, socket, flags)
}
override fun performFullBackup(
@ -191,15 +191,15 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
fileDescriptor: ParcelFileDescriptor,
): Int = runBlocking {
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 {
backupCoordinator.sendBackupData(numBytes)
backupCoordinatorService.sendBackupData(numBytes)
}
override fun cancelFullBackup() = runBlocking {
backupCoordinator.cancelFullBackup()
backupCoordinatorService.cancelFullBackup()
}
// ------------------------------------------------------------------------------------

View file

@ -1,22 +1,14 @@
package com.stevesoltys.seedvault.transport
import android.app.Service
import android.app.backup.BackupManager
import android.app.backup.IBackupManager
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.service.crypto.KeyManager
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.inject
import org.koin.core.context.GlobalContext.get
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 com.stevesoltys.seedvault.R
abstract class BackupActivity : AppCompatActivity() {
abstract class BackupActivityBase : AppCompatActivity() {
@CallSuper
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.ViewGroup
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.koin.androidx.viewmodel.ext.android.sharedViewModel
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.LiveData
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>>() {

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.ui;
package com.stevesoltys.seedvault.ui.liveevent;
public interface LiveEventHandler<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>() {

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_LOW
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
import com.stevesoltys.seedvault.settings.SettingsActivity
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
import com.stevesoltys.seedvault.ui.restore.ACTION_RESTORE_ERROR_UNINSTALL
import com.stevesoltys.seedvault.ui.restore.EXTRA_PACKAGE_NAME
import com.stevesoltys.seedvault.ui.restore.REQUEST_CODE_UNINSTALL
import com.stevesoltys.seedvault.ui.settings.ACTION_APP_STATUS_LIST
import com.stevesoltys.seedvault.ui.settings.SettingsActivity
import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
private const val CHANNEL_ID_ERROR = "NotificationError"

View file

@ -9,8 +9,8 @@ import android.util.Log.INFO
import android.util.Log.isLoggable
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
import com.stevesoltys.seedvault.service.metadata.MetadataService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -23,7 +23,7 @@ internal class NotificationBackupObserver(
) : IBackupObserver.Stub(), KoinComponent {
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 numPackages: Int = 0
@ -77,7 +77,7 @@ internal class NotificationBackupObserver(
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
}
val success = status == 0
val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null
val numBackedUp = if (success) metadataService.getPackagesNumBackedUp() else null
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.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.CallSuper
import com.stevesoltys.seedvault.ui.BackupActivityBase
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeActivity
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
* before starting.
*/
abstract class RequireProvisioningActivity : BackupActivity() {
abstract class RequireProvisioningActivity : BackupActivityBase() {
private val recoveryCodeRequest =
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 androidx.lifecycle.AndroidViewModel
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.service.crypto.KeyManager
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
abstract class RequireProvisioningViewModel(
protected val app: Application,
protected val settingsManager: SettingsManager,
protected val settingsService: SettingsService,
protected val keyManager: KeyManager,
) : AndroidViewModel(app) {
@ -18,7 +20,7 @@ abstract class RequireProvisioningViewModel(
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
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()

View file

@ -5,11 +5,11 @@ import android.view.MenuItem
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.isDebugBuild
import com.stevesoltys.seedvault.ui.BackupActivity
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import com.stevesoltys.seedvault.ui.BackupActivityBase
import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_RESTORE
import org.koin.androidx.viewmodel.ext.android.viewModel
class RecoveryCodeActivity : BackupActivity() {
class RecoveryCodeActivity : BackupActivityBase() {
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.toSeed
import com.stevesoltys.seedvault.App
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent
import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -29,17 +29,17 @@ private val TAG = RecoveryCodeViewModel::class.java.simpleName
internal class RecoveryCodeViewModel(
app: App,
private val crypto: Crypto,
private val cryptoService: CryptoService,
private val keyManager: KeyManager,
private val backupManager: IBackupManager,
private val backupCoordinator: BackupCoordinator,
private val backupCoordinatorService: BackupCoordinatorService,
private val notificationManager: BackupNotificationManager,
private val storageBackup: StorageBackup,
) : AndroidViewModel(app) {
internal val wordList: List<CharArray> by lazy {
// 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
Mnemonics.MnemonicCode(entropy).words
}
@ -73,7 +73,7 @@ internal class RecoveryCodeViewModel(
fun verifyExistingCode(input: List<CharSequence>) {
// we validate the code again, just in case
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
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
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.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.service.metadata.BackupMetadata
import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
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 androidx.annotation.CallSuper
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
import com.stevesoltys.seedvault.ui.restore.apk.InstallProgressFragment
import com.stevesoltys.seedvault.ui.provision.RequireProvisioningActivity
import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
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.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.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.view.LayoutInflater
@ -8,7 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.Adapter
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
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.AppViewHolder
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.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.HOUR_IN_MILLIS
@ -10,7 +10,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
import com.stevesoltys.seedvault.ui.restore.RestoreSetAdapter.RestoreSetViewHolder
internal class RestoreSetAdapter(
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.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.backup.BackupManager
@ -18,28 +18,28 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.asLiveData
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.R
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
import com.stevesoltys.seedvault.restore.install.ApkRestore
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
import com.stevesoltys.seedvault.restore.install.InstallResult
import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.service.crypto.KeyManager
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
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.ui.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
import com.stevesoltys.seedvault.ui.restore.apk.ApkRestore
import com.stevesoltys.seedvault.ui.restore.apk.InstallIntentCreator
import com.stevesoltys.seedvault.ui.restore.apk.InstallResult
import com.stevesoltys.seedvault.ui.restore.apk.isInstalled
import com.stevesoltys.seedvault.service.settings.SettingsService
import com.stevesoltys.seedvault.service.file.restore.FileRestoreService
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.FAILED
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.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.getAppName
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@ -74,20 +74,20 @@ internal const val PACKAGES_PER_CHUNK = 100
internal class RestoreViewModel(
app: Application,
settingsManager: SettingsManager,
settingsService: SettingsService,
keyManager: KeyManager,
private val backupManager: IBackupManager,
private val restoreCoordinator: RestoreCoordinator,
private val apkRestore: ApkRestore,
storageBackup: StorageBackup,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : RequireProvisioningViewModel(app, settingsManager, keyManager),
) : RequireProvisioningViewModel(app, settingsService, keyManager),
RestorableBackupClickListener, SnapshotViewModel {
override val isRestoreOperation = true
private var session: IRestoreSession? = null
private val monitor = BackupMonitor()
private val monitor = BackupManagerOperationMonitor()
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
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),
// use the token of the current restore set from now on
if (settingsManager.getToken() == null) {
settingsManager.setNewToken(token)
if (settingsService.getToken() == null) {
settingsService.setNewToken(token)
}
// start a new restore session
@ -316,7 +316,7 @@ internal class RestoreViewModel(
private val restorableBackup: RestorableBackup,
private val session: IRestoreSession,
private val packages: List<String>,
private val monitor: BackupMonitor,
private val monitor: BackupManagerOperationMonitor,
) : IRestoreObserver.Stub() {
/**
@ -441,7 +441,7 @@ internal class RestoreViewModel(
@UiThread
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_TIMESTAMP_START, item.time)
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.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
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() {
companion object {
private val TAG = BroadcastReceiver::class.java.simpleName
private const val RESTORE_SECRET_CODE = "7378673"
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.data?.host != RESTORE_SECRET_CODE) return
Log.d(TAG, "Restore secret code received.")
val i = Intent(context, RestoreActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
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.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.PackageManager
import android.util.Log
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
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.pm.PackageManager
import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash
import com.stevesoltys.seedvault.transport.backup.getSignatures
import com.stevesoltys.seedvault.transport.backup.isSystemApp
import com.stevesoltys.seedvault.service.crypto.CryptoService
import com.stevesoltys.seedvault.service.metadata.ApkSplit
import com.stevesoltys.seedvault.service.metadata.PackageMetadata
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
import com.stevesoltys.seedvault.service.storage.StoragePlugin
import com.stevesoltys.seedvault.ui.restore.RestorableBackup
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED_SYSTEM_APP
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.service.app.backup.apk.copyStreamsAndGetHash
import com.stevesoltys.seedvault.service.app.backup.apk.getSignatures
import com.stevesoltys.seedvault.service.app.isSystemApp
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
@ -31,7 +31,7 @@ internal class ApkRestore(
private val storagePlugin: StoragePlugin,
@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin,
private val crypto: Crypto,
private val cryptoService: CryptoService,
private val splitCompatChecker: ApkSplitCompatibilityChecker,
private val apkInstaller: ApkInstaller,
) {
@ -222,7 +222,7 @@ internal class ApkRestore(
@Suppress("Deprecation")
legacyStoragePlugin.getApkInputStream(token, packageName, suffix)
} else {
val name = crypto.getNameForApk(salt, packageName, suffix)
val name = cryptoService.getNameForApk(salt, packageName, suffix)
storagePlugin.getInputStream(token, name)
}
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

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.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.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.dsl.module

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