Refactor and repackage Seedvault
This commit is contained in:
parent
fa4c52fb83
commit
59a51d21b2
151 changed files with 1907 additions and 1512 deletions
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
|
|
@ -1,11 +1,6 @@
|
||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
|
||||||
<value />
|
|
||||||
</option>
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
|
||||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
|
|
|
||||||
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
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
|
||||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
import com.stevesoltys.seedvault.service.app.backup.InputFactory
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackup
|
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
|
||||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
||||||
|
|
@ -26,8 +26,8 @@ class KoinInstrumentationTestApp : App() {
|
||||||
val context = this@KoinInstrumentationTestApp
|
val context = this@KoinInstrumentationTestApp
|
||||||
|
|
||||||
single { spyk(BackupNotificationManager(context)) }
|
single { spyk(BackupNotificationManager(context)) }
|
||||||
single { spyk(FullBackup(get(), get(), get(), get())) }
|
single { spyk(FullBackupService(get(), get(), get(), get())) }
|
||||||
single { spyk(KVBackup(get(), get(), get(), get(), get())) }
|
single { spyk(KVBackupService(get(), get(), get(), get(), get())) }
|
||||||
single { spyk(InputFactory()) }
|
single { spyk(InputFactory()) }
|
||||||
|
|
||||||
single { spyk(FullRestore(get(), get(), get(), get(), get())) }
|
single { spyk(FullRestore(get(), get(), get(), get(), get())) }
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ import androidx.test.core.content.pm.PackageInfoBuilder
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
import com.stevesoltys.seedvault.service.storage.saf.DocumentsProviderStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.plugins.saf.deleteContents
|
import com.stevesoltys.seedvault.service.storage.saf.deleteContents
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -33,9 +33,9 @@ import org.koin.core.component.inject
|
||||||
class PluginTest : KoinComponent {
|
class PluginTest : KoinComponent {
|
||||||
|
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val settingsService: SettingsService by inject()
|
||||||
private val mockedSettingsManager: SettingsManager = mockk()
|
private val mockedSettingsService: SettingsService = mockk()
|
||||||
private val storage = DocumentsStorage(context, mockedSettingsManager)
|
private val storage = DocumentsStorage(context, mockedSettingsService)
|
||||||
|
|
||||||
private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage)
|
private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage)
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ class PluginTest : KoinComponent {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() = runBlocking {
|
fun setup() = runBlocking {
|
||||||
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
|
every { mockedSettingsService.getStorage() } returns settingsService.getStorage()
|
||||||
storage.rootBackupDir?.deleteContents(context)
|
storage.rootBackupDir?.deleteContents(context)
|
||||||
?: error("Select a storage location in the app first!")
|
?: error("Select a storage location in the app first!")
|
||||||
}
|
}
|
||||||
|
|
@ -76,11 +76,11 @@ class PluginTest : KoinComponent {
|
||||||
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
|
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
|
||||||
// no backups available initially
|
// no backups available initially
|
||||||
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
val s = settingsManager.getStorage() ?: error("no storage")
|
val s = settingsService.getStorage() ?: error("no storage")
|
||||||
assertFalse(storagePlugin.hasBackup(s))
|
assertFalse(storagePlugin.hasBackup(s))
|
||||||
|
|
||||||
// prepare returned tokens requested when initializing device
|
// prepare returned tokens requested when initializing device
|
||||||
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
every { mockedSettingsService.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
||||||
|
|
||||||
// start new restore set and initialize device afterwards
|
// start new restore set and initialize device afterwards
|
||||||
storagePlugin.startNewRestoreSet(token)
|
storagePlugin.startNewRestoreSet(token)
|
||||||
|
|
@ -114,7 +114,7 @@ class PluginTest : KoinComponent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
|
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
|
||||||
every { mockedSettingsManager.getToken() } returns token
|
every { mockedSettingsService.getToken() } returns token
|
||||||
|
|
||||||
storagePlugin.startNewRestoreSet(token)
|
storagePlugin.startNewRestoreSet(token)
|
||||||
storagePlugin.initializeDevice()
|
storagePlugin.initializeDevice()
|
||||||
|
|
@ -216,7 +216,7 @@ class PluginTest : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initStorage(token: Long) = runBlocking {
|
private fun initStorage(token: Long) = runBlocking {
|
||||||
every { mockedSettingsManager.getToken() } returns token
|
every { mockedSettingsService.getToken() } returns token
|
||||||
storagePlugin.initializeDevice()
|
storagePlugin.initializeDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import android.os.ParcelFileDescriptor
|
||||||
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
|
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
|
||||||
import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
|
import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
import com.stevesoltys.seedvault.service.app.backup.InputFactory
|
||||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackup
|
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
|
@ -27,9 +27,9 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
|
|
||||||
val spyBackupNotificationManager: BackupNotificationManager get() = get()
|
val spyBackupNotificationManager: BackupNotificationManager get() = get()
|
||||||
|
|
||||||
val spyFullBackup: FullBackup get() = get()
|
val spyFullBackupService: FullBackupService get() = get()
|
||||||
|
|
||||||
val spyKVBackup: KVBackup get() = get()
|
val spyKVBackupService: KVBackupService get() = get()
|
||||||
|
|
||||||
val spyInputFactory: InputFactory get() = get()
|
val spyInputFactory: InputFactory get() = get()
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
|
|
||||||
return backupResult.copy(
|
return backupResult.copy(
|
||||||
backupResults = backupResult.allUserApps().associate {
|
backupResults = backupResult.allUserApps().associate {
|
||||||
it.packageName to spyMetadataManager.getPackageMetadata(it.packageName)
|
it.packageName to spyMetadataService.getPackageMetadata(it.packageName)
|
||||||
}.toMutableMap()
|
}.toMutableMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun spyOnBackup(backupResult: SeedvaultLargeTestResult): AtomicBoolean {
|
private fun spyOnBackup(backupResult: SeedvaultLargeTestResult): AtomicBoolean {
|
||||||
clearMocks(spyInputFactory, spyKVBackup, spyFullBackup)
|
clearMocks(spyInputFactory, spyKVBackupService, spyFullBackupService)
|
||||||
spyOnFullBackupData(backupResult)
|
spyOnFullBackupData(backupResult)
|
||||||
spyOnKVBackupData(backupResult)
|
spyOnKVBackupData(backupResult)
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
var data = mutableMapOf<String, ByteArray>()
|
var data = mutableMapOf<String, ByteArray>()
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
spyKVBackup.performBackup(any(), any(), any(), any(), any())
|
spyKVBackupService.performBackup(any(), any(), any(), any(), any())
|
||||||
} answers {
|
} answers {
|
||||||
packageName = firstArg<PackageInfo>().packageName
|
packageName = firstArg<PackageInfo>().packageName
|
||||||
callOriginal()
|
callOriginal()
|
||||||
|
|
@ -117,7 +117,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
spyKVBackup.finishBackup()
|
spyKVBackupService.finishBackup()
|
||||||
} answers {
|
} answers {
|
||||||
backupResult.kv[packageName!!] = data
|
backupResult.kv[packageName!!] = data
|
||||||
.mapValues { entry -> entry.value.sha256() }
|
.mapValues { entry -> entry.value.sha256() }
|
||||||
|
|
@ -134,7 +134,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
var dataIntercept = ByteArrayOutputStream()
|
var dataIntercept = ByteArrayOutputStream()
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
spyFullBackup.performFullBackup(any(), any(), any(), any(), any())
|
spyFullBackupService.performFullBackup(any(), any(), any(), any(), any())
|
||||||
} answers {
|
} answers {
|
||||||
packageName = firstArg<PackageInfo>().packageName
|
packageName = firstArg<PackageInfo>().packageName
|
||||||
callOriginal()
|
callOriginal()
|
||||||
|
|
@ -150,7 +150,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
every {
|
every {
|
||||||
spyFullBackup.finishBackup()
|
spyFullBackupService.finishBackup()
|
||||||
} answers {
|
} answers {
|
||||||
val result = callOriginal()
|
val result = callOriginal()
|
||||||
backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
|
backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
|
||||||
import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
|
import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
|
||||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,21 @@ import androidx.preference.PreferenceManager
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.uiautomator.UiDevice
|
import androidx.test.uiautomator.UiDevice
|
||||||
import androidx.test.uiautomator.Until
|
import androidx.test.uiautomator.Until
|
||||||
import com.stevesoltys.seedvault.crypto.ANDROID_KEY_STORE
|
import com.stevesoltys.seedvault.service.crypto.ANDROID_KEY_STORE
|
||||||
import com.stevesoltys.seedvault.crypto.KEY_ALIAS_BACKUP
|
import com.stevesoltys.seedvault.service.crypto.KEY_ALIAS_BACKUP
|
||||||
import com.stevesoltys.seedvault.crypto.KEY_ALIAS_MAIN
|
import com.stevesoltys.seedvault.service.crypto.KEY_ALIAS_MAIN
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.service.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.currentRestoreStorageViewModel
|
import com.stevesoltys.seedvault.currentRestoreStorageViewModel
|
||||||
import com.stevesoltys.seedvault.currentRestoreViewModel
|
import com.stevesoltys.seedvault.currentRestoreViewModel
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
|
||||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
import com.stevesoltys.seedvault.service.app.PackageService
|
||||||
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -65,13 +65,13 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
val packageService: PackageService get() = get()
|
val packageService: PackageService get() = get()
|
||||||
|
|
||||||
val settingsManager: SettingsManager get() = get()
|
val settingsService: SettingsService get() = get()
|
||||||
|
|
||||||
val keyManager: KeyManager get() = get()
|
val keyManager: KeyManager get() = get()
|
||||||
|
|
||||||
val documentsStorage: DocumentsStorage get() = get()
|
val documentsStorage: DocumentsStorage get() = get()
|
||||||
|
|
||||||
val spyMetadataManager: MetadataManager get() = get()
|
val spyMetadataService: MetadataService get() = get()
|
||||||
|
|
||||||
val backupManager: IBackupManager get() = get()
|
val backupManager: IBackupManager get() = get()
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
fun resetApplicationState() {
|
fun resetApplicationState() {
|
||||||
backupManager.setAutoRestore(false)
|
backupManager.setAutoRestore(false)
|
||||||
settingsManager.setNewToken(null)
|
settingsService.setNewToken(null)
|
||||||
documentsStorage.reset(null)
|
documentsStorage.reset(null)
|
||||||
|
|
||||||
val sharedPreferences = permitDiskReads {
|
val sharedPreferences = permitDiskReads {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ internal abstract class SeedvaultLargeTest :
|
||||||
if (arguments.getString("d2d_backup_test") == "true") {
|
if (arguments.getString("d2d_backup_test") == "true") {
|
||||||
println("Enabling D2D backups for test")
|
println("Enabling D2D backups for test")
|
||||||
|
|
||||||
settingsManager.setD2dBackupsEnabled(true)
|
settingsService.setD2dBackupsEnabled(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.stevesoltys.seedvault.e2e
|
package com.stevesoltys.seedvault.e2e
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.service.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.restore.AppRestoreResult
|
import com.stevesoltys.seedvault.ui.restore.AppRestoreResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains maps of (package name -> SHA-256 hashes) of application data.
|
* Contains maps of (package name -> SHA-256 hashes) of application data.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.stevesoltys.seedvault.e2e.impl
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
|
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
|
||||||
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
|
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState
|
import com.stevesoltys.seedvault.service.metadata.PackageState
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
|
|
@ -17,7 +17,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
|
||||||
confirmCode()
|
confirmCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsManager.getStorage() == null) {
|
if (settingsService.getStorage() == null) {
|
||||||
chooseStorageLocation()
|
chooseStorageLocation()
|
||||||
} else {
|
} else {
|
||||||
changeBackupLocation()
|
changeBackupLocation()
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ import com.stevesoltys.seedvault.assertReadEquals
|
||||||
import com.stevesoltys.seedvault.coAssertThrows
|
import com.stevesoltys.seedvault.coAssertThrows
|
||||||
import com.stevesoltys.seedvault.getRandomBase64
|
import com.stevesoltys.seedvault.getRandomBase64
|
||||||
import com.stevesoltys.seedvault.getRandomByteArray
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.createOrGetFile
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.findFileBlocking
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.getLoadedCursor
|
||||||
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.writeAndClose
|
import com.stevesoltys.seedvault.writeAndClose
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -44,8 +48,8 @@ import kotlin.random.Random
|
||||||
class DocumentsStorageTest : KoinComponent {
|
class DocumentsStorageTest : KoinComponent {
|
||||||
|
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val settingsManager by inject<SettingsManager>()
|
private val settingsService by inject<SettingsService>()
|
||||||
private val storage = DocumentsStorage(context, settingsManager)
|
private val storage = DocumentsStorage(context, settingsService)
|
||||||
|
|
||||||
private val filename = getRandomBase64()
|
private val filename = getRandomBase64()
|
||||||
private lateinit var file: DocumentFile
|
private lateinit var file: DocumentFile
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.transport.backup
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.stevesoltys.seedvault.service.app.PackageService
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
<application>
|
<application>
|
||||||
<!-- Remove permission requirements only for debug versions to make development easier -->
|
<!-- Remove permission requirements only for debug versions to make development easier -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.stevesoltys.seedvault.settings.SettingsActivity"
|
android:name="com.stevesoltys.seedvault.ui.settings.SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
tools:remove="android:permission" />
|
tools:remove="android:permission" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.stevesoltys.seedvault.restore.RestoreActivity"
|
android:name="com.stevesoltys.seedvault.ui.restore.RestoreActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
tools:remove="android:permission" />
|
tools:remove="android:permission" />
|
||||||
</application>
|
</application>
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".ui.settings.SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="com.stevesoltys.seedvault.OPEN_SETTINGS" />
|
android:permission="com.stevesoltys.seedvault.OPEN_SETTINGS" />
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@
|
||||||
android:theme="@style/AppTheme.NoActionBar" />
|
android:theme="@style/AppTheme.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".restore.RestoreActivity"
|
android:name=".ui.restore.RestoreActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/restore_title"
|
android:label="@string/restore_title"
|
||||||
android:permission="com.stevesoltys.seedvault.RESTORE_BACKUP"
|
android:permission="com.stevesoltys.seedvault.RESTORE_BACKUP"
|
||||||
|
|
@ -137,7 +137,7 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".restore.RestoreErrorBroadcastReceiver"
|
android:name=".ui.restore.RestoreErrorBroadcastReceiver"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.stevesoltys.seedvault.action.UNINSTALL" />
|
<action android:name="com.stevesoltys.seedvault.action.UNINSTALL" />
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".SecretCodeReceiver"
|
android:name=".ui.restore.SecretCodeReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.telephony.action.SECRET_CODE" />
|
<action android:name="android.telephony.action.SECRET_CODE" />
|
||||||
|
|
@ -158,19 +158,19 @@
|
||||||
|
|
||||||
<!-- Used to start actual BackupService depending on scheduling criteria -->
|
<!-- Used to start actual BackupService depending on scheduling criteria -->
|
||||||
<service
|
<service
|
||||||
android:name=".storage.StorageBackupJobService"
|
android:name=".service.file.backup.FileBackupJobService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="BackupJobService"
|
android:label="BackupJobService"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
<!-- Does the actual backup work as a foreground service -->
|
<!-- Does the actual backup work as a foreground service -->
|
||||||
<service
|
<service
|
||||||
android:name=".storage.StorageBackupService"
|
android:name=".service.file.backup.FileBackupService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
android:label="BackupService" />
|
android:label="BackupService" />
|
||||||
<!-- Does restore as a foreground service -->
|
<!-- Does restore as a foreground service -->
|
||||||
<service
|
<service
|
||||||
android:name=".storage.StorageRestoreService"
|
android:name=".service.file.restore.FileRestoreService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
android:label="RestoreService" />
|
android:label="RestoreService" />
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,25 @@ import android.os.Build
|
||||||
import android.os.ServiceManager.getService
|
import android.os.ServiceManager.getService
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import com.stevesoltys.seedvault.crypto.cryptoModule
|
import com.stevesoltys.seedvault.service.crypto.cryptoModule
|
||||||
import com.stevesoltys.seedvault.header.headerModule
|
import com.stevesoltys.seedvault.service.header.headerModule
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
import com.stevesoltys.seedvault.service.metadata.metadataModule
|
||||||
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
import com.stevesoltys.seedvault.service.storage.saf.documentsProviderModule
|
||||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
|
||||||
import com.stevesoltys.seedvault.restore.install.installModule
|
import com.stevesoltys.seedvault.ui.restore.apk.installModule
|
||||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
import com.stevesoltys.seedvault.ui.settings.AppListRetriever
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
import com.stevesoltys.seedvault.ui.settings.SettingsViewModel
|
||||||
import com.stevesoltys.seedvault.storage.storageModule
|
import com.stevesoltys.seedvault.service.file.filesModule
|
||||||
import com.stevesoltys.seedvault.transport.backup.backupModule
|
import com.stevesoltys.seedvault.service.app.backup.backupModule
|
||||||
import com.stevesoltys.seedvault.transport.restore.restoreModule
|
import com.stevesoltys.seedvault.service.app.restore.restoreModule
|
||||||
import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
|
import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
|
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
||||||
|
import com.stevesoltys.seedvault.util.TimeSource
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
|
@ -43,9 +44,9 @@ import org.koin.dsl.module
|
||||||
open class App : Application() {
|
open class App : Application() {
|
||||||
|
|
||||||
private val appModule = module {
|
private val appModule = module {
|
||||||
single { SettingsManager(this@App) }
|
single { SettingsService(this@App) }
|
||||||
single { BackupNotificationManager(this@App) }
|
single { BackupNotificationManager(this@App) }
|
||||||
single { Clock() }
|
single { TimeSource() }
|
||||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
factory { AppListRetriever(this@App, get(), get(), get()) }
|
||||||
|
|
||||||
|
|
@ -94,24 +95,24 @@ open class App : Application() {
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
installModule,
|
installModule,
|
||||||
storageModule,
|
filesModule,
|
||||||
appModule
|
appModule
|
||||||
)
|
)
|
||||||
|
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val settingsService: SettingsService by inject()
|
||||||
private val metadataManager: MetadataManager by inject()
|
private val metadataService: MetadataService by inject()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The responsibility for the current token was moved to the [SettingsManager]
|
* The responsibility for the current token was moved to the [SettingsService]
|
||||||
* in the end of 2020.
|
* in the end of 2020.
|
||||||
* This method migrates the token for existing installs and can be removed
|
* This method migrates the token for existing installs and can be removed
|
||||||
* after sufficient time has passed.
|
* after sufficient time has passed.
|
||||||
*/
|
*/
|
||||||
private fun migrateTokenFromMetadataToSettingsManager() {
|
private fun migrateTokenFromMetadataToSettingsManager() {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val token = metadataManager.getBackupToken()
|
val token = metadataService.getBackupToken()
|
||||||
if (token != 0L && settingsManager.getToken() == null) {
|
if (token != 0L && settingsService.getToken() == null) {
|
||||||
settingsManager.setNewToken(token)
|
settingsService.setNewToken(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ import android.os.Looper
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat.startForegroundService
|
import androidx.core.content.ContextCompat.startForegroundService
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.app.backup.requestBackup
|
||||||
import com.stevesoltys.seedvault.settings.FlashDrive
|
import com.stevesoltys.seedvault.service.file.backup.FileBackupService
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.file.backup.FileBackupService.Companion.EXTRA_START_APP_BACKUP
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupService
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
import com.stevesoltys.seedvault.service.settings.FlashDrive
|
||||||
import com.stevesoltys.seedvault.transport.requestBackup
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
|
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
|
||||||
import org.koin.core.context.GlobalContext.get
|
import org.koin.core.context.GlobalContext.get
|
||||||
import java.util.concurrent.TimeUnit.HOURS
|
import java.util.concurrent.TimeUnit.HOURS
|
||||||
|
|
@ -32,17 +32,17 @@ private const val HOURS_AUTO_BACKUP: Long = 24
|
||||||
class UsbIntentReceiver : UsbMonitor() {
|
class UsbIntentReceiver : UsbMonitor() {
|
||||||
|
|
||||||
// using KoinComponent would crash robolectric tests :(
|
// using KoinComponent would crash robolectric tests :(
|
||||||
private val settingsManager: SettingsManager by lazy { get().get() }
|
private val settingsService: SettingsService by lazy { get().get() }
|
||||||
private val metadataManager: MetadataManager by lazy { get().get() }
|
private val metadataService: MetadataService by lazy { get().get() }
|
||||||
|
|
||||||
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
|
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
|
||||||
if (action != ACTION_USB_DEVICE_ATTACHED) return false
|
if (action != ACTION_USB_DEVICE_ATTACHED) return false
|
||||||
Log.d(TAG, "Checking if this is the current backup drive.")
|
Log.d(TAG, "Checking if this is the current backup drive.")
|
||||||
val savedFlashDrive = settingsManager.getFlashDrive() ?: return false
|
val savedFlashDrive = settingsService.getFlashDrive() ?: return false
|
||||||
val attachedFlashDrive = FlashDrive.from(device)
|
val attachedFlashDrive = FlashDrive.from(device)
|
||||||
return if (savedFlashDrive == attachedFlashDrive) {
|
return if (savedFlashDrive == attachedFlashDrive) {
|
||||||
Log.d(TAG, "Matches stored device, checking backup time...")
|
Log.d(TAG, "Matches stored device, checking backup time...")
|
||||||
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
|
val backupMillis = System.currentTimeMillis() - metadataService.getLastBackupTime()
|
||||||
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
|
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
|
||||||
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
|
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
|
||||||
true
|
true
|
||||||
|
|
@ -57,8 +57,8 @@ class UsbIntentReceiver : UsbMonitor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
|
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
|
||||||
if (settingsManager.isStorageBackupEnabled()) {
|
if (settingsService.isStorageBackupEnabled()) {
|
||||||
val i = Intent(context, StorageBackupService::class.java)
|
val i = Intent(context, FileBackupService::class.java)
|
||||||
// this starts an app backup afterwards
|
// this starts an app backup afterwards
|
||||||
i.putExtra(EXTRA_START_APP_BACKUP, true)
|
i.putExtra(EXTRA_START_APP_BACKUP, true)
|
||||||
startForegroundService(context, i)
|
startForegroundService(context, i)
|
||||||
|
|
|
||||||
|
|
@ -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_CATEGORY
|
||||||
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID
|
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID
|
||||||
|
|
@ -8,9 +8,9 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Log.DEBUG
|
import android.util.Log.DEBUG
|
||||||
|
|
||||||
private val TAG = BackupMonitor::class.java.name
|
private val TAG = BackupManagerOperationMonitor::class.java.name
|
||||||
|
|
||||||
class BackupMonitor : IBackupManagerMonitor.Stub() {
|
class BackupManagerOperationMonitor : IBackupManagerMonitor.Stub() {
|
||||||
|
|
||||||
override fun onEvent(bundle: Bundle) {
|
override fun onEvent(bundle: Bundle) {
|
||||||
if (!Log.isLoggable(TAG, DEBUG)) return
|
if (!Log.isLoggable(TAG, DEBUG)) return
|
||||||
|
|
@ -18,5 +18,4 @@ class BackupMonitor : IBackupManagerMonitor.Stub() {
|
||||||
Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
|
Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
|
||||||
Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"))
|
Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.service.app
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
||||||
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
||||||
|
|
@ -12,17 +11,12 @@ import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.GET_INSTRUMENTATION
|
import android.content.pm.PackageManager.GET_INSTRUMENTATION
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.os.UserHandle
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Log.INFO
|
import android.util.Log.INFO
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
|
|
||||||
private val TAG = PackageService::class.java.simpleName
|
|
||||||
|
|
||||||
private const val LOG_MAX_PACKAGES = 100
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
|
|
@ -30,13 +24,17 @@ private const val LOG_MAX_PACKAGES = 100
|
||||||
*/
|
*/
|
||||||
internal class PackageService(
|
internal class PackageService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val backupManager: IBackupManager,
|
private val settingsService: SettingsService,
|
||||||
private val settingsManager: SettingsManager,
|
|
||||||
private val plugin: StoragePlugin,
|
private val plugin: StoragePlugin,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PackageService::class.java.simpleName
|
||||||
|
|
||||||
|
private const val LOG_MAX_PACKAGES = 100
|
||||||
|
}
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
private val packageManager: PackageManager = context.packageManager
|
||||||
private val myUserId = UserHandle.myUserId()
|
|
||||||
|
|
||||||
val eligiblePackages: Array<String>
|
val eligiblePackages: Array<String>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|
@ -96,7 +94,7 @@ internal class PackageService(
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
|
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
|
||||||
packageInfo.isUserVisible(context) &&
|
packageInfo.isUserVisible(context) &&
|
||||||
packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled())
|
packageInfo.allowsBackup(settingsService.d2dBackupsEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,7 +103,7 @@ internal class PackageService(
|
||||||
val userNotAllowedApps: List<PackageInfo>
|
val userNotAllowedApps: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
||||||
!packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) &&
|
!packageInfo.allowsBackup(settingsService.d2dBackupsEnabled()) &&
|
||||||
!packageInfo.isSystemApp()
|
!packageInfo.isSystemApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +138,7 @@ internal class PackageService(
|
||||||
// apps that we, or the user, want to exclude.
|
// apps that we, or the user, want to exclude.
|
||||||
|
|
||||||
// Check that the app is not excluded by user preference
|
// Check that the app is not excluded by user preference
|
||||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
val enabled = settingsService.isBackupEnabled(packageName)
|
||||||
|
|
||||||
// We need to explicitly exclude DocumentsProvider and Seedvault.
|
// We need to explicitly exclude DocumentsProvider and Seedvault.
|
||||||
// Otherwise, they get killed while backing them up, terminating our backup.
|
// Otherwise, they get killed while backing them up, terminating our backup.
|
||||||
|
|
@ -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.app.backup.BackupDataInput
|
||||||
import android.os.ParcelFileDescriptor
|
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.annotation.SuppressLint
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
|
|
@ -8,13 +16,15 @@ import android.content.pm.SigningInfo
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.PackageUtils.computeSha256DigestBytes
|
import android.util.PackageUtils.computeSha256DigestBytes
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
import com.stevesoltys.seedvault.util.encodeBase64
|
||||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
import com.stevesoltys.seedvault.service.metadata.ApkSplit
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.service.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState
|
import com.stevesoltys.seedvault.service.metadata.PackageState
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
|
import com.stevesoltys.seedvault.service.app.isNotUpdatedSystemApp
|
||||||
|
import com.stevesoltys.seedvault.service.app.isTestOnly
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
@ -23,14 +33,14 @@ import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
private val TAG = ApkBackup::class.java.simpleName
|
private val TAG = ApkBackupService::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class ApkBackup(
|
internal class ApkBackupService(
|
||||||
private val pm: PackageManager,
|
private val pm: PackageManager,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsService: SettingsService,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataService: MetadataService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,7 +63,7 @@ internal class ApkBackup(
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER) return null
|
if (packageName == MAGIC_PACKAGE_MANAGER) return null
|
||||||
|
|
||||||
// do not back up when setting is not enabled
|
// do not back up when setting is not enabled
|
||||||
if (!settingsManager.backupApks()) return null
|
if (!settingsService.backupApks()) return null
|
||||||
|
|
||||||
// do not back up test-only apps as we can't re-install them anyway
|
// do not back up test-only apps as we can't re-install them anyway
|
||||||
// see: https://commonsware.com/blog/2017/10/31/android-studio-3p0-flag-test-only.html
|
// see: https://commonsware.com/blog/2017/10/31/android-studio-3p0-flag-test-only.html
|
||||||
|
|
@ -82,7 +92,7 @@ internal class ApkBackup(
|
||||||
}
|
}
|
||||||
|
|
||||||
// get cached metadata about package
|
// get cached metadata about package
|
||||||
val packageMetadata = metadataManager.getPackageMetadata(packageName)
|
val packageMetadata = metadataService.getPackageMetadata(packageName)
|
||||||
?: PackageMetadata()
|
?: PackageMetadata()
|
||||||
|
|
||||||
// get version codes
|
// get version codes
|
||||||
|
|
@ -104,7 +114,7 @@ internal class ApkBackup(
|
||||||
// get an InputStream for the APK
|
// get an InputStream for the APK
|
||||||
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
|
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
|
||||||
// copy the APK to the storage's output and calculate SHA-256 hash while at it
|
// copy the APK to the storage's output and calculate SHA-256 hash while at it
|
||||||
val name = crypto.getNameForApk(metadataManager.salt, packageName)
|
val name = cryptoService.getNameForApk(metadataService.salt, packageName)
|
||||||
val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(name))
|
val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(name))
|
||||||
|
|
||||||
// back up splits if they exist
|
// back up splits if they exist
|
||||||
|
|
@ -195,7 +205,7 @@ internal class ApkBackup(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sha256 = messageDigest.digest().encodeBase64()
|
val sha256 = messageDigest.digest().encodeBase64()
|
||||||
val name = crypto.getNameForApk(metadataManager.salt, packageName, splitName)
|
val name = cryptoService.getNameForApk(metadataService.salt, packageName, splitName)
|
||||||
// copy the split APK to the storage stream
|
// copy the split APK to the storage stream
|
||||||
getApkInputStream(sourceDir).use { inputStream ->
|
getApkInputStream(sourceDir).use { inputStream ->
|
||||||
streamGetter(name).use { outputStream ->
|
streamGetter(name).use { outputStream ->
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.service.app.backup.coordinator
|
||||||
|
|
||||||
import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED
|
import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED
|
||||||
import android.app.backup.BackupTransport.FLAG_INCREMENTAL
|
import android.app.backup.BackupTransport.FLAG_INCREMENTAL
|
||||||
|
|
@ -18,62 +18,52 @@ import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.Clock
|
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.metadata.BackupType
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.app.PackageService
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState
|
import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
import com.stevesoltys.seedvault.service.app.isStopped
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.service.app.isSystemApp
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.service.metadata.BackupType
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
|
||||||
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
|
||||||
|
import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
|
||||||
|
import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
|
||||||
|
import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
|
||||||
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
import com.stevesoltys.seedvault.util.TimeSource
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.util.concurrent.TimeUnit.DAYS
|
import java.util.concurrent.TimeUnit.DAYS
|
||||||
import java.util.concurrent.TimeUnit.HOURS
|
import java.util.concurrent.TimeUnit.HOURS
|
||||||
|
|
||||||
private val TAG = BackupCoordinator::class.java.simpleName
|
|
||||||
|
|
||||||
private class CoordinatorState(
|
|
||||||
var calledInitialize: Boolean,
|
|
||||||
var calledClearBackupData: Boolean,
|
|
||||||
var cancelReason: PackageState,
|
|
||||||
) {
|
|
||||||
val expectFinish: Boolean
|
|
||||||
get() = calledInitialize || calledClearBackupData
|
|
||||||
|
|
||||||
fun onFinish() {
|
|
||||||
calledInitialize = false
|
|
||||||
calledClearBackupData = false
|
|
||||||
cancelReason = UNKNOWN_ERROR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
* @author Torsten Grote
|
* @author Torsten Grote
|
||||||
*/
|
*/
|
||||||
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
|
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class BackupCoordinator(
|
internal class BackupCoordinatorService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val plugin: StoragePlugin,
|
private val plugin: StoragePlugin,
|
||||||
private val kv: KVBackup,
|
private val kv: KVBackupService,
|
||||||
private val full: FullBackup,
|
private val full: FullBackupService,
|
||||||
private val apkBackup: ApkBackup,
|
private val apkBackupService: ApkBackupService,
|
||||||
private val clock: Clock,
|
private val timeSource: TimeSource,
|
||||||
private val packageService: PackageService,
|
private val packageService: PackageService,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataService: MetadataService,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsService: SettingsService,
|
||||||
private val nm: BackupNotificationManager,
|
private val nm: BackupNotificationManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val state = CoordinatorState(
|
private val TAG = BackupCoordinatorService::class.java.simpleName
|
||||||
|
|
||||||
|
private val state = BackupCoordinatorState(
|
||||||
calledInitialize = false,
|
calledInitialize = false,
|
||||||
calledClearBackupData = false,
|
calledClearBackupData = false,
|
||||||
cancelReason = UNKNOWN_ERROR
|
cancelReason = UNKNOWN_ERROR
|
||||||
|
|
@ -92,9 +82,9 @@ internal class BackupCoordinator(
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private suspend fun startNewRestoreSet(): Long {
|
private suspend fun startNewRestoreSet(): Long {
|
||||||
val token = clock.time()
|
val token = timeSource.time()
|
||||||
Log.i(TAG, "Starting new RestoreSet with token $token...")
|
Log.i(TAG, "Starting new RestoreSet with token $token...")
|
||||||
settingsManager.setNewToken(token)
|
settingsService.setNewToken(token)
|
||||||
plugin.startNewRestoreSet(token)
|
plugin.startNewRestoreSet(token)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +115,7 @@ internal class BackupCoordinator(
|
||||||
plugin.initializeDevice()
|
plugin.initializeDevice()
|
||||||
Log.d(TAG, "Resetting backup metadata for token $token...")
|
Log.d(TAG, "Resetting backup metadata for token $token...")
|
||||||
plugin.getMetadataOutputStream(token).use {
|
plugin.getMetadataOutputStream(token).use {
|
||||||
metadataManager.onDeviceInitialization(token, it)
|
metadataService.onDeviceInitialization(token, it)
|
||||||
}
|
}
|
||||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||||
// so we remember that we initialized successfully
|
// so we remember that we initialized successfully
|
||||||
|
|
@ -134,7 +124,7 @@ internal class BackupCoordinator(
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error initializing device", e)
|
Log.e(TAG, "Error initializing device", e)
|
||||||
// Show error notification if we needed init or were ready for backups
|
// Show error notification if we needed init or were ready for backups
|
||||||
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError()
|
if (metadataService.requiresInit || settingsService.canDoBackupNow()) nm.onBackupError()
|
||||||
TRANSPORT_ERROR
|
TRANSPORT_ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +220,7 @@ internal class BackupCoordinator(
|
||||||
flags: Int,
|
flags: Int,
|
||||||
): Int {
|
): Int {
|
||||||
state.cancelReason = UNKNOWN_ERROR
|
state.cancelReason = UNKNOWN_ERROR
|
||||||
if (metadataManager.requiresInit) {
|
if (metadataService.requiresInit) {
|
||||||
// start a new restore set to upgrade from legacy format
|
// start a new restore set to upgrade from legacy format
|
||||||
// by starting a clean backup with all files using the new version
|
// by starting a clean backup with all files using the new version
|
||||||
try {
|
try {
|
||||||
|
|
@ -241,8 +231,8 @@ internal class BackupCoordinator(
|
||||||
// this causes a backup error, but things should go back to normal afterwards
|
// this causes a backup error, but things should go back to normal afterwards
|
||||||
return TRANSPORT_NOT_INITIALIZED
|
return TRANSPORT_NOT_INITIALIZED
|
||||||
}
|
}
|
||||||
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
|
val token = settingsService.getToken() ?: error("no token in performFullBackup")
|
||||||
val salt = metadataManager.salt
|
val salt = metadataService.salt
|
||||||
return kv.performBackup(packageInfo, data, flags, token, salt)
|
return kv.performBackup(packageInfo, data, flags, token, salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,8 +270,8 @@ internal class BackupCoordinator(
|
||||||
flags: Int,
|
flags: Int,
|
||||||
): Int {
|
): Int {
|
||||||
state.cancelReason = UNKNOWN_ERROR
|
state.cancelReason = UNKNOWN_ERROR
|
||||||
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
|
val token = settingsService.getToken() ?: error("no token in performFullBackup")
|
||||||
val salt = metadataManager.salt
|
val salt = metadataService.salt
|
||||||
return full.performFullBackup(targetPackage, fileDescriptor, flags, token, salt)
|
return full.performFullBackup(targetPackage, fileDescriptor, flags, token, salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,8 +300,8 @@ internal class BackupCoordinator(
|
||||||
// don't bother with system apps that have no data
|
// don't bother with system apps that have no data
|
||||||
val ignoreApp = state.cancelReason == NO_DATA && packageInfo.isSystemApp()
|
val ignoreApp = state.cancelReason == NO_DATA && packageInfo.isSystemApp()
|
||||||
if (!ignoreApp) onPackageBackupError(packageInfo, BackupType.FULL)
|
if (!ignoreApp) onPackageBackupError(packageInfo, BackupType.FULL)
|
||||||
val token = settingsManager.getToken() ?: error("no token in cancelFullBackup")
|
val token = settingsService.getToken() ?: error("no token in cancelFullBackup")
|
||||||
val salt = metadataManager.salt
|
val salt = metadataService.salt
|
||||||
full.cancelFullBackup(token, salt, ignoreApp)
|
full.cancelFullBackup(token, salt, ignoreApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,8 +319,8 @@ internal class BackupCoordinator(
|
||||||
suspend fun clearBackupData(packageInfo: PackageInfo): Int {
|
suspend fun clearBackupData(packageInfo: PackageInfo): Int {
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
Log.i(TAG, "Clear Backup Data of $packageName.")
|
Log.i(TAG, "Clear Backup Data of $packageName.")
|
||||||
val token = settingsManager.getToken() ?: error("no token in clearBackupData")
|
val token = settingsService.getToken() ?: error("no token in clearBackupData")
|
||||||
val salt = metadataManager.salt
|
val salt = metadataService.salt
|
||||||
try {
|
try {
|
||||||
kv.clearBackupData(packageInfo, token, salt)
|
kv.clearBackupData(packageInfo, token, salt)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
|
@ -368,7 +358,7 @@ internal class BackupCoordinator(
|
||||||
if (result == TRANSPORT_OK) {
|
if (result == TRANSPORT_OK) {
|
||||||
val isPmBackup = packageName == MAGIC_PACKAGE_MANAGER
|
val isPmBackup = packageName == MAGIC_PACKAGE_MANAGER
|
||||||
// call onPackageBackedUp for @pm@ only if we can do backups right now
|
// call onPackageBackedUp for @pm@ only if we can do backups right now
|
||||||
if (!isPmBackup || settingsManager.canDoBackupNow()) {
|
if (!isPmBackup || settingsService.canDoBackupNow()) {
|
||||||
try {
|
try {
|
||||||
onPackageBackedUp(packageInfo, BackupType.KV)
|
onPackageBackedUp(packageInfo, BackupType.KV)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -377,7 +367,7 @@ internal class BackupCoordinator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
||||||
if (isPmBackup && settingsManager.canDoBackupNow()) {
|
if (isPmBackup && settingsService.canDoBackupNow()) {
|
||||||
try {
|
try {
|
||||||
backUpApksOfNotBackedUpPackages()
|
backUpApksOfNotBackedUpPackages()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -426,7 +416,7 @@ internal class BackupCoordinator(
|
||||||
val wasBackedUp = backUpApk(packageInfo, packageState)
|
val wasBackedUp = backUpApk(packageInfo, packageState)
|
||||||
if (!wasBackedUp) {
|
if (!wasBackedUp) {
|
||||||
val packageMetadata =
|
val packageMetadata =
|
||||||
metadataManager.getPackageMetadata(packageName)
|
metadataService.getPackageMetadata(packageName)
|
||||||
val oldPackageState = packageMetadata?.state
|
val oldPackageState = packageMetadata?.state
|
||||||
if (oldPackageState != packageState) {
|
if (oldPackageState != packageState) {
|
||||||
Log.i(
|
Log.i(
|
||||||
|
|
@ -434,7 +424,7 @@ internal class BackupCoordinator(
|
||||||
", update to $packageState"
|
", update to $packageState"
|
||||||
)
|
)
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onPackageBackupError(packageInfo, packageState, it)
|
metadataService.onPackageBackupError(packageInfo, packageState, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -455,12 +445,12 @@ internal class BackupCoordinator(
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
return try {
|
return try {
|
||||||
apkBackup.backupApkIfNecessary(packageInfo, packageState) { name ->
|
apkBackupService.backupApkIfNecessary(packageInfo, packageState) { name ->
|
||||||
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
val token = settingsService.getToken() ?: throw IOException("no current token")
|
||||||
plugin.getOutputStream(token, name)
|
plugin.getOutputStream(token, name)
|
||||||
}?.let { packageMetadata ->
|
}?.let { packageMetadata ->
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onApkBackedUp(packageInfo, packageMetadata, it)
|
metadataService.onApkBackedUp(packageInfo, packageMetadata, it)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
@ -472,7 +462,7 @@ internal class BackupCoordinator(
|
||||||
|
|
||||||
private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
|
private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onPackageBackedUp(packageInfo, type, it)
|
metadataService.onPackageBackedUp(packageInfo, type, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,7 +470,7 @@ internal class BackupCoordinator(
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
try {
|
try {
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onPackageBackupError(packageInfo, state.cancelReason, it, type)
|
metadataService.onPackageBackupError(packageInfo, state.cancelReason, it, type)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error while writing metadata for $packageName", e)
|
Log.e(TAG, "Error while writing metadata for $packageName", e)
|
||||||
|
|
@ -491,7 +481,7 @@ internal class BackupCoordinator(
|
||||||
val longBackoff = DAYS.toMillis(30)
|
val longBackoff = DAYS.toMillis(30)
|
||||||
|
|
||||||
// back off if there's no storage set
|
// back off if there's no storage set
|
||||||
val storage = settingsManager.getStorage() ?: return longBackoff
|
val storage = settingsService.getStorage() ?: return longBackoff
|
||||||
return when {
|
return when {
|
||||||
// back off if storage is removable and not available right now
|
// back off if storage is removable and not available right now
|
||||||
storage.isUnavailableUsb(context) -> longBackoff
|
storage.isUnavailableUsb(context) -> longBackoff
|
||||||
|
|
@ -503,8 +493,10 @@ internal class BackupCoordinator(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream {
|
private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream {
|
||||||
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
val t = token ?: settingsService.getToken() ?: throw IOException("no current token")
|
||||||
return getOutputStream(t, FILE_BACKUP_METADATA)
|
return getOutputStream(t,
|
||||||
|
com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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.FLAG_USER_INITIATED
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
|
|
@ -8,41 +12,26 @@ import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.app.backup.InputFactory
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.header.getADForFull
|
import com.stevesoltys.seedvault.service.header.VERSION
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.header.getADForFull
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
private class FullBackupState(
|
|
||||||
val packageInfo: PackageInfo,
|
|
||||||
val inputFileDescriptor: ParcelFileDescriptor,
|
|
||||||
val inputStream: InputStream,
|
|
||||||
var outputStreamInit: (suspend () -> OutputStream)?,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* This is an encrypted stream that can be written to directly.
|
|
||||||
*/
|
|
||||||
var outputStream: OutputStream? = null
|
|
||||||
val packageName: String = packageInfo.packageName
|
|
||||||
var size: Long = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
|
const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
|
||||||
|
|
||||||
private val TAG = FullBackup::class.java.simpleName
|
private val TAG = FullBackupService::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class FullBackup(
|
internal class FullBackupService(
|
||||||
private val plugin: StoragePlugin,
|
private val plugin: StoragePlugin,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsService: SettingsService,
|
||||||
private val inputFactory: InputFactory,
|
private val inputFactory: InputFactory,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var state: FullBackupState? = null
|
private var state: FullBackupState? = null
|
||||||
|
|
@ -52,7 +41,7 @@ internal class FullBackup(
|
||||||
fun getCurrentPackage() = state?.packageInfo
|
fun getCurrentPackage() = state?.packageInfo
|
||||||
|
|
||||||
fun getQuota(): Long {
|
fun getQuota(): Long {
|
||||||
return if (settingsManager.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP
|
return if (settingsService.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkFullBackupSize(size: Long): Int {
|
fun checkFullBackupSize(size: Long): Int {
|
||||||
|
|
@ -114,7 +103,7 @@ internal class FullBackup(
|
||||||
val inputStream = inputFactory.getInputStream(socket)
|
val inputStream = inputFactory.getInputStream(socket)
|
||||||
state = FullBackupState(targetPackage, socket, inputStream) {
|
state = FullBackupState(targetPackage, socket, inputStream) {
|
||||||
Log.d(TAG, "Initializing OutputStream for $packageName.")
|
Log.d(TAG, "Initializing OutputStream for $packageName.")
|
||||||
val name = crypto.getNameForPackage(salt, packageName)
|
val name = cryptoService.getNameForPackage(salt, packageName)
|
||||||
// get OutputStream to write backup data into
|
// get OutputStream to write backup data into
|
||||||
val outputStream = try {
|
val outputStream = try {
|
||||||
plugin.getOutputStream(token, name)
|
plugin.getOutputStream(token, name)
|
||||||
|
|
@ -127,7 +116,10 @@ internal class FullBackup(
|
||||||
// store version header
|
// store version header
|
||||||
val state = this.state ?: throw AssertionError()
|
val state = this.state ?: throw AssertionError()
|
||||||
outputStream.write(ByteArray(1) { VERSION })
|
outputStream.write(ByteArray(1) { VERSION })
|
||||||
crypto.newEncryptingStream(outputStream, getADForFull(VERSION, state.packageName))
|
cryptoService.newEncryptingStream(
|
||||||
|
outputStream,
|
||||||
|
getADForFull(VERSION, state.packageName)
|
||||||
|
)
|
||||||
} // this lambda is only called before we actually write backup data the first time
|
} // this lambda is only called before we actually write backup data the first time
|
||||||
return TRANSPORT_OK
|
return TRANSPORT_OK
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +165,7 @@ internal class FullBackup(
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
|
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
|
||||||
val name = crypto.getNameForPackage(salt, packageInfo.packageName)
|
val name = cryptoService.getNameForPackage(salt, packageInfo.packageName)
|
||||||
plugin.removeData(token, name)
|
plugin.removeData(token, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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_DATA_NOT_CHANGED
|
||||||
import android.app.backup.BackupTransport.FLAG_INCREMENTAL
|
import android.app.backup.BackupTransport.FLAG_INCREMENTAL
|
||||||
|
|
@ -10,33 +14,25 @@ import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.service.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.getADForKV
|
import com.stevesoltys.seedvault.service.header.getADForKV
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
|
import com.stevesoltys.seedvault.service.app.backup.InputFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
class KVBackupState(
|
|
||||||
internal val packageInfo: PackageInfo,
|
|
||||||
val token: Long,
|
|
||||||
val name: String,
|
|
||||||
val db: KVDb,
|
|
||||||
) {
|
|
||||||
var needsUpload: Boolean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
|
const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
|
||||||
|
|
||||||
private val TAG = KVBackup::class.java.simpleName
|
private val TAG = KVBackupService::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class KVBackup(
|
internal class KVBackupService(
|
||||||
private val plugin: StoragePlugin,
|
private val plugin: StoragePlugin,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsService: SettingsService,
|
||||||
private val inputFactory: InputFactory,
|
private val inputFactory: InputFactory,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val dbManager: KvDbManager,
|
private val dbManager: KvDbManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -46,7 +42,7 @@ internal class KVBackup(
|
||||||
|
|
||||||
fun getCurrentPackage() = state?.packageInfo
|
fun getCurrentPackage() = state?.packageInfo
|
||||||
|
|
||||||
fun getQuota(): Long = if (settingsManager.isQuotaUnlimited()) {
|
fun getQuota(): Long = if (settingsService.isQuotaUnlimited()) {
|
||||||
Long.MAX_VALUE
|
Long.MAX_VALUE
|
||||||
} else {
|
} else {
|
||||||
DEFAULT_QUOTA_KEY_VALUE_BACKUP
|
DEFAULT_QUOTA_KEY_VALUE_BACKUP
|
||||||
|
|
@ -84,7 +80,7 @@ internal class KVBackup(
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
throw AssertionError("Have state for ${state.packageInfo.packageName}")
|
throw AssertionError("Have state for ${state.packageInfo.packageName}")
|
||||||
}
|
}
|
||||||
val name = crypto.getNameForPackage(salt, packageName)
|
val name = cryptoService.getNameForPackage(salt, packageName)
|
||||||
val db = dbManager.getDb(packageName)
|
val db = dbManager.getDb(packageName)
|
||||||
this.state = KVBackupState(packageInfo, token, name, db)
|
this.state = KVBackupState(packageInfo, token, name, db)
|
||||||
|
|
||||||
|
|
@ -134,7 +130,7 @@ internal class KVBackup(
|
||||||
// K/V backups (typically starting with package manager metadata - @pm@)
|
// K/V backups (typically starting with package manager metadata - @pm@)
|
||||||
// are scheduled with JobInfo.Builder#setOverrideDeadline()
|
// are scheduled with JobInfo.Builder#setOverrideDeadline()
|
||||||
// and thus do not respect backoff.
|
// and thus do not respect backoff.
|
||||||
settingsManager.canDoBackupNow()
|
settingsService.canDoBackupNow()
|
||||||
} else {
|
} else {
|
||||||
// all other packages always need upload
|
// all other packages always need upload
|
||||||
true
|
true
|
||||||
|
|
@ -194,7 +190,7 @@ internal class KVBackup(
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
|
suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
|
||||||
Log.i(TAG, "Clearing K/V data of ${packageInfo.packageName}")
|
Log.i(TAG, "Clearing K/V data of ${packageInfo.packageName}")
|
||||||
val name = state?.name ?: crypto.getNameForPackage(salt, packageInfo.packageName)
|
val name = state?.name ?: cryptoService.getNameForPackage(salt, packageInfo.packageName)
|
||||||
plugin.removeData(token, name)
|
plugin.removeData(token, name)
|
||||||
if (!dbManager.deleteDb(packageInfo.packageName)) throw IOException()
|
if (!dbManager.deleteDb(packageInfo.packageName)) throw IOException()
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +240,7 @@ internal class KVBackup(
|
||||||
plugin.getOutputStream(token, name).use { outputStream ->
|
plugin.getOutputStream(token, name).use { outputStream ->
|
||||||
outputStream.write(ByteArray(1) { VERSION })
|
outputStream.write(ByteArray(1) { VERSION })
|
||||||
val ad = getADForKV(VERSION, packageName)
|
val ad = getADForKV(VERSION, packageName)
|
||||||
crypto.newEncryptingStream(outputStream, ad).use { encryptedStream ->
|
cryptoService.newEncryptingStream(outputStream, ad).use { encryptedStream ->
|
||||||
GZIPOutputStream(encryptedStream).use { gZipStream ->
|
GZIPOutputStream(encryptedStream).use { gZipStream ->
|
||||||
dbManager.getDbInputStream(packageName).use { inputStream ->
|
dbManager.getDbInputStream(packageName).use { inputStream ->
|
||||||
inputStream.copyTo(gZipStream)
|
inputStream.copyTo(gZipStream)
|
||||||
|
|
@ -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.ContentValues
|
||||||
import android.content.Context
|
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.app.backup.BackupDataOutput
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
|
@ -17,5 +17,4 @@ internal class OutputFactory {
|
||||||
fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
|
fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
|
||||||
return FileOutputStream(outputFileDescriptor.fileDescriptor)
|
return FileOutputStream(outputFileDescriptor.fileDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package com.stevesoltys.seedvault.transport.restore
|
package com.stevesoltys.seedvault.service.app.restore
|
||||||
|
|
||||||
|
import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
|
||||||
|
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
|
||||||
|
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.transport.restore
|
package com.stevesoltys.seedvault.service.app.restore.coordinator
|
||||||
|
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
|
|
@ -13,15 +13,17 @@ import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.metadata.BackupType
|
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
import com.stevesoltys.seedvault.service.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.metadata.BackupType
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
import com.stevesoltys.seedvault.service.metadata.DecryptionFailedException
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.metadata.MetadataReader
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
||||||
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
|
@ -35,26 +37,14 @@ import java.io.IOException
|
||||||
*/
|
*/
|
||||||
internal const val D2D_DEVICE_NAME = "D2D"
|
internal const val D2D_DEVICE_NAME = "D2D"
|
||||||
|
|
||||||
private data class RestoreCoordinatorState(
|
|
||||||
val token: Long,
|
|
||||||
val packages: Iterator<PackageInfo>,
|
|
||||||
/**
|
|
||||||
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
|
|
||||||
*/
|
|
||||||
val autoRestorePackageInfo: PackageInfo?,
|
|
||||||
val backupMetadata: BackupMetadata,
|
|
||||||
) {
|
|
||||||
var currentPackage: String? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val TAG = RestoreCoordinator::class.java.simpleName
|
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class RestoreCoordinator(
|
internal class RestoreCoordinator(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsService: SettingsService,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataService: MetadataService,
|
||||||
private val notificationManager: BackupNotificationManager,
|
private val notificationManager: BackupNotificationManager,
|
||||||
private val plugin: StoragePlugin,
|
private val plugin: StoragePlugin,
|
||||||
private val kv: KVRestore,
|
private val kv: KVRestore,
|
||||||
|
|
@ -126,7 +116,7 @@ internal class RestoreCoordinator(
|
||||||
* or 0 if there is no backup set available corresponding to the current device state.
|
* or 0 if there is no backup set available corresponding to the current device state.
|
||||||
*/
|
*/
|
||||||
fun getCurrentRestoreSet(): Long {
|
fun getCurrentRestoreSet(): Long {
|
||||||
return (settingsManager.getToken() ?: 0L).apply {
|
return (settingsService.getToken() ?: 0L).apply {
|
||||||
Log.i(TAG, "Got current restore set token: $this")
|
Log.i(TAG, "Got current restore set token: $this")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +128,7 @@ internal class RestoreCoordinator(
|
||||||
this.backupMetadata = backupMetadata
|
this.backupMetadata = backupMetadata
|
||||||
|
|
||||||
if (backupMetadata.d2dBackup) {
|
if (backupMetadata.d2dBackup) {
|
||||||
settingsManager.setD2dBackupsEnabled(true)
|
settingsService.setD2dBackupsEnabled(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,9 +157,9 @@ internal class RestoreCoordinator(
|
||||||
// check if the backup is on removable storage that is not plugged in
|
// check if the backup is on removable storage that is not plugged in
|
||||||
if (isStorageRemovableAndNotAvailable()) {
|
if (isStorageRemovableAndNotAvailable()) {
|
||||||
// check if we even have a backup of that app
|
// check if we even have a backup of that app
|
||||||
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
|
if (metadataService.getPackageMetadata(pmPackageName) != null) {
|
||||||
// remind user to plug in storage device
|
// remind user to plug in storage device
|
||||||
val storageName = settingsManager.getStorage()?.name
|
val storageName = settingsService.getStorage()?.name
|
||||||
?: context.getString(R.string.settings_backup_location_none)
|
?: context.getString(R.string.settings_backup_location_none)
|
||||||
notificationManager.onRemovableStorageNotAvailableForRestore(
|
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||||
pmPackageName,
|
pmPackageName,
|
||||||
|
|
@ -231,7 +221,8 @@ internal class RestoreCoordinator(
|
||||||
val type = try {
|
val type = try {
|
||||||
when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
|
when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
|
||||||
BackupType.KV -> {
|
BackupType.KV -> {
|
||||||
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
|
val name =
|
||||||
|
cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
|
||||||
if (plugin.hasData(state.token, name)) {
|
if (plugin.hasData(state.token, name)) {
|
||||||
Log.i(TAG, "Found K/V data for $packageName.")
|
Log.i(TAG, "Found K/V data for $packageName.")
|
||||||
kv.initializeState(
|
kv.initializeState(
|
||||||
|
|
@ -247,7 +238,8 @@ internal class RestoreCoordinator(
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupType.FULL -> {
|
BackupType.FULL -> {
|
||||||
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
|
val name =
|
||||||
|
cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
|
||||||
if (plugin.hasData(state.token, name)) {
|
if (plugin.hasData(state.token, name)) {
|
||||||
Log.i(TAG, "Found full backup data for $packageName.")
|
Log.i(TAG, "Found full backup data for $packageName.")
|
||||||
full.initializeState(version, state.token, name, packageInfo)
|
full.initializeState(version, state.token, name, packageInfo)
|
||||||
|
|
@ -365,7 +357,7 @@ internal class RestoreCoordinator(
|
||||||
|
|
||||||
// TODO this is plugin specific, needs to be factored out when supporting different plugins
|
// TODO this is plugin specific, needs to be factored out when supporting different plugins
|
||||||
private fun isStorageRemovableAndNotAvailable(): Boolean {
|
private fun isStorageRemovableAndNotAvailable(): Boolean {
|
||||||
val storage = settingsManager.getStorage() ?: return false
|
val storage = settingsService.getStorage() ?: return false
|
||||||
return storage.isUnavailableUsb(context)
|
return storage.isUnavailableUsb(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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.NO_MORE_DATA
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
|
|
@ -7,29 +12,20 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.header.HeaderReader
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
|
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.header.getADForFull
|
import com.stevesoltys.seedvault.service.header.HeaderDecodeService
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
|
||||||
|
import com.stevesoltys.seedvault.service.header.getADForFull
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
|
|
||||||
private class FullRestoreState(
|
|
||||||
val version: Byte,
|
|
||||||
val token: Long,
|
|
||||||
val name: String,
|
|
||||||
val packageInfo: PackageInfo,
|
|
||||||
) {
|
|
||||||
var inputStream: InputStream? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val TAG = FullRestore::class.java.simpleName
|
private val TAG = FullRestore::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
|
@ -38,8 +34,8 @@ internal class FullRestore(
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyPlugin: LegacyStoragePlugin,
|
private val legacyPlugin: LegacyStoragePlugin,
|
||||||
private val outputFactory: OutputFactory,
|
private val outputFactory: OutputFactory,
|
||||||
private val headerReader: HeaderReader,
|
private val headerDecodeService: HeaderDecodeService,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var state: FullRestoreState? = null
|
private var state: FullRestoreState? = null
|
||||||
|
|
@ -104,15 +100,15 @@ internal class FullRestore(
|
||||||
if (state.version == 0.toByte()) {
|
if (state.version == 0.toByte()) {
|
||||||
val inputStream =
|
val inputStream =
|
||||||
legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo)
|
legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo)
|
||||||
val version = headerReader.readVersion(inputStream, state.version)
|
val version = headerDecodeService.readVersion(inputStream, state.version)
|
||||||
@Suppress("deprecation")
|
@Suppress("deprecation")
|
||||||
crypto.decryptHeader(inputStream, version, packageName)
|
cryptoService.decryptHeader(inputStream, version, packageName)
|
||||||
state.inputStream = inputStream
|
state.inputStream = inputStream
|
||||||
} else {
|
} else {
|
||||||
val inputStream = plugin.getInputStream(state.token, state.name)
|
val inputStream = plugin.getInputStream(state.token, state.name)
|
||||||
val version = headerReader.readVersion(inputStream, state.version)
|
val version = headerDecodeService.readVersion(inputStream, state.version)
|
||||||
val ad = getADForFull(version, packageName)
|
val ad = getADForFull(version, packageName)
|
||||||
state.inputStream = crypto.newDecryptingStream(inputStream, ad)
|
state.inputStream = cryptoService.newDecryptingStream(inputStream, ad)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, "Error getting input stream for $packageName", e)
|
Log.w(TAG, "Error getting input stream for $packageName", e)
|
||||||
|
|
@ -148,7 +144,7 @@ internal class FullRestore(
|
||||||
// read segment from input stream and decrypt it
|
// read segment from input stream and decrypt it
|
||||||
val decrypted = try {
|
val decrypted = try {
|
||||||
@Suppress("deprecation")
|
@Suppress("deprecation")
|
||||||
crypto.decryptSegment(inputStream)
|
cryptoService.decryptSegment(inputStream)
|
||||||
} catch (e: EOFException) {
|
} catch (e: EOFException) {
|
||||||
Log.i(TAG, " EOF")
|
Log.i(TAG, " EOF")
|
||||||
// close input stream here as we won't need it anymore
|
// close input stream here as we won't need it anymore
|
||||||
|
|
@ -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.BackupDataOutput
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
|
|
@ -9,34 +14,23 @@ import android.util.Log
|
||||||
import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY
|
import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY
|
||||||
import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY
|
import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
|
||||||
import com.stevesoltys.seedvault.decodeBase64
|
import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
|
||||||
import com.stevesoltys.seedvault.header.HeaderReader
|
import com.stevesoltys.seedvault.service.app.restore.OutputFactory
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.service.header.HeaderDecodeService
|
||||||
import com.stevesoltys.seedvault.header.getADForKV
|
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.service.header.VERSION
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.header.getADForKV
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVDb
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.util.decodeBase64
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import javax.crypto.AEADBadTagException
|
import javax.crypto.AEADBadTagException
|
||||||
|
|
||||||
private class KVRestoreState(
|
|
||||||
val version: Byte,
|
|
||||||
val token: Long,
|
|
||||||
val name: String,
|
|
||||||
val packageInfo: PackageInfo,
|
|
||||||
/**
|
|
||||||
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
|
|
||||||
*/
|
|
||||||
val autoRestorePackageInfo: PackageInfo?,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val TAG = KVRestore::class.java.simpleName
|
private val TAG = KVRestore::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
|
@ -45,8 +39,8 @@ internal class KVRestore(
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyPlugin: LegacyStoragePlugin,
|
private val legacyPlugin: LegacyStoragePlugin,
|
||||||
private val outputFactory: OutputFactory,
|
private val outputFactory: OutputFactory,
|
||||||
private val headerReader: HeaderReader,
|
private val headerDecodeService: HeaderDecodeService,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val dbManager: KvDbManager,
|
private val dbManager: KvDbManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -153,9 +147,9 @@ internal class KVRestore(
|
||||||
private suspend fun downloadRestoreDb(state: KVRestoreState): KVDb {
|
private suspend fun downloadRestoreDb(state: KVRestoreState): KVDb {
|
||||||
val packageName = state.packageInfo.packageName
|
val packageName = state.packageInfo.packageName
|
||||||
plugin.getInputStream(state.token, state.name).use { inputStream ->
|
plugin.getInputStream(state.token, state.name).use { inputStream ->
|
||||||
headerReader.readVersion(inputStream, state.version)
|
headerDecodeService.readVersion(inputStream, state.version)
|
||||||
val ad = getADForKV(VERSION, packageName)
|
val ad = getADForKV(VERSION, packageName)
|
||||||
crypto.newDecryptingStream(inputStream, ad).use { decryptedStream ->
|
cryptoService.newDecryptingStream(inputStream, ad).use { decryptedStream ->
|
||||||
GZIPInputStream(decryptedStream).use { gzipStream ->
|
GZIPInputStream(decryptedStream).use { gzipStream ->
|
||||||
dbManager.getDbOutputStream(packageName).use { outputStream ->
|
dbManager.getDbOutputStream(packageName).use { outputStream ->
|
||||||
gzipStream.copyTo(outputStream)
|
gzipStream.copyTo(outputStream)
|
||||||
|
|
@ -241,10 +235,10 @@ internal class KVRestore(
|
||||||
out: BackupDataOutput,
|
out: BackupDataOutput,
|
||||||
) = legacyPlugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
|
) = legacyPlugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
|
||||||
.use { inputStream ->
|
.use { inputStream ->
|
||||||
val version = headerReader.readVersion(inputStream, state.version)
|
val version = headerDecodeService.readVersion(inputStream, state.version)
|
||||||
val packageName = state.packageInfo.packageName
|
val packageName = state.packageInfo.packageName
|
||||||
crypto.decryptHeader(inputStream, version, packageName, dKey.key)
|
cryptoService.decryptHeader(inputStream, version, packageName, dKey.key)
|
||||||
val value = crypto.decryptMultipleSegments(inputStream)
|
val value = cryptoService.decryptMultipleSegments(inputStream)
|
||||||
val size = value.size
|
val size = value.size
|
||||||
Log.v(TAG, " ... key=${dKey.key} size=$size")
|
Log.v(TAG, " ... key=${dKey.key} size=$size")
|
||||||
|
|
||||||
|
|
@ -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 java.security.Key
|
||||||
import javax.crypto.Cipher
|
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 org.koin.dsl.module
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
|
|
@ -15,5 +15,5 @@ val cryptoModule = module {
|
||||||
}
|
}
|
||||||
KeyManagerImpl(keyStore)
|
KeyManagerImpl(keyStore)
|
||||||
}
|
}
|
||||||
single<Crypto> { CryptoImpl(get(), get(), get()) }
|
single<CryptoService> { CryptoServiceImpl(get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
@ -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.service.header.HeaderDecodeService
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
|
||||||
import com.stevesoltys.seedvault.header.HeaderReader
|
import com.stevesoltys.seedvault.service.header.MAX_VERSION_HEADER_SIZE
|
||||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
|
import com.stevesoltys.seedvault.service.header.VersionHeader
|
||||||
import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
|
import com.stevesoltys.seedvault.util.encodeBase64
|
||||||
import com.stevesoltys.seedvault.header.SegmentHeader
|
|
||||||
import com.stevesoltys.seedvault.header.VersionHeader
|
|
||||||
import org.calyxos.backup.storage.crypto.StreamCrypto
|
import org.calyxos.backup.storage.crypto.StreamCrypto
|
||||||
import org.calyxos.backup.storage.crypto.StreamCrypto.deriveStreamKey
|
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
@ -19,111 +16,14 @@ import java.security.NoSuchAlgorithmException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
/**
|
internal class CryptoServiceImpl(
|
||||||
* A version 1 backup stream uses [AesGcmHkdfStreaming] from the tink library.
|
|
||||||
*
|
|
||||||
* A version 0 backup stream starts with a version byte followed by an encrypted [VersionHeader].
|
|
||||||
*
|
|
||||||
* The header will be encrypted with AES/GCM to provide authentication.
|
|
||||||
* It can be read using [decryptHeader] which throws a [SecurityException],
|
|
||||||
* if the expected version and package name do not match the encrypted header.
|
|
||||||
*
|
|
||||||
* After the header, follows one or more data segments.
|
|
||||||
* Each segment begins with a clear-text [SegmentHeader]
|
|
||||||
* that contains the length of the segment
|
|
||||||
* and a nonce acting as the initialization vector for the encryption.
|
|
||||||
* The segment can be read using [decryptSegment] which throws a [SecurityException],
|
|
||||||
* if the length of the segment is specified larger than allowed.
|
|
||||||
*/
|
|
||||||
internal interface Crypto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a ByteArray with bytes retrieved from [SecureRandom].
|
|
||||||
*/
|
|
||||||
fun getRandomBytes(size: Int): ByteArray
|
|
||||||
|
|
||||||
fun getNameForPackage(salt: String, packageName: String): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name that identifies an APK in the backup storage plugin.
|
|
||||||
* @param suffix empty string for normal APKs and the name of the split in case of an APK split
|
|
||||||
*/
|
|
||||||
fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a [AesGcmHkdfStreaming] encrypting stream
|
|
||||||
* that gets encrypted and authenticated the given associated data.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class, GeneralSecurityException::class)
|
|
||||||
fun newEncryptingStream(
|
|
||||||
outputStream: OutputStream,
|
|
||||||
associatedData: ByteArray,
|
|
||||||
): OutputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a [AesGcmHkdfStreaming] decrypting stream
|
|
||||||
* that gets decrypted and authenticated the given associated data.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class, GeneralSecurityException::class)
|
|
||||||
fun newDecryptingStream(
|
|
||||||
inputStream: InputStream,
|
|
||||||
associatedData: ByteArray,
|
|
||||||
): InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads and decrypts a [VersionHeader] from the given [InputStream]
|
|
||||||
* and ensures that the expected version, package name and key match
|
|
||||||
* what is found in the header.
|
|
||||||
* If a mismatch is found, a [SecurityException] is thrown.
|
|
||||||
*
|
|
||||||
* @return The read [VersionHeader] present in the beginning of the given [InputStream].
|
|
||||||
*/
|
|
||||||
@Suppress("Deprecation")
|
|
||||||
@Deprecated("Use newDecryptingStream instead")
|
|
||||||
@Throws(IOException::class, SecurityException::class)
|
|
||||||
fun decryptHeader(
|
|
||||||
inputStream: InputStream,
|
|
||||||
expectedVersion: Byte,
|
|
||||||
expectedPackageName: String,
|
|
||||||
expectedKey: String? = null,
|
|
||||||
): VersionHeader
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads and decrypts a segment from the given [InputStream].
|
|
||||||
*
|
|
||||||
* @return The decrypted segment payload.
|
|
||||||
*/
|
|
||||||
@Deprecated("Use newDecryptingStream instead")
|
|
||||||
@Throws(EOFException::class, IOException::class, SecurityException::class)
|
|
||||||
fun decryptSegment(inputStream: InputStream): ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like [decryptSegment], but decrypts multiple segments and does not throw [EOFException].
|
|
||||||
*/
|
|
||||||
@Deprecated("Use newDecryptingStream instead")
|
|
||||||
@Throws(IOException::class, SecurityException::class)
|
|
||||||
fun decryptMultipleSegments(inputStream: InputStream): ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that the stored backup key was created from the given seed.
|
|
||||||
*
|
|
||||||
* @return true if the key was created from given seed, false otherwise.
|
|
||||||
*/
|
|
||||||
fun verifyBackupKey(seed: ByteArray): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
internal const val TYPE_METADATA: Byte = 0x00
|
|
||||||
internal const val TYPE_BACKUP_KV: Byte = 0x01
|
|
||||||
internal const val TYPE_BACKUP_FULL: Byte = 0x02
|
|
||||||
|
|
||||||
internal class CryptoImpl(
|
|
||||||
private val keyManager: KeyManager,
|
private val keyManager: KeyManager,
|
||||||
private val cipherFactory: CipherFactory,
|
private val cipherFactory: CipherFactory,
|
||||||
private val headerReader: HeaderReader,
|
private val headerDecodeService: HeaderDecodeService,
|
||||||
) : Crypto {
|
) : CryptoService {
|
||||||
|
|
||||||
private val key: ByteArray by lazy {
|
private val key: ByteArray by lazy {
|
||||||
deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray())
|
StreamCrypto.deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray())
|
||||||
}
|
}
|
||||||
private val secureRandom: SecureRandom by lazy { SecureRandom() }
|
private val secureRandom: SecureRandom by lazy { SecureRandom() }
|
||||||
|
|
||||||
|
|
@ -175,7 +75,7 @@ internal class CryptoImpl(
|
||||||
expectedKey: String?,
|
expectedKey: String?,
|
||||||
): VersionHeader {
|
): VersionHeader {
|
||||||
val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
|
val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
|
||||||
val header = headerReader.getVersionHeader(decrypted)
|
val header = headerDecodeService.getVersionHeader(decrypted)
|
||||||
|
|
||||||
if (header.version != expectedVersion) {
|
if (header.version != expectedVersion) {
|
||||||
throw SecurityException(
|
throw SecurityException(
|
||||||
|
|
@ -219,7 +119,7 @@ internal class CryptoImpl(
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
@Throws(EOFException::class, IOException::class, SecurityException::class)
|
@Throws(EOFException::class, IOException::class, SecurityException::class)
|
||||||
private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
|
private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
|
||||||
val segmentHeader = headerReader.readSegmentHeader(inputStream)
|
val segmentHeader = headerDecodeService.readSegmentHeader(inputStream)
|
||||||
if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException(
|
if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException(
|
||||||
"Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength"
|
"Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength"
|
||||||
)
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.crypto
|
package com.stevesoltys.seedvault.service.crypto
|
||||||
|
|
||||||
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
||||||
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
||||||
|
|
@ -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 android.content.Context
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
import com.stevesoltys.seedvault.service.crypto.KeyManager
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
|
||||||
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
internal class SeedvaultStoragePlugin(
|
/**
|
||||||
|
* [SafStoragePlugin] for backing up files.
|
||||||
|
*/
|
||||||
|
internal class FileBackupStoragePlugin(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
private val storage: DocumentsStorage,
|
private val storage: DocumentsStorage,
|
||||||
private val keyManager: KeyManager,
|
private val keyManager: KeyManager,
|
||||||
|
|
@ -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.service.crypto.GCM_AUTHENTICATION_TAG_LENGTH
|
||||||
import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_FULL
|
import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_FULL
|
||||||
import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_KV
|
import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_KV
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
internal const val VERSION: Byte = 1
|
internal const val VERSION: Byte = 1
|
||||||
|
|
@ -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.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
|
|
||||||
internal interface HeaderReader {
|
internal class HeaderDecodeServiceImpl : HeaderDecodeService {
|
||||||
@Throws(IOException::class, UnsupportedVersionException::class)
|
|
||||||
fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte
|
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
|
||||||
@Deprecated("For restoring v0 backups only")
|
|
||||||
@Throws(SecurityException::class)
|
|
||||||
fun getVersionHeader(byteArray: ByteArray): VersionHeader
|
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
|
||||||
@Deprecated("For restoring v0 backups only")
|
|
||||||
@Throws(EOFException::class, IOException::class)
|
|
||||||
fun readSegmentHeader(inputStream: InputStream): SegmentHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class HeaderReaderImpl : HeaderReader {
|
|
||||||
|
|
||||||
@Throws(IOException::class, UnsupportedVersionException::class, GeneralSecurityException::class)
|
@Throws(IOException::class, UnsupportedVersionException::class, GeneralSecurityException::class)
|
||||||
override fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte {
|
override fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte {
|
||||||
|
|
@ -85,5 +70,3 @@ internal class HeaderReaderImpl : HeaderReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnsupportedVersionException(val version: Byte) : IOException()
|
|
||||||
|
|
@ -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.content.pm.ApplicationInfo.FLAG_STOPPED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.stevesoltys.seedvault.crypto.TYPE_METADATA
|
import com.stevesoltys.seedvault.service.crypto.TYPE_METADATA
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.service.header.VERSION
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
|
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package com.stevesoltys.seedvault.metadata
|
package com.stevesoltys.seedvault.service.metadata
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val metadataModule = module {
|
val metadataModule = module {
|
||||||
single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) }
|
single { MetadataService(androidContext(), get(), get(), get(), get(), get()) }
|
||||||
single<MetadataWriter> { MetadataWriterImpl(get()) }
|
single<MetadataWriter> { MetadataWriterImpl(get()) }
|
||||||
single<MetadataReader> { MetadataReaderImpl(get()) }
|
single<MetadataReader> { MetadataReaderImpl(get()) }
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package com.stevesoltys.seedvault.metadata
|
package com.stevesoltys.seedvault.service.metadata
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.Utf8
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.service.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.util.Utf8
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
@ -36,7 +36,7 @@ interface MetadataReader {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
internal class MetadataReaderImpl(private val cryptoService: CryptoService) : MetadataReader {
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
SecurityException::class,
|
SecurityException::class,
|
||||||
|
|
@ -51,7 +51,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken)
|
if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken)
|
||||||
|
|
||||||
val metadataBytes = try {
|
val metadataBytes = try {
|
||||||
crypto.newDecryptingStream(inputStream, getAD(version, expectedToken)).readBytes()
|
cryptoService.newDecryptingStream(inputStream, getAD(version, expectedToken))
|
||||||
|
.readBytes()
|
||||||
} catch (e: GeneralSecurityException) {
|
} catch (e: GeneralSecurityException) {
|
||||||
throw DecryptionFailedException(e)
|
throw DecryptionFailedException(e)
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +68,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private fun readMetadataV0(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
private fun readMetadataV0(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
||||||
val metadataBytes = try {
|
val metadataBytes = try {
|
||||||
crypto.decryptMultipleSegments(inputStream)
|
cryptoService.decryptMultipleSegments(inputStream)
|
||||||
} catch (e: AEADBadTagException) {
|
} catch (e: AEADBadTagException) {
|
||||||
throw DecryptionFailedException(e)
|
throw DecryptionFailedException(e)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.metadata
|
package com.stevesoltys.seedvault.service.metadata
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
|
@ -9,34 +9,34 @@ import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
import com.stevesoltys.seedvault.Clock
|
import com.stevesoltys.seedvault.service.app.isSystemApp
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
import com.stevesoltys.seedvault.service.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.util.TimeSource
|
||||||
import com.stevesoltys.seedvault.transport.backup.isSystemApp
|
import com.stevesoltys.seedvault.util.encodeBase64
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
private val TAG = MetadataManager::class.java.simpleName
|
private val TAG = MetadataService::class.java.simpleName
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal const val METADATA_CACHE_FILE = "metadata.cache"
|
internal const val METADATA_CACHE_FILE = "metadata.cache"
|
||||||
internal const val METADATA_SALT_SIZE = 32
|
internal const val METADATA_SALT_SIZE = 32
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
internal class MetadataManager(
|
internal class MetadataService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val clock: Clock,
|
private val timeSource: TimeSource,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val metadataWriter: MetadataWriter,
|
private val metadataWriter: MetadataWriter,
|
||||||
private val metadataReader: MetadataReader,
|
private val metadataReader: MetadataReader,
|
||||||
private val settingsManager: SettingsManager
|
private val settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
|
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
|
||||||
|
|
@ -63,7 +63,7 @@ internal class MetadataManager(
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
|
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
|
||||||
val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
|
val salt = cryptoService.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
|
||||||
modifyMetadata(metadataOutputStream) {
|
modifyMetadata(metadataOutputStream) {
|
||||||
metadata = BackupMetadata(token = token, salt = salt)
|
metadata = BackupMetadata(token = token, salt = salt)
|
||||||
}
|
}
|
||||||
|
|
@ -135,9 +135,9 @@ internal class MetadataManager(
|
||||||
) {
|
) {
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
modifyMetadata(metadataOutputStream) {
|
modifyMetadata(metadataOutputStream) {
|
||||||
val now = clock.time()
|
val now = timeSource.time()
|
||||||
metadata.time = now
|
metadata.time = now
|
||||||
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
|
metadata.d2dBackup = settingsService.d2dBackupsEnabled()
|
||||||
|
|
||||||
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
||||||
metadata.packageMetadataMap[packageName]!!.time = now
|
metadata.packageMetadataMap[packageName]!!.time = now
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.stevesoltys.seedvault.metadata
|
package com.stevesoltys.seedvault.service.metadata
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.Utf8
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.util.Utf8
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
@ -15,12 +15,13 @@ interface MetadataWriter {
|
||||||
fun encode(metadata: BackupMetadata): ByteArray
|
fun encode(metadata: BackupMetadata): ByteArray
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
|
internal class MetadataWriterImpl(private val cryptoService: CryptoService) : MetadataWriter {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
|
override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
|
||||||
outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
|
outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
|
||||||
crypto.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token)).use {
|
cryptoService.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token))
|
||||||
|
.use {
|
||||||
it.write(encode(metadata))
|
it.write(encode(metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.content.Context
|
||||||
import android.hardware.usb.UsbDevice
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
|
||||||
|
import com.stevesoltys.seedvault.ui.settings.AppStatus
|
||||||
import java.util.concurrent.ConcurrentSkipListSet
|
import java.util.concurrent.ConcurrentSkipListSet
|
||||||
|
|
||||||
internal const val PREF_KEY_TOKEN = "token"
|
internal const val PREF_KEY_TOKEN = "token"
|
||||||
|
|
@ -34,7 +31,9 @@ private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
||||||
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
||||||
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
|
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
|
||||||
|
|
||||||
class SettingsManager(private val context: Context) {
|
class SettingsService(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
private val prefs = permitDiskReads {
|
private val prefs = permitDiskReads {
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
@ -59,7 +58,7 @@ class SettingsManager(private val context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a new RestoreSet token.
|
* Sets a new RestoreSet token.
|
||||||
* Should only be called by the [BackupCoordinator]
|
* Should only be called by the [BackupCoordinatorService]
|
||||||
* to ensure that related work is performed after moving to a new token.
|
* to ensure that related work is performed after moving to a new token.
|
||||||
*/
|
*/
|
||||||
fun setNewToken(newToken: Long?) {
|
fun setNewToken(newToken: Long?) {
|
||||||
|
|
@ -162,54 +161,3 @@ class SettingsManager(private val context: Context) {
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Storage(
|
|
||||||
val uri: Uri,
|
|
||||||
val name: String,
|
|
||||||
val isUsb: Boolean,
|
|
||||||
val requiresNetwork: Boolean,
|
|
||||||
) {
|
|
||||||
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
|
|
||||||
?: throw AssertionError("Should only happen on API < 21.")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this is USB storage that is not available, false otherwise.
|
|
||||||
*
|
|
||||||
* Must be run off UI thread (ideally I/O).
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun isUnavailableUsb(context: Context): Boolean {
|
|
||||||
return isUsb && !getDocumentFile(context).isDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this is storage that requires network access,
|
|
||||||
* but it isn't available right now.
|
|
||||||
*/
|
|
||||||
fun isUnavailableNetwork(context: Context): Boolean {
|
|
||||||
return requiresNetwork && !hasUnmeteredInternet(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasUnmeteredInternet(context: Context): Boolean {
|
|
||||||
val cm = context.getSystemService(ConnectivityManager::class.java)
|
|
||||||
val isMetered = cm.isActiveNetworkMetered
|
|
||||||
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FlashDrive(
|
|
||||||
val name: String,
|
|
||||||
val serialNumber: String?,
|
|
||||||
val vendorId: Int,
|
|
||||||
val productId: Int,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun from(device: UsbDevice) = FlashDrive(
|
|
||||||
name = "${device.manufacturerName} ${device.productName}",
|
|
||||||
serialNumber = device.serialNumber,
|
|
||||||
vendorId = device.vendorId,
|
|
||||||
productId = device.productId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 android.app.backup.RestoreSet
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.service.settings.Storage
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
@ -61,7 +61,7 @@ interface StoragePlugin {
|
||||||
* @return metadata for the set of restore images available,
|
* @return metadata for the set of restore images available,
|
||||||
* or null if an error occurred (the attempt should be rescheduled).
|
* or null if an error occurred (the attempt should be rescheduled).
|
||||||
**/
|
**/
|
||||||
suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>?
|
suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the package name of the app that provides the backend storage
|
* Returns the package name of the app that provides the backend storage
|
||||||
|
|
@ -74,5 +74,3 @@ interface StoragePlugin {
|
||||||
val providerPackageName: String?
|
val providerPackageName: String?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedMetadata(val token: Long, val inputStreamRetriever: () -> InputStream)
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.service.storage.saf
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.service.storage.saf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
import com.stevesoltys.seedvault.service.storage.EncryptedBackupMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.service.settings.Storage
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
@ -90,14 +90,14 @@ internal class DocumentsProviderStoragePlugin(
|
||||||
return backupSets.isNotEmpty()
|
return backupSets.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
|
||||||
val rootDir = storage.rootBackupDir ?: return null
|
val rootDir = storage.rootBackupDir ?: return null
|
||||||
val backupSets = getBackups(context, rootDir)
|
val backupSets = getBackups(context, rootDir)
|
||||||
val iterator = backupSets.iterator()
|
val iterator = backupSets.iterator()
|
||||||
return generateSequence {
|
return generateSequence {
|
||||||
if (!iterator.hasNext()) return@generateSequence null // end sequence
|
if (!iterator.hasNext()) return@generateSequence null // end sequence
|
||||||
val backupSet = iterator.next()
|
val backupSet = iterator.next()
|
||||||
EncryptedMetadata(backupSet.token) {
|
EncryptedBackupMetadata(backupSet.token) {
|
||||||
storage.getInputStream(backupSet.metadataFile)
|
storage.getInputStream(backupSet.metadataFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
@file:Suppress("BlockingMethodInNonBlockingContext")
|
@file:Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.service.storage.saf
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -17,8 +17,8 @@ import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.service.settings.Storage
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
@ -43,11 +43,11 @@ private val TAG = DocumentsStorage::class.java.simpleName
|
||||||
|
|
||||||
internal class DocumentsStorage(
|
internal class DocumentsStorage(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsService: SettingsService,
|
||||||
) {
|
) {
|
||||||
internal var storage: Storage? = null
|
internal var storage: Storage? = null
|
||||||
get() {
|
get() {
|
||||||
if (field == null) field = settingsManager.getStorage()
|
if (field == null) field = settingsService.getStorage()
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ internal class DocumentsStorage(
|
||||||
|
|
||||||
private var currentToken: Long? = null
|
private var currentToken: Long? = null
|
||||||
get() {
|
get() {
|
||||||
if (field == null) field = settingsManager.getToken()
|
if (field == null) field = settingsService.getToken()
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.service.storage.saf.legacy
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.assertRightFile
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.findFileBlocking
|
||||||
|
import com.stevesoltys.seedvault.service.storage.saf.listFilesBlocking
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
|
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
|
||||||
|
@Deprecated("Only for old v0 backup format")
|
||||||
internal class DocumentsProviderLegacyPlugin(
|
internal class DocumentsProviderLegacyPlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storage: DocumentsStorage,
|
private val storage: DocumentsStorage,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.plugins
|
package com.stevesoltys.seedvault.service.storage.saf.legacy
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
@ -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.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
import com.stevesoltys.seedvault.ui.settings.SettingsActivity
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
@ -36,9 +36,9 @@ private val TAG = ConfigurableBackupTransport::class.java.simpleName
|
||||||
class ConfigurableBackupTransport internal constructor(private val context: Context) :
|
class ConfigurableBackupTransport internal constructor(private val context: Context) :
|
||||||
BackupTransport(), KoinComponent {
|
BackupTransport(), KoinComponent {
|
||||||
|
|
||||||
private val backupCoordinator by inject<BackupCoordinator>()
|
private val backupCoordinatorService by inject<BackupCoordinatorService>()
|
||||||
private val restoreCoordinator by inject<RestoreCoordinator>()
|
private val restoreCoordinator by inject<RestoreCoordinator>()
|
||||||
private val settingsManager by inject<SettingsManager>()
|
private val settingsService by inject<SettingsService>()
|
||||||
|
|
||||||
override fun transportDirName(): String {
|
override fun transportDirName(): String {
|
||||||
return TRANSPORT_DIRECTORY_NAME
|
return TRANSPORT_DIRECTORY_NAME
|
||||||
|
|
@ -58,7 +58,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
* This allows the agent to decide what to do based on properties of the transport.
|
* This allows the agent to decide what to do based on properties of the transport.
|
||||||
*/
|
*/
|
||||||
override fun getTransportFlags(): Int {
|
override fun getTransportFlags(): Int {
|
||||||
return if (settingsManager.d2dBackupsEnabled()) {
|
return if (settingsService.d2dBackupsEnabled()) {
|
||||||
D2D_TRANSPORT_FLAGS
|
D2D_TRANSPORT_FLAGS
|
||||||
} else {
|
} else {
|
||||||
DEFAULT_TRANSPORT_FLAGS
|
DEFAULT_TRANSPORT_FLAGS
|
||||||
|
|
@ -120,26 +120,26 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
//
|
//
|
||||||
|
|
||||||
override fun initializeDevice(): Int = runBlocking {
|
override fun initializeDevice(): Int = runBlocking {
|
||||||
backupCoordinator.initializeDevice()
|
backupCoordinatorService.initializeDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isAppEligibleForBackup(
|
override fun isAppEligibleForBackup(
|
||||||
targetPackage: PackageInfo,
|
targetPackage: PackageInfo,
|
||||||
isFullBackup: Boolean,
|
isFullBackup: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
|
return backupCoordinatorService.isAppEligibleForBackup(targetPackage, isFullBackup)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
|
override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
|
||||||
backupCoordinator.getBackupQuota(packageName, isFullBackup)
|
backupCoordinatorService.getBackupQuota(packageName, isFullBackup)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
|
override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
|
||||||
backupCoordinator.clearBackupData(packageInfo)
|
backupCoordinatorService.clearBackupData(packageInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishBackup(): Int = runBlocking {
|
override fun finishBackup(): Int = runBlocking {
|
||||||
backupCoordinator.finishBackup()
|
backupCoordinatorService.finishBackup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
|
|
@ -147,7 +147,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
//
|
//
|
||||||
|
|
||||||
override fun requestBackupTime(): Long {
|
override fun requestBackupTime(): Long {
|
||||||
return backupCoordinator.requestBackupTime()
|
return backupCoordinatorService.requestBackupTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performBackup(
|
override fun performBackup(
|
||||||
|
|
@ -155,7 +155,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
inFd: ParcelFileDescriptor,
|
inFd: ParcelFileDescriptor,
|
||||||
flags: Int,
|
flags: Int,
|
||||||
): Int = runBlocking {
|
): Int = runBlocking {
|
||||||
backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags)
|
backupCoordinatorService.performIncrementalBackup(packageInfo, inFd, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performBackup(
|
override fun performBackup(
|
||||||
|
|
@ -171,11 +171,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
//
|
//
|
||||||
|
|
||||||
override fun requestFullBackupTime(): Long {
|
override fun requestFullBackupTime(): Long {
|
||||||
return backupCoordinator.requestFullBackupTime()
|
return backupCoordinatorService.requestFullBackupTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun checkFullBackupSize(size: Long): Int {
|
override fun checkFullBackupSize(size: Long): Int {
|
||||||
return backupCoordinator.checkFullBackupSize(size)
|
return backupCoordinatorService.checkFullBackupSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performFullBackup(
|
override fun performFullBackup(
|
||||||
|
|
@ -183,7 +183,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
socket: ParcelFileDescriptor,
|
socket: ParcelFileDescriptor,
|
||||||
flags: Int,
|
flags: Int,
|
||||||
): Int = runBlocking {
|
): Int = runBlocking {
|
||||||
backupCoordinator.performFullBackup(targetPackage, socket, flags)
|
backupCoordinatorService.performFullBackup(targetPackage, socket, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performFullBackup(
|
override fun performFullBackup(
|
||||||
|
|
@ -191,15 +191,15 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
fileDescriptor: ParcelFileDescriptor,
|
fileDescriptor: ParcelFileDescriptor,
|
||||||
): Int = runBlocking {
|
): Int = runBlocking {
|
||||||
Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
|
Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
|
||||||
backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0)
|
backupCoordinatorService.performFullBackup(targetPackage, fileDescriptor, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendBackupData(numBytes: Int): Int = runBlocking {
|
override fun sendBackupData(numBytes: Int): Int = runBlocking {
|
||||||
backupCoordinator.sendBackupData(numBytes)
|
backupCoordinatorService.sendBackupData(numBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelFullBackup() = runBlocking {
|
override fun cancelFullBackup() = runBlocking {
|
||||||
backupCoordinator.cancelFullBackup()
|
backupCoordinatorService.cancelFullBackup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,14 @@
|
||||||
package com.stevesoltys.seedvault.transport
|
package com.stevesoltys.seedvault.transport
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.app.backup.BackupManager
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.RemoteException
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import com.stevesoltys.seedvault.service.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.BackupMonitor
|
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.koin.core.context.GlobalContext.get
|
|
||||||
|
|
||||||
private val TAG = ConfigurableBackupTransportService::class.java.simpleName
|
private val TAG = ConfigurableBackupTransportService::class.java.simpleName
|
||||||
|
|
||||||
|
|
@ -59,30 +51,3 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
fun requestBackup(context: Context) {
|
|
||||||
val backupManager: IBackupManager = get().get()
|
|
||||||
if (backupManager.isBackupEnabled) {
|
|
||||||
val packageService: PackageService = get().get()
|
|
||||||
val packages = packageService.eligiblePackages
|
|
||||||
val appTotals = packageService.expectedAppTotals
|
|
||||||
|
|
||||||
val result = try {
|
|
||||||
Log.d(TAG, "Backup is enabled, request backup...")
|
|
||||||
val observer = NotificationBackupObserver(context, packages.size, appTotals)
|
|
||||||
backupManager.requestBackup(packages, observer, BackupMonitor(), 0)
|
|
||||||
} catch (e: RemoteException) {
|
|
||||||
Log.e(TAG, "Error during backup: ", e)
|
|
||||||
val nm: BackupNotificationManager = get().get()
|
|
||||||
nm.onBackupError()
|
|
||||||
}
|
|
||||||
if (result == BackupManager.SUCCESS) {
|
|
||||||
Log.i(TAG, "Backup succeeded ")
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Backup failed: $result")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Backup is not enabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 androidx.fragment.app.Fragment
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
|
||||||
abstract class BackupActivity : AppCompatActivity() {
|
abstract class BackupActivityBase : AppCompatActivity() {
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
|
|
@ -5,7 +5,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
import com.stevesoltys.seedvault.ui.settings.SettingsViewModel
|
||||||
import org.calyxos.backup.storage.ui.backup.BackupContentFragment
|
import org.calyxos.backup.storage.ui.backup.BackupContentFragment
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.ui
|
package com.stevesoltys.seedvault.ui.liveevent
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent.ConsumableEvent
|
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent.ConsumableEvent
|
||||||
|
|
||||||
open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() {
|
open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() {
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.ui;
|
package com.stevesoltys.seedvault.ui.liveevent;
|
||||||
|
|
||||||
public interface LiveEventHandler<T> {
|
public interface LiveEventHandler<T> {
|
||||||
void onEvent(T t);
|
void onEvent(T t);
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.ui
|
package com.stevesoltys.seedvault.ui.liveevent
|
||||||
|
|
||||||
class MutableLiveEvent<T> : LiveEvent<T>() {
|
class MutableLiveEvent<T> : LiveEvent<T>() {
|
||||||
|
|
||||||
|
|
@ -20,12 +20,12 @@ import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
|
||||||
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
|
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
|
||||||
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
|
import com.stevesoltys.seedvault.ui.restore.ACTION_RESTORE_ERROR_UNINSTALL
|
||||||
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
|
import com.stevesoltys.seedvault.ui.restore.EXTRA_PACKAGE_NAME
|
||||||
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
|
import com.stevesoltys.seedvault.ui.restore.REQUEST_CODE_UNINSTALL
|
||||||
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
|
import com.stevesoltys.seedvault.ui.settings.ACTION_APP_STATUS_LIST
|
||||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
import com.stevesoltys.seedvault.ui.settings.SettingsActivity
|
||||||
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
|
import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
|
||||||
|
|
||||||
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
||||||
private const val CHANNEL_ID_ERROR = "NotificationError"
|
private const val CHANNEL_ID_ERROR = "NotificationError"
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import android.util.Log.INFO
|
||||||
import android.util.Log.isLoggable
|
import android.util.Log.isLoggable
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
|
||||||
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
|
import com.stevesoltys.seedvault.service.metadata.MetadataService
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ internal class NotificationBackupObserver(
|
||||||
) : IBackupObserver.Stub(), KoinComponent {
|
) : IBackupObserver.Stub(), KoinComponent {
|
||||||
|
|
||||||
private val nm: BackupNotificationManager by inject()
|
private val nm: BackupNotificationManager by inject()
|
||||||
private val metadataManager: MetadataManager by inject()
|
private val metadataService: MetadataService by inject()
|
||||||
private var currentPackage: String? = null
|
private var currentPackage: String? = null
|
||||||
private var numPackages: Int = 0
|
private var numPackages: Int = 0
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ internal class NotificationBackupObserver(
|
||||||
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
|
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
|
||||||
}
|
}
|
||||||
val success = status == 0
|
val success = status == 0
|
||||||
val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null
|
val numBackedUp = if (success) metadataService.getPackagesNumBackedUp() else null
|
||||||
nm.onBackupFinished(success, numBackedUp)
|
nm.onBackupFinished(success, numBackedUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
package com.stevesoltys.seedvault.ui
|
package com.stevesoltys.seedvault.ui.provision
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
|
import com.stevesoltys.seedvault.ui.BackupActivityBase
|
||||||
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeActivity
|
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeActivity
|
||||||
import com.stevesoltys.seedvault.ui.storage.StorageActivity
|
import com.stevesoltys.seedvault.ui.storage.StorageActivity
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ private val TAG = RequireProvisioningActivity::class.java.name
|
||||||
* An Activity that requires the recovery code and the backup location to be set up
|
* An Activity that requires the recovery code and the backup location to be set up
|
||||||
* before starting.
|
* before starting.
|
||||||
*/
|
*/
|
||||||
abstract class RequireProvisioningActivity : BackupActivity() {
|
abstract class RequireProvisioningActivity : BackupActivityBase() {
|
||||||
|
|
||||||
private val recoveryCodeRequest =
|
private val recoveryCodeRequest =
|
||||||
registerForActivityResult(StartActivityForResult()) { result ->
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
package com.stevesoltys.seedvault.ui
|
package com.stevesoltys.seedvault.ui.provision
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.service.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
|
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
|
||||||
|
import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
||||||
|
|
||||||
abstract class RequireProvisioningViewModel(
|
abstract class RequireProvisioningViewModel(
|
||||||
protected val app: Application,
|
protected val app: Application,
|
||||||
protected val settingsManager: SettingsManager,
|
protected val settingsService: SettingsService,
|
||||||
protected val keyManager: KeyManager,
|
protected val keyManager: KeyManager,
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
|
||||||
|
|
@ -18,7 +20,7 @@ abstract class RequireProvisioningViewModel(
|
||||||
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
||||||
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
||||||
|
|
||||||
internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsManager)
|
internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsService)
|
||||||
|
|
||||||
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()
|
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()
|
||||||
|
|
||||||
|
|
@ -5,11 +5,11 @@ import android.view.MenuItem
|
||||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.isDebugBuild
|
import com.stevesoltys.seedvault.isDebugBuild
|
||||||
import com.stevesoltys.seedvault.ui.BackupActivity
|
import com.stevesoltys.seedvault.ui.BackupActivityBase
|
||||||
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
|
import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_RESTORE
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class RecoveryCodeActivity : BackupActivity() {
|
class RecoveryCodeActivity : BackupActivityBase() {
|
||||||
|
|
||||||
private val viewModel: RecoveryCodeViewModel by viewModel()
|
private val viewModel: RecoveryCodeViewModel by viewModel()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ import cash.z.ecc.android.bip39.Mnemonics.InvalidWordException
|
||||||
import cash.z.ecc.android.bip39.Mnemonics.WordCountException
|
import cash.z.ecc.android.bip39.Mnemonics.WordCountException
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import com.stevesoltys.seedvault.App
|
import com.stevesoltys.seedvault.App
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.service.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
|
@ -29,17 +29,17 @@ private val TAG = RecoveryCodeViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal class RecoveryCodeViewModel(
|
internal class RecoveryCodeViewModel(
|
||||||
app: App,
|
app: App,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val keyManager: KeyManager,
|
private val keyManager: KeyManager,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val backupCoordinator: BackupCoordinator,
|
private val backupCoordinatorService: BackupCoordinatorService,
|
||||||
private val notificationManager: BackupNotificationManager,
|
private val notificationManager: BackupNotificationManager,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
|
||||||
internal val wordList: List<CharArray> by lazy {
|
internal val wordList: List<CharArray> by lazy {
|
||||||
// we use our own entropy to not having to trust the library to use SecureRandom
|
// we use our own entropy to not having to trust the library to use SecureRandom
|
||||||
val entropy = crypto.getRandomBytes(Mnemonics.WordCount.COUNT_12.bitLength / 8)
|
val entropy = cryptoService.getRandomBytes(Mnemonics.WordCount.COUNT_12.bitLength / 8)
|
||||||
// create the words from the entropy
|
// create the words from the entropy
|
||||||
Mnemonics.MnemonicCode(entropy).words
|
Mnemonics.MnemonicCode(entropy).words
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +73,7 @@ internal class RecoveryCodeViewModel(
|
||||||
fun verifyExistingCode(input: List<CharSequence>) {
|
fun verifyExistingCode(input: List<CharSequence>) {
|
||||||
// we validate the code again, just in case
|
// we validate the code again, just in case
|
||||||
val seed = validateCode(input).toSeed()
|
val seed = validateCode(input).toSeed()
|
||||||
val verified = crypto.verifyBackupKey(seed)
|
val verified = cryptoService.verifyBackupKey(seed)
|
||||||
// store main key at this opportunity if it is still missing
|
// store main key at this opportunity if it is still missing
|
||||||
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
|
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
|
||||||
mExistingCodeChecked.setEvent(verified)
|
mExistingCodeChecked.setEvent(verified)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
import com.stevesoltys.seedvault.service.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
|
||||||
|
|
||||||
data class RestorableBackup(val backupMetadata: BackupMetadata) {
|
data class RestorableBackup(val backupMetadata: BackupMetadata) {
|
||||||
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
|
import com.stevesoltys.seedvault.ui.restore.apk.InstallProgressFragment
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
import com.stevesoltys.seedvault.ui.provision.RequireProvisioningActivity
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class RestoreActivity : RequireProvisioningActivity() {
|
class RestoreActivity : RequireProvisioningActivity() {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -8,7 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
|
import com.stevesoltys.seedvault.ui.restore.RestoreProgressAdapter.PackageViewHolder
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
|
import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
|
||||||
import android.text.format.DateUtils.HOUR_IN_MILLIS
|
import android.text.format.DateUtils.HOUR_IN_MILLIS
|
||||||
|
|
@ -10,7 +10,7 @@ import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
|
import com.stevesoltys.seedvault.ui.restore.RestoreSetAdapter.RestoreSetViewHolder
|
||||||
|
|
||||||
internal class RestoreSetAdapter(
|
internal class RestoreSetAdapter(
|
||||||
private val listener: RestorableBackupClickListener,
|
private val listener: RestorableBackupClickListener,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
|
|
@ -18,28 +18,28 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations.switchMap
|
import androidx.lifecycle.Transformations.switchMap
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.stevesoltys.seedvault.BackupMonitor
|
import com.stevesoltys.seedvault.service.app.BackupManagerOperationMonitor
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.service.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
|
import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkRestore
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkRestore
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
import com.stevesoltys.seedvault.ui.restore.apk.InstallIntentCreator
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallResult
|
import com.stevesoltys.seedvault.ui.restore.apk.InstallResult
|
||||||
import com.stevesoltys.seedvault.restore.install.isInstalled
|
import com.stevesoltys.seedvault.ui.restore.apk.isInstalled
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.service.settings.SettingsService
|
||||||
import com.stevesoltys.seedvault.storage.StorageRestoreService
|
import com.stevesoltys.seedvault.service.file.restore.FileRestoreService
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
||||||
|
|
@ -49,9 +49,9 @@ import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS
|
import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
|
import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -74,20 +74,20 @@ internal const val PACKAGES_PER_CHUNK = 100
|
||||||
|
|
||||||
internal class RestoreViewModel(
|
internal class RestoreViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
settingsManager: SettingsManager,
|
settingsService: SettingsService,
|
||||||
keyManager: KeyManager,
|
keyManager: KeyManager,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val restoreCoordinator: RestoreCoordinator,
|
private val restoreCoordinator: RestoreCoordinator,
|
||||||
private val apkRestore: ApkRestore,
|
private val apkRestore: ApkRestore,
|
||||||
storageBackup: StorageBackup,
|
storageBackup: StorageBackup,
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager),
|
) : RequireProvisioningViewModel(app, settingsService, keyManager),
|
||||||
RestorableBackupClickListener, SnapshotViewModel {
|
RestorableBackupClickListener, SnapshotViewModel {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
override val isRestoreOperation = true
|
||||||
|
|
||||||
private var session: IRestoreSession? = null
|
private var session: IRestoreSession? = null
|
||||||
private val monitor = BackupMonitor()
|
private val monitor = BackupManagerOperationMonitor()
|
||||||
|
|
||||||
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
|
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
|
||||||
internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
|
internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
|
||||||
|
|
@ -193,8 +193,8 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
// if we had no token before (i.e. restore from setup wizard),
|
// if we had no token before (i.e. restore from setup wizard),
|
||||||
// use the token of the current restore set from now on
|
// use the token of the current restore set from now on
|
||||||
if (settingsManager.getToken() == null) {
|
if (settingsService.getToken() == null) {
|
||||||
settingsManager.setNewToken(token)
|
settingsService.setNewToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// start a new restore session
|
// start a new restore session
|
||||||
|
|
@ -316,7 +316,7 @@ internal class RestoreViewModel(
|
||||||
private val restorableBackup: RestorableBackup,
|
private val restorableBackup: RestorableBackup,
|
||||||
private val session: IRestoreSession,
|
private val session: IRestoreSession,
|
||||||
private val packages: List<String>,
|
private val packages: List<String>,
|
||||||
private val monitor: BackupMonitor,
|
private val monitor: BackupManagerOperationMonitor,
|
||||||
) : IRestoreObserver.Stub() {
|
) : IRestoreObserver.Stub() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -441,7 +441,7 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
internal fun startFilesRestore(item: SnapshotItem) {
|
internal fun startFilesRestore(item: SnapshotItem) {
|
||||||
val i = Intent(app, StorageRestoreService::class.java)
|
val i = Intent(app, FileRestoreService::class.java)
|
||||||
i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId)
|
i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId)
|
||||||
i.putExtra(EXTRA_TIMESTAMP_START, item.time)
|
i.putExtra(EXTRA_TIMESTAMP_START, item.time)
|
||||||
app.startForegroundService(i)
|
app.startForegroundService(i)
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
package com.stevesoltys.seedvault
|
package com.stevesoltys.seedvault.ui.restore
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
|
||||||
|
|
||||||
private val TAG = BroadcastReceiver::class.java.simpleName
|
|
||||||
private const val RESTORE_SECRET_CODE = "7378673"
|
|
||||||
|
|
||||||
class SecretCodeReceiver : BroadcastReceiver() {
|
class SecretCodeReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BroadcastReceiver::class.java.simpleName
|
||||||
|
|
||||||
|
private const val RESTORE_SECRET_CODE = "7378673"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.data?.host != RESTORE_SECRET_CODE) return
|
if (intent.data?.host != RESTORE_SECRET_CODE) return
|
||||||
Log.d(TAG, "Restore secret code received.")
|
Log.d(TAG, "Restore secret code received.")
|
||||||
val i = Intent(context, RestoreActivity::class.java).apply {
|
val i = Intent(context, RestoreActivity::class.java).apply {
|
||||||
flags = FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
}
|
}
|
||||||
context.startActivity(i)
|
context.startActivity(i)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore.install
|
package com.stevesoltys.seedvault.ui.restore.apk
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
|
@ -20,8 +20,8 @@ import android.content.pm.PackageInstaller.SessionParams
|
||||||
import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
package com.stevesoltys.seedvault.restore.install
|
package com.stevesoltys.seedvault.ui.restore.apk
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.service.crypto.CryptoService
|
||||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
import com.stevesoltys.seedvault.service.metadata.ApkSplit
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.service.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.service.storage.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
import com.stevesoltys.seedvault.ui.restore.RestorableBackup
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED_SYSTEM_APP
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
|
import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
|
||||||
import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash
|
import com.stevesoltys.seedvault.service.app.backup.apk.copyStreamsAndGetHash
|
||||||
import com.stevesoltys.seedvault.transport.backup.getSignatures
|
import com.stevesoltys.seedvault.service.app.backup.apk.getSignatures
|
||||||
import com.stevesoltys.seedvault.transport.backup.isSystemApp
|
import com.stevesoltys.seedvault.service.app.isSystemApp
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
@ -31,7 +31,7 @@ internal class ApkRestore(
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePlugin: StoragePlugin,
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyStoragePlugin: LegacyStoragePlugin,
|
private val legacyStoragePlugin: LegacyStoragePlugin,
|
||||||
private val crypto: Crypto,
|
private val cryptoService: CryptoService,
|
||||||
private val splitCompatChecker: ApkSplitCompatibilityChecker,
|
private val splitCompatChecker: ApkSplitCompatibilityChecker,
|
||||||
private val apkInstaller: ApkInstaller,
|
private val apkInstaller: ApkInstaller,
|
||||||
) {
|
) {
|
||||||
|
|
@ -222,7 +222,7 @@ internal class ApkRestore(
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
legacyStoragePlugin.getApkInputStream(token, packageName, suffix)
|
legacyStoragePlugin.getApkInputStream(token, packageName, suffix)
|
||||||
} else {
|
} else {
|
||||||
val name = crypto.getNameForApk(salt, packageName, suffix)
|
val name = cryptoService.getNameForApk(salt, packageName, suffix)
|
||||||
storagePlugin.getInputStream(token, name)
|
storagePlugin.getInputStream(token, name)
|
||||||
}
|
}
|
||||||
val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream())
|
val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream())
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore.install
|
package com.stevesoltys.seedvault.ui.restore.apk
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore.install
|
package com.stevesoltys.seedvault.ui.restore.apk
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore.install
|
package com.stevesoltys.seedvault.ui.restore.apk
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.ACTION_VIEW
|
import android.content.Intent.ACTION_VIEW
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stevesoltys.seedvault.restore.install
|
package com.stevesoltys.seedvault.ui.restore.apk
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue