Refactor and repackage Seedvault
This commit is contained in:
parent
fa4c52fb83
commit
59a51d21b2
151 changed files with 1907 additions and 1512 deletions
.idea
app/src
androidTest/java/com/stevesoltys/seedvault
KoinInstrumentationTestApp.ktPluginTest.kt
e2e
LargeBackupTestBase.ktLargeRestoreTestBase.ktLargeTestBase.ktSeedvaultLargeTest.ktSeedvaultLargeTestResult.kt
impl
plugins/saf
transport/backup
debug
main
AndroidManifest.xml
java/com/stevesoltys/seedvault
App.ktUsbIntentReceiver.kt
header
service
app
BackupManagerOperationMonitor.ktPackageService.kt
backup
restore
crypto
file
header
metadata
settings
storage
storage
transport
ui
BackupActivityBase.kt
files
liveevent
notification
provision
recoverycode
restore
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
|
@ -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>
|
||||
|
|
6
.idea/copyright/Apache_2_0.xml
generated
6
.idea/copyright/Apache_2_0.xml
generated
|
@ -1,6 +0,0 @@
|
|||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="SPDX-FileCopyrightText: &#36;today.year The Calyx Institute SPDX-License-Identifier: Apache-2.0" />
|
||||
<option name="myName" value="Apache-2.0" />
|
||||
</copyright>
|
||||
</component>
|
|
@ -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())) }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package com.stevesoltys.seedvault.header
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val headerModule = module {
|
||||
single<HeaderReader> { HeaderReaderImpl() }
|
||||
}
|
|
@ -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, "?"))
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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 ->
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
package com.stevesoltys.seedvault.service.app.restore
|
||||
|
||||
import com.stevesoltys.seedvault.ui.restore.RestorableBackup
|
||||
|
||||
class AppRestoreService {
|
||||
|
||||
fun initiateRestore(restorableBackup: RestorableBackup) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
|
|
@ -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?,
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.crypto
|
||||
package com.stevesoltys.seedvault.service.crypto
|
||||
|
||||
import java.security.Key
|
||||
import javax.crypto.Cipher
|
|
@ -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()) }
|
||||
}
|
|
@ -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
|
|
@ -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"
|
||||
)
|
|
@ -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
|
|
@ -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()) }
|
||||
}
|
|
@ -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,
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -0,0 +1,7 @@
|
|||
package com.stevesoltys.seedvault.service.header
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val headerModule = module {
|
||||
single<HeaderDecodeService> { HeaderDecodeServiceImpl() }
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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()) }
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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 {
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.stevesoltys.seedvault.service.storage
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
class EncryptedBackupMetadata(
|
||||
val token: Long,
|
||||
val inputStreamRetriever: () -> InputStream,
|
||||
)
|
|
@ -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)
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()) }
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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
|
||||
|
|
|
@ -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>>() {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.ui;
|
||||
package com.stevesoltys.seedvault.ui.liveevent;
|
||||
|
||||
public interface LiveEventHandler<T> {
|
||||
void onEvent(T t);
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.ui
|
||||
package com.stevesoltys.seedvault.ui.liveevent
|
||||
|
||||
class MutableLiveEvent<T> : LiveEvent<T>() {
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ->
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
@ -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() {
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
package com.stevesoltys.seedvault.ui.restore
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
package com.stevesoltys.seedvault.ui.restore
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
|
@ -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,
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
package com.stevesoltys.seedvault.ui.restore
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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())
|
|
@ -1,4 +1,4 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
package com.stevesoltys.seedvault.ui.restore.apk
|
||||
|
||||
import android.util.Log
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Add table
Reference in a new issue