Merge pull request #562 from seedvault-app/feature/d2d-transfer

Add experimental support for forcing D2D transfer backups
This commit is contained in:
Torsten Grote 2024-01-15 11:24:27 -03:00 committed by GitHub
commit 0319d733c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 342 additions and 69 deletions

View file

@ -14,8 +14,10 @@ echo "Setting Seedvault transport..."
sleep 10 sleep 10
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
D2D_BACKUP_TEST=$1
large_test_exit_code=0 large_test_exit_code=0
./gradlew --stacktrace -Pinstrumented_test_size=large :app:connectedAndroidTest || large_test_exit_code=$? ./gradlew --stacktrace -Pinstrumented_test_size=large -Pd2d_backup_test="$D2D_BACKUP_TEST" :app:connectedAndroidTest || large_test_exit_code=$?
adb pull /sdcard/seedvault_test_results adb pull /sdcard/seedvault_test_results

View file

@ -5,6 +5,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: read
actions: read
checks: write
jobs: jobs:
build: build:
name: Build name: Build
@ -40,3 +45,10 @@ jobs:
app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/debug/app-debug.apk
contactsbackup/build/outputs/apk/debug/contactsbackup-debug.apk contactsbackup/build/outputs/apk/debug/contactsbackup-debug.apk
storage/demo/build/outputs/apk/debug/demo-debug.apk storage/demo/build/outputs/apk/debug/demo-debug.apk
- name: Publish Test Report
uses: mikepenz/action-junit-report@v4
if: success() || failure()
with:
report_paths: '**/build/test-results/**/TEST-*.xml'

View file

@ -20,6 +20,7 @@ jobs:
matrix: matrix:
android_target: [ 33, 34 ] android_target: [ 33, 34 ]
emulator_type: [ default ] emulator_type: [ default ]
d2d_backup_test: [ true, false ]
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -52,7 +53,7 @@ jobs:
disable-animations: true disable-animations: true
script: | script: |
./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64" ./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64"
./.github/scripts/run_tests.sh ./.github/scripts/run_tests.sh ${{ matrix.d2d_backup_test }}
- name: Upload test results - name: Upload test results
if: always() if: always()

View file

@ -24,14 +24,17 @@ android {
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionNameSuffix = "-${gitDescribe()}" versionNameSuffix = "-${gitDescribe()}"
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner" testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
testInstrumentationRunnerArguments(mapOf("disableAnalytics" to "true")) testInstrumentationRunnerArguments["disableAnalytics"] = "true"
if (project.hasProperty("instrumented_test_size")) { if (project.hasProperty("instrumented_test_size")) {
val testSize = project.property("instrumented_test_size").toString() val testSize = project.property("instrumented_test_size").toString()
println("Instrumented test size: $testSize") println("Instrumented test size: $testSize")
testInstrumentationRunnerArguments(mapOf("size" to testSize)) testInstrumentationRunnerArguments["size"] = testSize
} }
val d2dBackupTest = project.findProperty("d2d_backup_test")?.toString() ?: "true"
testInstrumentationRunnerArguments["d2d_backup_test"] = d2dBackupTest
} }
signingConfigs { signingConfigs {

View file

@ -84,7 +84,7 @@ echo "Downloading and extracting test backup to '/sdcard/seedvault_baseline'..."
if [ ! -f backup.tar.gz ]; then if [ ! -f backup.tar.gz ]; then
echo "Downloading test backup..." echo "Downloading test backup..."
wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/1/backup.tar.gz wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/3/backup.tar.gz
fi fi
$ADB root $ADB root

View file

@ -1,9 +1,11 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault
import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory import com.stevesoltys.seedvault.transport.backup.InputFactory
import com.stevesoltys.seedvault.transport.backup.KVBackup import com.stevesoltys.seedvault.transport.backup.KVBackup
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.restore.FullRestore import com.stevesoltys.seedvault.transport.restore.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore import com.stevesoltys.seedvault.transport.restore.KVRestore
import com.stevesoltys.seedvault.transport.restore.OutputFactory import com.stevesoltys.seedvault.transport.restore.OutputFactory
@ -25,6 +27,9 @@ class KoinInstrumentationTestApp : App() {
val testModule = module { val testModule = module {
val context = this@KoinInstrumentationTestApp val context = this@KoinInstrumentationTestApp
single { spyk(PackageService(context, get(), get(), get())) }
single { spyk(SettingsManager(context)) }
single { spyk(BackupNotificationManager(context)) } single { spyk(BackupNotificationManager(context)) }
single { spyk(FullBackup(get(), get(), get(), get())) } single { spyk(FullBackup(get(), get(), get(), get())) }
single { spyk(KVBackup(get(), get(), get(), get(), get())) } single { spyk(KVBackup(get(), get(), get(), get(), get())) }

View file

@ -1,6 +1,5 @@
package com.stevesoltys.seedvault.e2e package com.stevesoltys.seedvault.e2e
import android.app.backup.IBackupManager
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
@ -26,8 +25,6 @@ internal interface LargeBackupTestBase : LargeTestBase {
private const val BACKUP_TIMEOUT = 360 * 1000L private const val BACKUP_TIMEOUT = 360 * 1000L
} }
val backupManager: IBackupManager get() = get()
val spyBackupNotificationManager: BackupNotificationManager get() = get() val spyBackupNotificationManager: BackupNotificationManager get() = get()
val spyFullBackup: FullBackup get() = get() val spyFullBackup: FullBackup get() = get()

View file

@ -173,6 +173,10 @@ internal interface LargeRestoreTestBase : LargeTestBase {
coEvery { coEvery {
spyFullRestore.initializeState(any(), any(), any(), any()) spyFullRestore.initializeState(any(), any(), any(), any())
} answers { } answers {
packageName?.let {
restoreResult.full[it] = dataIntercept.toByteArray().sha256()
}
packageName = arg<PackageInfo>(3).packageName packageName = arg<PackageInfo>(3).packageName
dataIntercept = ByteArrayOutputStream() dataIntercept = ByteArrayOutputStream()

View file

@ -1,6 +1,7 @@
package com.stevesoltys.seedvault.e2e package com.stevesoltys.seedvault.e2e
import android.app.UiAutomation import android.app.UiAutomation
import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.pm.PackageManager.PERMISSION_GRANTED
@ -72,6 +73,8 @@ internal interface LargeTestBase : KoinComponent {
val spyMetadataManager: MetadataManager get() = get() val spyMetadataManager: MetadataManager get() = get()
val backupManager: IBackupManager get() = get()
val spyRestoreViewModel: RestoreViewModel val spyRestoreViewModel: RestoreViewModel
get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null") get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null")
@ -79,6 +82,7 @@ internal interface LargeTestBase : KoinComponent {
get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null") get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null")
fun resetApplicationState() { fun resetApplicationState() {
backupManager.setAutoRestore(false)
settingsManager.setNewToken(null) settingsManager.setNewToken(null)
documentsStorage.reset(null) documentsStorage.reset(null)
@ -95,6 +99,7 @@ internal interface LargeTestBase : KoinComponent {
} }
clearDocumentPickerAppData() clearDocumentPickerAppData()
device.executeShellCommand("rm -R $externalStorageDir/.SeedVaultAndroidBackup")
} }
fun waitUntilIdle() { fun waitUntilIdle() {
@ -157,6 +162,7 @@ internal interface LargeTestBase : KoinComponent {
fun clearTestBackups() { fun clearTestBackups() {
File(testStoragePath).deleteRecursively() File(testStoragePath).deleteRecursively()
File(testVideoPath).deleteRecursively()
} }
fun changeBackupLocation( fun changeBackupLocation(

View file

@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -40,6 +41,17 @@ internal abstract class SeedvaultLargeTest :
startRecordingTest(keepRecordingScreen, name.methodName) startRecordingTest(keepRecordingScreen, name.methodName)
restoreBaselineBackup() restoreBaselineBackup()
val arguments = InstrumentationRegistry.getArguments()
if (arguments.getString("d2d_backup_test") == "true") {
println("Enabling D2D backups for test")
settingsManager.setD2dBackupsEnabled(true)
} else {
println("Disabling D2D backups for test")
settingsManager.setD2dBackupsEnabled(false)
}
} }
@After @After
@ -63,10 +75,14 @@ internal abstract class SeedvaultLargeTest :
val extDir = externalStorageDir val extDir = externalStorageDir
device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup") device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup")
device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" + device.executeShellCommand(
".SeedVaultAndroidBackup $extDir") "cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" + ".SeedVaultAndroidBackup $extDir"
"recovery-code.txt $extDir") )
device.executeShellCommand(
"cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
"recovery-code.txt $extDir"
)
} }
if (backupFile.exists()) { if (backupFile.exists()) {

View file

@ -2,6 +2,7 @@ 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.metadata.PackageMetadata
import com.stevesoltys.seedvault.restore.AppRestoreResult
/** /**
* Contains maps of (package name -> SHA-256 hashes) of application data. * Contains maps of (package name -> SHA-256 hashes) of application data.
@ -12,8 +13,9 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
* For full backups, the mapping is: Map<PackageName, SHA-256> * For full backups, the mapping is: Map<PackageName, SHA-256>
* For K/V backups, the mapping is: Map<PackageName, Map<Key, SHA-256>> * For K/V backups, the mapping is: Map<PackageName, Map<Key, SHA-256>>
*/ */
data class SeedvaultLargeTestResult( internal data class SeedvaultLargeTestResult(
val backupResults: Map<String, PackageMetadata?> = emptyMap(), val backupResults: Map<String, PackageMetadata?> = emptyMap(),
val restoreResults: Map<String, AppRestoreResult?> = emptyMap(),
val full: MutableMap<String, String>, val full: MutableMap<String, String>,
val kv: MutableMap<String, MutableMap<String, String>>, val kv: MutableMap<String, MutableMap<String, String>>,
val userApps: List<PackageInfo>, val userApps: List<PackageInfo>,

View file

@ -1,8 +1,16 @@
package com.stevesoltys.seedvault.transport.backup package com.stevesoltys.seedvault.transport.backup
import android.content.pm.PackageInfo
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.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.AppStatus
import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
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
@ -14,10 +22,41 @@ class PackageServiceTest : KoinComponent {
private val packageService: PackageService by inject() private val packageService: PackageService by inject()
private val settingsManager: SettingsManager by inject()
private val storagePlugin: StoragePlugin by inject()
@Test @Test
fun testNotAllowedPackages() { fun testNotAllowedPackages() {
val packages = packageService.notBackedUpPackages val packages = packageService.notBackedUpPackages
Log.e("TEST", "Packages: $packages") Log.e("TEST", "Packages: $packages")
} }
@Test
fun `shouldIncludeAppInBackup exempts plugin provider and blacklisted apps`() {
val packageInfo = PackageInfo().apply {
packageName = "com.example"
}
val disabledAppStatus = mockk<AppStatus>().apply {
every { packageName } returns packageInfo.packageName
every { enabled } returns false
}
settingsManager.onAppBackupStatusChanged(disabledAppStatus)
// Should not backup blacklisted apps
assertFalse(packageService.shouldIncludeAppInBackup(packageInfo.packageName))
val enabledAppStatus = mockk<AppStatus>().apply {
every { packageName } returns packageInfo.packageName
every { enabled } returns true
}
settingsManager.onAppBackupStatusChanged(enabledAppStatus)
// Should backup non-blacklisted apps
assertTrue(packageService.shouldIncludeAppInBackup(packageInfo.packageName))
// Should not backup storage provider
assertFalse(packageService.shouldIncludeAppInBackup(storagePlugin.providerPackageName!!))
}
} }

View file

@ -18,6 +18,7 @@ data class BackupMetadata(
internal val androidVersion: Int = Build.VERSION.SDK_INT, internal val androidVersion: Int = Build.VERSION.SDK_INT,
internal val androidIncremental: String = Build.VERSION.INCREMENTAL, internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}", internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
internal var d2dBackup: Boolean = false,
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(), internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
) )
@ -29,6 +30,7 @@ internal const val JSON_METADATA_TIME = "time"
internal const val JSON_METADATA_SDK_INT = "sdk_int" internal const val JSON_METADATA_SDK_INT = "sdk_int"
internal const val JSON_METADATA_INCREMENTAL = "incremental" internal const val JSON_METADATA_INCREMENTAL = "incremental"
internal const val JSON_METADATA_NAME = "name" internal const val JSON_METADATA_NAME = "name"
internal const val JSON_METADATA_D2D_BACKUP = "d2d_backup"
enum class PackageState { enum class PackageState {
/** /**

View file

@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.isSystemApp import com.stevesoltys.seedvault.transport.backup.isSystemApp
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
@ -35,6 +36,7 @@ internal class MetadataManager(
private val crypto: Crypto, private val crypto: Crypto,
private val metadataWriter: MetadataWriter, private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader, private val metadataReader: MetadataReader,
private val settingsManager: SettingsManager
) { ) {
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "") private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
@ -135,6 +137,8 @@ internal class MetadataManager(
modifyMetadata(metadataOutputStream) { modifyMetadata(metadataOutputStream) {
val now = clock.time() val now = clock.time()
metadata.time = now metadata.time = now
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
if (metadata.packageMetadataMap.containsKey(packageName)) { if (metadata.packageMetadataMap.containsKey(packageName)) {
metadata.packageMetadataMap[packageName]!!.time = now metadata.packageMetadataMap[packageName]!!.time = now
metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA

View file

@ -4,7 +4,7 @@ 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()) } single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) } single<MetadataWriter> { MetadataWriterImpl(get()) }
single<MetadataReader> { MetadataReaderImpl(get()) } single<MetadataReader> { MetadataReaderImpl(get()) }
} }

View file

@ -152,7 +152,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
androidVersion = meta.getInt(JSON_METADATA_SDK_INT), androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL), androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
deviceName = meta.getString(JSON_METADATA_NAME), deviceName = meta.getString(JSON_METADATA_NAME),
packageMetadataMap = packageMetadataMap d2dBackup = meta.optBoolean(JSON_METADATA_D2D_BACKUP, false),
packageMetadataMap = packageMetadataMap,
) )
} catch (e: JSONException) { } catch (e: JSONException) {
throw SecurityException(e) throw SecurityException(e)

View file

@ -35,6 +35,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
put(JSON_METADATA_SDK_INT, metadata.androidVersion) put(JSON_METADATA_SDK_INT, metadata.androidVersion)
put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental) put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
put(JSON_METADATA_NAME, metadata.deviceName) put(JSON_METADATA_NAME, metadata.deviceName)
put(JSON_METADATA_D2D_BACKUP, metadata.d2dBackup)
}) })
} }
for ((packageName, packageMetadata) in metadata.packageMetadataMap) { for ((packageName, packageMetadata) in metadata.packageMetadataMap) {

View file

@ -23,6 +23,9 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) {
val deviceName: String val deviceName: String
get() = backupMetadata.deviceName get() = backupMetadata.deviceName
val d2dBackup: Boolean
get() = backupMetadata.d2dBackup
val packageMetadataMap: PackageMetadataMap val packageMetadataMap: PackageMetadataMap
get() = backupMetadata.packageMetadataMap get() = backupMetadata.packageMetadataMap

View file

@ -55,9 +55,16 @@ internal class AppListRetriever(
@WorkerThread @WorkerThread
fun getAppList(): List<AppListItem> { fun getAppList(): List<AppListItem> {
return listOf(AppSectionTitle(R.string.backup_section_system)) + getSpecialApps() +
listOf(AppSectionTitle(R.string.backup_section_user)) + getUserApps() + val appListSections = linkedMapOf(
listOf(AppSectionTitle(R.string.backup_section_not_allowed)) + getNotAllowedApps() AppSectionTitle(R.string.backup_section_system) to getSpecialApps(),
AppSectionTitle(R.string.backup_section_user) to getUserApps(),
AppSectionTitle(R.string.backup_section_not_allowed) to getNotAllowedApps()
).filter { it.value.isNotEmpty() }
return appListSections.flatMap { (sectionTitle, appList) ->
listOf(sectionTitle) + appList
}
} }
private fun getSpecialApps(): List<AppListItem> { private fun getSpecialApps(): List<AppListItem> {

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
@ -14,6 +15,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by sharedViewModel() private val viewModel: SettingsViewModel by sharedViewModel()
private val packageService: PackageService by inject() private val packageService: PackageService by inject()
// TODO set mimeType when upgrading androidx lib // TODO set mimeType when upgrading androidx lib
private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri -> private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri ->
viewModel.onLogcatUriReceived(uri) viewModel.onLogcatUriReceived(uri)
@ -23,6 +25,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
permitDiskReads { permitDiskReads {
setPreferencesFromResource(R.xml.settings_expert, rootKey) setPreferencesFromResource(R.xml.settings_expert, rootKey)
} }
findPreference<Preference>("logcat")?.setOnPreferenceClickListener { findPreference<Preference>("logcat")?.setOnPreferenceClickListener {
val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver" val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver"
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
@ -30,6 +33,25 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
createFileLauncher.launch(name) createFileLauncher.launch(name)
true true
} }
val quotaPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_UNLIMITED_QUOTA)
quotaPreference?.setOnPreferenceChangeListener { _, newValue ->
quotaPreference.isChecked = newValue as Boolean
true
}
val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
d2dPreference.isChecked = newValue as Boolean
// automatically enable unlimited quota when enabling D2D backups
if (d2dPreference.isChecked) {
quotaPreference?.isChecked = true
}
true
}
} }
override fun onStart() { override fun onStart() {

View file

@ -89,7 +89,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
true true
} }
autoRestore = findPreference("auto_restore")!! autoRestore = findPreference(PREF_KEY_AUTO_RESTORE)!!
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean val enabled = newValue as Boolean
try { try {

View file

@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentSkipListSet
internal const val PREF_KEY_TOKEN = "token" internal const val PREF_KEY_TOKEN = "token"
internal const val PREF_KEY_BACKUP_APK = "backup_apk" internal const val PREF_KEY_BACKUP_APK = "backup_apk"
internal const val PREF_KEY_AUTO_RESTORE = "auto_restore"
private const val PREF_KEY_STORAGE_URI = "storageUri" private const val PREF_KEY_STORAGE_URI = "storageUri"
private const val PREF_KEY_STORAGE_NAME = "storageName" private const val PREF_KEY_STORAGE_NAME = "storageName"
@ -30,7 +31,8 @@ private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist" private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage" private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota" internal const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
class SettingsManager(private val context: Context) { class SettingsManager(private val context: Context) {
@ -151,6 +153,14 @@ class SettingsManager(private val context: Context) {
} }
fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false) fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false)
fun d2dBackupsEnabled() = prefs.getBoolean(PREF_KEY_D2D_BACKUPS, false)
fun setD2dBackupsEnabled(enabled: Boolean) {
prefs.edit()
.putBoolean(PREF_KEY_D2D_BACKUPS, enabled)
.apply()
}
} }
data class Storage( data class Storage(

View file

@ -1,6 +1,7 @@
package com.stevesoltys.seedvault.transport package com.stevesoltys.seedvault.transport
import android.app.backup.BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED import android.app.backup.BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
import android.app.backup.BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER
import android.app.backup.BackupTransport import android.app.backup.BackupTransport
import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
@ -11,6 +12,7 @@ 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.settings.SettingsActivity
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -20,7 +22,8 @@ import org.koin.core.component.inject
// If we ever change this, we should use a ComponentName like the other backup transports. // If we ever change this, we should use a ComponentName like the other backup transports.
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED const val DEFAULT_TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
const val D2D_TRANSPORT_FLAGS = DEFAULT_TRANSPORT_FLAGS or FLAG_DEVICE_TO_DEVICE_TRANSFER
private const val TRANSPORT_DIRECTORY_NAME = private const val TRANSPORT_DIRECTORY_NAME =
"com.stevesoltys.seedvault.transport.ConfigurableBackupTransport" "com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
@ -35,6 +38,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
private val backupCoordinator by inject<BackupCoordinator>() private val backupCoordinator by inject<BackupCoordinator>()
private val restoreCoordinator by inject<RestoreCoordinator>() private val restoreCoordinator by inject<RestoreCoordinator>()
private val settingsManager by inject<SettingsManager>()
override fun transportDirName(): String { override fun transportDirName(): String {
return TRANSPORT_DIRECTORY_NAME return TRANSPORT_DIRECTORY_NAME
@ -54,7 +58,11 @@ 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 TRANSPORT_FLAGS return if (settingsManager.d2dBackupsEnabled()) {
D2D_TRANSPORT_FLAGS
} else {
DEFAULT_TRANSPORT_FLAGS
}
} }
/** /**

View file

@ -143,12 +143,9 @@ internal class BackupCoordinator(
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean, @Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
): Boolean { ): Boolean {
val packageName = targetPackage.packageName val packageName = targetPackage.packageName
// Check that the app is not blacklisted by the user val shouldInclude = packageService.shouldIncludeAppInBackup(packageName)
val enabled = settingsManager.isBackupEnabled(packageName) if (!shouldInclude) Log.i(TAG, "Excluding $packageName from backup.")
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.") return shouldInclude
// We need to exclude the DocumentsProvider used to store backup data.
// Otherwise, it gets killed when we back it up, terminating our backup.
return enabled && targetPackage.packageName != plugin.providerPackageName
} }
/** /**

View file

@ -8,7 +8,9 @@ val backupModule = module {
single { single {
PackageService( PackageService(
context = androidContext(), context = androidContext(),
backupManager = get() backupManager = get(),
settingsManager = get(),
plugin = get()
) )
} }
single { single {

View file

@ -17,6 +17,8 @@ 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.settings.SettingsManager
private val TAG = PackageService::class.java.simpleName private val TAG = PackageService::class.java.simpleName
@ -29,6 +31,8 @@ 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 backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val plugin: StoragePlugin,
) { ) {
private val packageManager: PackageManager = context.packageManager private val packageManager: PackageManager = context.packageManager
@ -45,13 +49,16 @@ internal class PackageService(
// log packages // log packages
if (Log.isLoggable(TAG, INFO)) { if (Log.isLoggable(TAG, INFO)) {
Log.i(TAG, "Got ${packages.size} packages:") Log.i(TAG, "Got ${packages.size} packages:")
packages.chunked(LOG_MAX_PACKAGES).forEach { logPackages(packages)
Log.i(TAG, it.toString())
}
} }
val eligibleApps = val eligibleApps = if (settingsManager.d2dBackupsEnabled()) {
// if D2D is enabled, use the "new method" for filtering packages
packages.filter(::shouldIncludeAppInBackup).toTypedArray()
} else {
// otherwise, use the BackupManager call.
backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray()) backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray())
}
// log eligible packages // log eligible packages
if (Log.isLoggable(TAG, INFO)) { if (Log.isLoggable(TAG, INFO)) {
@ -66,6 +73,9 @@ internal class PackageService(
return packageArray.toTypedArray() return packageArray.toTypedArray()
} }
/**
* A list of packages that will not be backed up.
*/
val notBackedUpPackages: List<PackageInfo> val notBackedUpPackages: List<PackageInfo>
@WorkerThread @WorkerThread
get() { get() {
@ -94,16 +104,23 @@ internal class PackageService(
val userApps: List<PackageInfo> val userApps: List<PackageInfo>
@WorkerThread @WorkerThread
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo -> get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
packageInfo.isUserVisible(context) && packageInfo.allowsBackup() packageInfo.isUserVisible(context) &&
packageInfo.allowsBackup()
} }
/** /**
* A list of apps that does not allow backup. * A list of apps that do not allow backup.
*/ */
val userNotAllowedApps: List<PackageInfo> val userNotAllowedApps: List<PackageInfo>
@WorkerThread @WorkerThread
get() = packageManager.getInstalledPackages(0).filter { packageInfo -> get() {
!packageInfo.allowsBackup() && !packageInfo.isSystemApp() // if D2D backups are enabled, all apps are allowed
if (settingsManager.d2dBackupsEnabled()) return emptyList()
return packageManager.getInstalledPackages(0).filter { packageInfo ->
!packageInfo.allowsBackup() &&
!packageInfo.isSystemApp()
}
} }
val expectedAppTotals: ExpectedAppTotals val expectedAppTotals: ExpectedAppTotals
@ -128,12 +145,64 @@ internal class PackageService(
null null
} }
fun shouldIncludeAppInBackup(packageName: String): Boolean {
// We do not use BackupManager.filterAppsEligibleForBackupForUser for D2D because it
// always makes its determinations based on OperationType.BACKUP, never based on
// OperationType.MIGRATION, and there are no alternative publicly-available APIs.
// We don't need to use it, here, either; during a backup or migration, the system
// will perform its own eligibility checks regardless. We merely need to filter out
// apps that we, or the user, want to exclude.
// Check that the app is not excluded by user preference
val enabled = settingsManager.isBackupEnabled(packageName)
// We need to explicitly exclude DocumentsProvider and Seedvault.
// Otherwise, they get killed while backing them up, terminating our backup.
val excludedPackages = setOf(
plugin.providerPackageName,
context.packageName
)
return enabled && !excludedPackages.contains(packageName)
}
private fun logPackages(packages: List<String>) { private fun logPackages(packages: List<String>) {
packages.chunked(LOG_MAX_PACKAGES).forEach { packages.chunked(LOG_MAX_PACKAGES).forEach {
Log.i(TAG, it.toString()) Log.i(TAG, it.toString())
} }
} }
private fun PackageInfo.allowsBackup(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
return if (settingsManager.d2dBackupsEnabled()) {
/**
* TODO: Consider ways of replicating the system's logic so that the user can have
* advance knowledge of apps that the system will exclude, particularly apps targeting
* SDK 30 or below.
*
* At backup time, the system will filter out any apps that *it* does not want to be
* backed up. If the user has enabled D2D, *we* generally want to back up as much as
* possible; part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup).
* So, we return true.
* See frameworks/base/services/backup/java/com/android/server/backup/utils/
* BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
*/
true
} else {
applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
}
}
/**
* A flag indicating whether or not this package should _not_ be backed up.
*
* This happens when the app has opted-out of backup, or when it is stopped.
*/
private fun PackageInfo.doesNotGetBackedUp(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
return !allowsBackup() || isStopped()
}
} }
internal data class ExpectedAppTotals( internal data class ExpectedAppTotals(
@ -157,11 +226,6 @@ internal fun PackageInfo.isSystemApp(): Boolean {
return applicationInfo.flags and FLAG_SYSTEM != 0 return applicationInfo.flags and FLAG_SYSTEM != 0
} }
internal fun PackageInfo.allowsBackup(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
}
/** /**
* Returns true if this is a system app that hasn't been updated. * Returns true if this is a system app that hasn't been updated.
* We don't back up those APKs. * We don't back up those APKs.
@ -173,12 +237,6 @@ internal fun PackageInfo.isNotUpdatedSystemApp(): Boolean {
return isSystemApp && !isUpdatedSystemApp return isSystemApp && !isUpdatedSystemApp
} }
internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
}
internal fun PackageInfo.isStopped(): Boolean { internal fun PackageInfo.isStopped(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
return applicationInfo.flags and FLAG_STOPPED != 0 return applicationInfo.flags and FLAG_STOPPED != 0

View file

@ -22,10 +22,19 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_FLAGS import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import java.io.IOException import java.io.IOException
/**
* Device name used in AOSP to indicate that a restore set is part of a device-to-device migration.
* See getBackupEligibilityRules in frameworks/base/services/backup/java/com/android/server/
* backup/restore/ActiveRestoreSession.java. AOSP currently relies on this constant, and it is not
* publicly exposed. Framework code indicates they intend to use a flag, instead, in the future.
*/
internal const val D2D_DEVICE_NAME = "D2D"
private data class RestoreCoordinatorState( private data class RestoreCoordinatorState(
val token: Long, val token: Long,
val packages: Iterator<PackageInfo>, val packages: Iterator<PackageInfo>,
@ -92,7 +101,20 @@ internal class RestoreCoordinator(
**/ **/
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? { suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
return getAvailableMetadata()?.map { (_, metadata) -> return getAvailableMetadata()?.map { (_, metadata) ->
RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token, TRANSPORT_FLAGS)
val transportFlags = if (metadata.d2dBackup) {
D2D_TRANSPORT_FLAGS
} else {
DEFAULT_TRANSPORT_FLAGS
}
val deviceName = if (metadata.d2dBackup) {
D2D_DEVICE_NAME
} else {
metadata.deviceName
}
RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
}?.toTypedArray() }?.toTypedArray()
} }
@ -114,6 +136,10 @@ internal class RestoreCoordinator(
*/ */
fun beforeStartRestore(backupMetadata: BackupMetadata) { fun beforeStartRestore(backupMetadata: BackupMetadata) {
this.backupMetadata = backupMetadata this.backupMetadata = backupMetadata
if (backupMetadata.d2dBackup) {
settingsManager.setD2dBackupsEnabled(true)
}
} }
/** /**
@ -219,6 +245,7 @@ internal class RestoreCoordinator(
TYPE_KEY_VALUE TYPE_KEY_VALUE
} else throw IOException("No data found for $packageName. Skipping.") } else throw IOException("No data found for $packageName. Skipping.")
} }
BackupType.FULL -> { BackupType.FULL -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
if (plugin.hasData(state.token, name)) { if (plugin.hasData(state.token, name)) {
@ -228,6 +255,7 @@ internal class RestoreCoordinator(
TYPE_FULL_STREAM TYPE_FULL_STREAM
} else throw IOException("No data found for $packageName. Skipping...") } else throw IOException("No data found for $packageName. Skipping...")
} }
null -> { null -> {
Log.i(TAG, "No backup type found for $packageName. Skipping...") Log.i(TAG, "No backup type found for $packageName. Skipping...")
state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s -> state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s ->
@ -261,12 +289,14 @@ internal class RestoreCoordinator(
state.currentPackage = packageName state.currentPackage = packageName
TYPE_KEY_VALUE TYPE_KEY_VALUE
} }
full.hasDataForPackage(state.token, packageInfo) -> { full.hasDataForPackage(state.token, packageInfo) -> {
Log.i(TAG, "Found full backup data for $packageName.") Log.i(TAG, "Found full backup data for $packageName.")
full.initializeState(0x00, state.token, "", packageInfo) full.initializeState(0x00, state.token, "", packageInfo)
state.currentPackage = packageName state.currentPackage = packageName
TYPE_FULL_STREAM TYPE_FULL_STREAM
} }
else -> { else -> {
Log.i(TAG, "No data found for $packageName. Skipping.") Log.i(TAG, "No data found for $packageName. Skipping.")
return nextRestorePackage() return nextRestorePackage()

View file

@ -49,6 +49,8 @@
<string name="settings_expert_title">Expert settings</string> <string name="settings_expert_title">Expert settings</string>
<string name="settings_expert_quota_title">Unlimited app quota</string> <string name="settings_expert_quota_title">Unlimited app quota</string>
<string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string> <string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string>
<string name="settings_expert_d2d_title">Device-to-device backups</string>
<string name="settings_expert_d2d_summary">This forces backups for most apps, even when they disallow them. This is experimental, use at your own risk.\n\n1. To backup apps in D2D mode, you will need to run \"Backup now\" manually.\n\n2. Android may overwrite D2D backups for apps which normally allow backups.</string>
<string name="settings_expert_logcat_title">Save app log</string> <string name="settings_expert_logcat_title">Save app log</string>
<string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string> <string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string>
<string name="settings_expert_logcat_error">Error: Could not save app log</string> <string name="settings_expert_logcat_error">Error: Could not save app log</string>

View file

@ -5,6 +5,12 @@
android:key="unlimited_quota" android:key="unlimited_quota"
android:summary="@string/settings_expert_quota_summary" android:summary="@string/settings_expert_quota_summary"
android:title="@string/settings_expert_quota_title" /> android:title="@string/settings_expert_quota_title" />
<SwitchPreferenceCompat
android:id="@+id/d2d_backup_preference"
android:defaultValue="false"
android:key="d2d_backups"
android:summary="@string/settings_expert_d2d_summary"
android:title="@string/settings_expert_d2d_title" />
<Preference <Preference
android:icon="@drawable/ic_bug_report" android:icon="@drawable/ic_bug_report"
android:key="logcat" android:key="logcat"

View file

@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.metadataModule import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
import com.stevesoltys.seedvault.restore.install.installModule import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.backupModule import com.stevesoltys.seedvault.transport.backup.backupModule
import com.stevesoltys.seedvault.transport.restore.restoreModule import com.stevesoltys.seedvault.transport.restore.restoreModule
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@ -25,6 +26,7 @@ class TestApp : App() {
} }
private val appModule = module { private val appModule = module {
single { Clock() } single { Clock() }
single { SettingsManager(this@TestApp) }
} }
override fun startKoin() = startKoin { override fun startKoin() = startKoin {

View file

@ -19,6 +19,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
@ -26,7 +27,10 @@ import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.core.context.stopKoin import org.koin.core.context.stopKoin
@ -51,8 +55,16 @@ class MetadataManagerTest {
private val crypto: Crypto = mockk() private val crypto: Crypto = mockk()
private val metadataWriter: MetadataWriter = mockk() private val metadataWriter: MetadataWriter = mockk()
private val metadataReader: MetadataReader = mockk() private val metadataReader: MetadataReader = mockk()
private val settingsManager: SettingsManager = mockk()
private val manager = MetadataManager(context, clock, crypto, metadataWriter, metadataReader) private val manager = MetadataManager(
context = context,
clock = clock,
crypto = crypto,
metadataWriter = metadataWriter,
metadataReader = metadataReader,
settingsManager = settingsManager
)
private val time = 42L private val time = 42L
private val token = Random.nextLong() private val token = Random.nextLong()
@ -69,6 +81,11 @@ class MetadataManagerTest {
private val cacheInputStream: FileInputStream = mockk() private val cacheInputStream: FileInputStream = mockk()
private val encodedMetadata = getRandomByteArray() private val encodedMetadata = getRandomByteArray()
@Before
fun beforeEachTest() {
every { settingsManager.d2dBackupsEnabled() } returns false
}
@After @After
fun afterEachTest() { fun afterEachTest() {
stopKoin() stopKoin()
@ -246,6 +263,23 @@ class MetadataManagerTest {
manager.getPackageMetadata(packageName) manager.getPackageMetadata(packageName)
) )
assertEquals(time, manager.getLastBackupTime()) assertEquals(time, manager.getLastBackupTime())
assertFalse(updatedMetadata.d2dBackup)
verify {
cacheInputStream.close()
cacheOutputStream.close()
}
}
@Test
fun `test onPackageBackedUp() with D2D enabled`() {
expectReadFromCache()
every { clock.time() } returns time
expectModifyMetadata(initialMetadata)
every { settingsManager.d2dBackupsEnabled() } returns true
manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
assertTrue(initialMetadata.d2dBackup)
verify { verify {
cacheInputStream.close() cacheInputStream.close()

View file

@ -59,6 +59,10 @@ internal abstract class TransportTest {
put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV)) put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV))
} }
) )
protected val d2dMetadata = metadata.copy(
d2dBackup = true
)
protected val salt = metadata.salt protected val salt = metadata.salt
protected val name = getRandomString(12) protected val name = getRandomString(12)
protected val name2 = getRandomString(23) protected val name2 = getRandomString(23)

View file

@ -32,8 +32,6 @@ import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@ -46,8 +44,8 @@ internal class BackupCoordinatorTest : BackupTest() {
private val kv = mockk<KVBackup>() private val kv = mockk<KVBackup>()
private val full = mockk<FullBackup>() private val full = mockk<FullBackup>()
private val apkBackup = mockk<ApkBackup>() private val apkBackup = mockk<ApkBackup>()
private val packageService: PackageService = mockk()
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val packageService = mockk<PackageService>()
private val backup = BackupCoordinator( private val backup = BackupCoordinator(
context, context,
@ -170,20 +168,6 @@ internal class BackupCoordinatorTest : BackupTest() {
verify { metadataOutputStream.close() } verify { metadataOutputStream.close() }
} }
@Test
fun `isAppEligibleForBackup() exempts plugin provider and blacklisted apps`() {
every {
settingsManager.isBackupEnabled(packageInfo.packageName)
} returns true andThen false andThen true
every {
plugin.providerPackageName
} returns packageInfo.packageName andThen "new.package" andThen "new.package"
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
assertTrue(backup.isAppEligibleForBackup(packageInfo, true))
}
@Test @Test
fun `clearing KV backup data throws`() = runBlocking { fun `clearing KV backup data throws`() = runBlocking {
every { settingsManager.getToken() } returns token every { settingsManager.getToken() } returns token

View file

@ -90,6 +90,15 @@ internal class RestoreCoordinatorTest : TransportTest() {
assertEquals(metadata.deviceName, sets[0].device) assertEquals(metadata.deviceName, sets[0].device)
assertEquals(metadata.deviceName, sets[0].name) assertEquals(metadata.deviceName, sets[0].name)
assertEquals(metadata.token, sets[0].token) assertEquals(metadata.token, sets[0].token)
every { metadataReader.readMetadata(inputStream, token) } returns d2dMetadata
every { metadataReader.readMetadata(inputStream, token + 1) } returns d2dMetadata
val d2dSets = restore.getAvailableRestoreSets() ?: fail()
assertEquals(2, d2dSets.size)
assertEquals(D2D_DEVICE_NAME, d2dSets[0].device)
assertEquals(metadata.deviceName, d2dSets[0].name)
assertEquals(metadata.token, d2dSets[0].token)
} }
@Test @Test