From fa4c52fb83a6ed477a02a2cb316856529ca35d3d Mon Sep 17 00:00:00 2001 From: Steve Soltys <steve@stevesoltys.com> Date: Sat, 30 Dec 2023 15:53:35 -0500 Subject: [PATCH] Add experimental support for forcing D2D transfer backups --- .github/scripts/run_tests.sh | 4 +- .github/workflows/test.yml | 3 +- app/build.gradle.kts | 7 ++- app/development/scripts/provision_emulator.sh | 2 +- .../seedvault/e2e/LargeBackupTestBase.kt | 3 - .../seedvault/e2e/LargeRestoreTestBase.kt | 4 ++ .../seedvault/e2e/LargeTestBase.kt | 6 ++ .../seedvault/e2e/SeedvaultLargeTest.kt | 21 +++++-- .../seedvault/e2e/SeedvaultLargeTestResult.kt | 4 +- .../java/com/stevesoltys/seedvault/App.kt | 4 -- .../seedvault/metadata/Metadata.kt | 2 + .../seedvault/metadata/MetadataManager.kt | 4 ++ .../seedvault/metadata/MetadataModule.kt | 2 +- .../seedvault/metadata/MetadataReader.kt | 3 +- .../seedvault/metadata/MetadataWriter.kt | 1 + .../seedvault/restore/RestorableBackup.kt | 3 + .../seedvault/settings/AppListRetriever.kt | 13 +++- .../settings/ExpertSettingsFragment.kt | 10 +++ .../seedvault/settings/SettingsFragment.kt | 2 +- .../seedvault/settings/SettingsManager.kt | 10 +++ .../transport/ConfigurableBackupTransport.kt | 13 ++-- .../transport/backup/BackupModule.kt | 1 + .../transport/backup/PackageService.kt | 61 ++++++++++++------- .../transport/restore/RestoreCoordinator.kt | 29 +++++++-- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/settings_expert.xml | 6 ++ .../java/com/stevesoltys/seedvault/TestApp.kt | 2 + .../seedvault/metadata/MetadataManagerTest.kt | 17 +++++- .../seedvault/transport/TransportTest.kt | 4 ++ .../restore/RestoreCoordinatorTest.kt | 11 +++- 30 files changed, 200 insertions(+), 54 deletions(-) diff --git a/.github/scripts/run_tests.sh b/.github/scripts/run_tests.sh index 48a86572..b680a247 100755 --- a/.github/scripts/run_tests.sh +++ b/.github/scripts/run_tests.sh @@ -14,8 +14,10 @@ echo "Setting Seedvault transport..." sleep 10 adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport +D2D_BACKUP_TEST=$1 + 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a624c775..8aeddf71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: matrix: android_target: [ 33, 34 ] emulator_type: [ default ] + d2d_backup_test: [ true, false ] steps: - name: Checkout Code uses: actions/checkout@v3 @@ -52,7 +53,7 @@ jobs: disable-animations: true script: | ./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 if: always() diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb2151cf..52a0e183 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,14 +24,17 @@ android { targetSdk = libs.versions.targetSdk.get().toInt() versionNameSuffix = "-${gitDescribe()}" testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner" - testInstrumentationRunnerArguments(mapOf("disableAnalytics" to "true")) + testInstrumentationRunnerArguments["disableAnalytics"] = "true" if (project.hasProperty("instrumented_test_size")) { val testSize = project.property("instrumented_test_size").toString() 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 { diff --git a/app/development/scripts/provision_emulator.sh b/app/development/scripts/provision_emulator.sh index fef04b8b..284e7082 100755 --- a/app/development/scripts/provision_emulator.sh +++ b/app/development/scripts/provision_emulator.sh @@ -84,7 +84,7 @@ echo "Downloading and extracting test backup to '/sdcard/seedvault_baseline'..." if [ ! -f backup.tar.gz ]; then 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 $ADB root diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt index 70f58974..0886c682 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt @@ -1,6 +1,5 @@ package com.stevesoltys.seedvault.e2e -import android.app.backup.IBackupManager import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept @@ -26,8 +25,6 @@ internal interface LargeBackupTestBase : LargeTestBase { private const val BACKUP_TIMEOUT = 360 * 1000L } - val backupManager: IBackupManager get() = get() - val spyBackupNotificationManager: BackupNotificationManager get() = get() val spyFullBackup: FullBackup get() = get() diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt index 3d74aede..be95877d 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt @@ -173,6 +173,10 @@ internal interface LargeRestoreTestBase : LargeTestBase { coEvery { spyFullRestore.initializeState(any(), any(), any(), any()) } answers { + packageName?.let { + restoreResult.full[it] = dataIntercept.toByteArray().sha256() + } + packageName = arg<PackageInfo>(3).packageName dataIntercept = ByteArrayOutputStream() diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt index 86f14a28..69d0cf62 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt @@ -1,6 +1,7 @@ package com.stevesoltys.seedvault.e2e import android.app.UiAutomation +import android.app.backup.IBackupManager import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager.PERMISSION_GRANTED @@ -72,6 +73,8 @@ internal interface LargeTestBase : KoinComponent { val spyMetadataManager: MetadataManager get() = get() + val backupManager: IBackupManager get() = get() + val spyRestoreViewModel: RestoreViewModel get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null") @@ -79,6 +82,7 @@ internal interface LargeTestBase : KoinComponent { get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null") fun resetApplicationState() { + backupManager.setAutoRestore(false) settingsManager.setNewToken(null) documentsStorage.reset(null) @@ -95,6 +99,7 @@ internal interface LargeTestBase : KoinComponent { } clearDocumentPickerAppData() + device.executeShellCommand("rm -R $externalStorageDir/.SeedVaultAndroidBackup") } fun waitUntilIdle() { @@ -157,6 +162,7 @@ internal interface LargeTestBase : KoinComponent { fun clearTestBackups() { File(testStoragePath).deleteRecursively() + File(testVideoPath).deleteRecursively() } fun changeBackupLocation( diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt index 2d2be5f1..e0e29f10 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt @@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e import android.content.pm.PackageManager import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before @@ -40,6 +41,14 @@ internal abstract class SeedvaultLargeTest : startRecordingTest(keepRecordingScreen, name.methodName) restoreBaselineBackup() + + val arguments = InstrumentationRegistry.getArguments() + + if (arguments.getString("d2d_backup_test") == "true") { + println("Enabling D2D backups for test") + + settingsManager.setD2dBackupsEnabled(true) + } } @After @@ -63,10 +72,14 @@ internal abstract class SeedvaultLargeTest : val extDir = externalStorageDir device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup") - device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" + - ".SeedVaultAndroidBackup $extDir") - device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" + - "recovery-code.txt $extDir") + device.executeShellCommand( + "cp -R $extDir/$BASELINE_BACKUP_FOLDER/" + + ".SeedVaultAndroidBackup $extDir" + ) + device.executeShellCommand( + "cp -R $extDir/$BASELINE_BACKUP_FOLDER/" + + "recovery-code.txt $extDir" + ) } if (backupFile.exists()) { diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt index 3223aa52..4c5e3b6c 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt @@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e import android.content.pm.PackageInfo import com.stevesoltys.seedvault.metadata.PackageMetadata +import com.stevesoltys.seedvault.restore.AppRestoreResult /** * 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 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 restoreResults: Map<String, AppRestoreResult?> = emptyMap(), val full: MutableMap<String, String>, val kv: MutableMap<String, MutableMap<String, String>>, val userApps: List<PackageInfo>, diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 3cdc3365..f3403de7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -9,7 +9,6 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build import android.os.ServiceManager.getService import android.os.StrictMode -import android.os.SystemProperties import android.os.UserManager import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.header.headerModule @@ -60,7 +59,6 @@ open class App : Application() { override fun onCreate() { super.onCreate() - SystemProperties.set(BACKUP_D2D_PROPERTY, "true") startKoin() if (isDebugBuild()) { StrictMode.setThreadPolicy( @@ -123,8 +121,6 @@ const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL const val ANCESTRAL_RECORD_KEY = "@ancestral_record@" const val GLOBAL_METADATA_KEY = "@meta@" -const val BACKUP_D2D_PROPERTY = "persist.backup.fake-d2d" - // TODO this doesn't work for LineageOS as they do public debug builds fun isDebugBuild() = Build.TYPE == "userdebug" diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt index c4808331..c8ad6aac 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt @@ -18,6 +18,7 @@ data class BackupMetadata( internal val androidVersion: Int = Build.VERSION.SDK_INT, internal val androidIncremental: String = Build.VERSION.INCREMENTAL, internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}", + internal var d2dBackup: Boolean = false, 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_INCREMENTAL = "incremental" internal const val JSON_METADATA_NAME = "name" +internal const val JSON_METADATA_D2D_BACKUP = "d2d_backup" enum class PackageState { /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt index 98a57f4a..f58a29a1 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -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.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.isSystemApp import java.io.FileNotFoundException import java.io.IOException @@ -35,6 +36,7 @@ internal class MetadataManager( private val crypto: Crypto, private val metadataWriter: MetadataWriter, private val metadataReader: MetadataReader, + private val settingsManager: SettingsManager ) { private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "") @@ -135,6 +137,8 @@ internal class MetadataManager( modifyMetadata(metadataOutputStream) { val now = clock.time() metadata.time = now + metadata.d2dBackup = settingsManager.d2dBackupsEnabled() + if (metadata.packageMetadataMap.containsKey(packageName)) { metadata.packageMetadataMap[packageName]!!.time = now metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt index 68c723a4..0d7ed1a2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt @@ -4,7 +4,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val metadataModule = module { - single { MetadataManager(androidContext(), get(), get(), get(), get()) } + single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) } single<MetadataWriter> { MetadataWriterImpl(get()) } single<MetadataReader> { MetadataReaderImpl(get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt index 382a1757..bbd6df19 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt @@ -152,7 +152,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { androidVersion = meta.getInt(JSON_METADATA_SDK_INT), androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL), deviceName = meta.getString(JSON_METADATA_NAME), - packageMetadataMap = packageMetadataMap + d2dBackup = meta.optBoolean(JSON_METADATA_D2D_BACKUP, false), + packageMetadataMap = packageMetadataMap, ) } catch (e: JSONException) { throw SecurityException(e) diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt index 1359c112..bbed50c7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt @@ -35,6 +35,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { put(JSON_METADATA_SDK_INT, metadata.androidVersion) put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental) put(JSON_METADATA_NAME, metadata.deviceName) + put(JSON_METADATA_D2D_BACKUP, metadata.d2dBackup) }) } for ((packageName, packageMetadata) in metadata.packageMetadataMap) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt index f1c33c16..2c3abb3c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt @@ -23,6 +23,9 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) { val deviceName: String get() = backupMetadata.deviceName + val d2dBackup: Boolean + get() = backupMetadata.d2dBackup + val packageMetadataMap: PackageMetadataMap get() = backupMetadata.packageMetadataMap diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt index 29cb923c..e185b79b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt @@ -55,9 +55,16 @@ internal class AppListRetriever( @WorkerThread fun getAppList(): List<AppListItem> { - return listOf(AppSectionTitle(R.string.backup_section_system)) + getSpecialApps() + - listOf(AppSectionTitle(R.string.backup_section_user)) + getUserApps() + - listOf(AppSectionTitle(R.string.backup_section_not_allowed)) + getNotAllowedApps() + + val appListSections = linkedMapOf( + 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> { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt index f4a2effd..05607375 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.transport.backup.PackageService @@ -14,6 +15,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { private val viewModel: SettingsViewModel by sharedViewModel() private val packageService: PackageService by inject() + // TODO set mimeType when upgrading androidx lib private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri -> viewModel.onLogcatUriReceived(uri) @@ -23,6 +25,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { permitDiskReads { setPreferencesFromResource(R.xml.settings_expert, rootKey) } + findPreference<Preference>("logcat")?.setOnPreferenceClickListener { val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver" val timestamp = System.currentTimeMillis() @@ -30,6 +33,13 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { createFileLauncher.launch(name) true } + + val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS) + + d2dPreference?.setOnPreferenceChangeListener { _, newValue -> + d2dPreference.isChecked = newValue as Boolean + true + } } override fun onStart() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 93ed0883..38aaf805 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -89,7 +89,7 @@ class SettingsFragment : PreferenceFragmentCompat() { true } - autoRestore = findPreference("auto_restore")!! + autoRestore = findPreference(PREF_KEY_AUTO_RESTORE)!! autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> val enabled = newValue as Boolean try { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index a83c844f..2647e57b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentSkipListSet internal const val PREF_KEY_TOKEN = "token" 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_NAME = "storageName" @@ -31,6 +32,7 @@ private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist" private const val PREF_KEY_BACKUP_STORAGE = "backup_storage" private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota" +internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups" class SettingsManager(private val context: Context) { @@ -151,6 +153,14 @@ class SettingsManager(private val context: Context) { } 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( diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt index a90b14b0..cead1eab 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt @@ -12,6 +12,7 @@ import android.os.ParcelFileDescriptor import android.util.Log import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.settings.SettingsActivity +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import kotlinx.coroutines.runBlocking @@ -21,9 +22,8 @@ import org.koin.core.component.inject // If we ever change this, we should use a ComponentName like the other backup transports. val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name -// Since there seems to be consensus in the community to pose as device-to-device transport, -// we are pretending to be one here. This will back up opt-out apps that target at least API 31. -const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED or FLAG_DEVICE_TO_DEVICE_TRANSFER +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 = "com.stevesoltys.seedvault.transport.ConfigurableBackupTransport" @@ -38,6 +38,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont private val backupCoordinator by inject<BackupCoordinator>() private val restoreCoordinator by inject<RestoreCoordinator>() + private val settingsManager by inject<SettingsManager>() override fun transportDirName(): String { return TRANSPORT_DIRECTORY_NAME @@ -57,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. */ override fun getTransportFlags(): Int { - return TRANSPORT_FLAGS + return if (settingsManager.d2dBackupsEnabled()) { + D2D_TRANSPORT_FLAGS + } else { + DEFAULT_TRANSPORT_FLAGS + } } /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt index 14fd6261..bf7d3272 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt @@ -8,6 +8,7 @@ val backupModule = module { single { PackageService( context = androidContext(), + backupManager = get(), settingsManager = get(), plugin = get() ) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt index d298856e..ca2e8ab0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt @@ -1,5 +1,6 @@ package com.stevesoltys.seedvault.transport.backup +import android.app.backup.IBackupManager import android.content.Context import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP import android.content.pm.ApplicationInfo.FLAG_STOPPED @@ -11,6 +12,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_INSTRUMENTATION import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.os.RemoteException +import android.os.UserHandle import android.util.Log import android.util.Log.INFO import androidx.annotation.WorkerThread @@ -28,11 +30,13 @@ private const val LOG_MAX_PACKAGES = 100 */ internal class PackageService( private val context: Context, + private val backupManager: IBackupManager, private val settingsManager: SettingsManager, private val plugin: StoragePlugin, ) { private val packageManager: PackageManager = context.packageManager + private val myUserId = UserHandle.myUserId() val eligiblePackages: Array<String> @WorkerThread @@ -48,13 +52,7 @@ internal class PackageService( logPackages(packages) } - // We do not use BackupManager.filterAppsEligibleForBackupForUser 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. - val eligibleApps = packages.filter(::shouldIncludeAppInBackup) + val eligibleApps = packages.filter(::shouldIncludeAppInBackup).toTypedArray() // log eligible packages if (Log.isLoggable(TAG, INFO)) { @@ -97,7 +95,8 @@ internal class PackageService( val userApps: List<PackageInfo> @WorkerThread get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo -> - packageInfo.isUserVisible(context) && packageInfo.allowsBackup() + packageInfo.isUserVisible(context) && + packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) } /** @@ -106,7 +105,8 @@ internal class PackageService( val userNotAllowedApps: List<PackageInfo> @WorkerThread get() = packageManager.getInstalledPackages(0).filter { packageInfo -> - !packageInfo.allowsBackup() && !packageInfo.isSystemApp() + !packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) && + !packageInfo.isSystemApp() } val expectedAppTotals: ExpectedAppTotals @@ -132,11 +132,24 @@ internal class PackageService( } 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 also need to exclude the DocumentsProvider used to store backup data. - // Otherwise, it gets killed when we back it up, terminating our backup. - return enabled && packageName != plugin.providerPackageName + + // 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>) { @@ -168,18 +181,24 @@ internal fun PackageInfo.isSystemApp(): Boolean { return applicationInfo.flags and FLAG_SYSTEM != 0 } -internal fun PackageInfo.allowsBackup(): Boolean { +internal fun PackageInfo.allowsBackup(d2dBackup: Boolean): Boolean { if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false - // 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. + return if (d2dBackup) { + // 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. - // Now that we have switched to 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). - return true + // 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 + } } /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index f6a00ecc..68431258 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -22,7 +22,8 @@ import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.settings.SettingsManager -import com.stevesoltys.seedvault.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 java.io.IOException @@ -32,7 +33,7 @@ import java.io.IOException * 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 DEVICE_NAME_FOR_D2D_SET = "D2D" +internal const val D2D_DEVICE_NAME = "D2D" private data class RestoreCoordinatorState( val token: Long, @@ -100,8 +101,20 @@ internal class RestoreCoordinator( **/ suspend fun getAvailableRestoreSets(): Array<RestoreSet>? { return getAvailableMetadata()?.map { (_, metadata) -> - RestoreSet(metadata.deviceName /* name */, DEVICE_NAME_FOR_D2D_SET /* device */, - 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() } @@ -123,6 +136,10 @@ internal class RestoreCoordinator( */ fun beforeStartRestore(backupMetadata: BackupMetadata) { this.backupMetadata = backupMetadata + + if (backupMetadata.d2dBackup) { + settingsManager.setD2dBackupsEnabled(true) + } } /** @@ -228,6 +245,7 @@ internal class RestoreCoordinator( TYPE_KEY_VALUE } else throw IOException("No data found for $packageName. Skipping.") } + BackupType.FULL -> { val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) if (plugin.hasData(state.token, name)) { @@ -237,6 +255,7 @@ internal class RestoreCoordinator( TYPE_FULL_STREAM } else throw IOException("No data found for $packageName. Skipping...") } + null -> { Log.i(TAG, "No backup type found for $packageName. Skipping...") state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s -> @@ -270,12 +289,14 @@ internal class RestoreCoordinator( state.currentPackage = packageName TYPE_KEY_VALUE } + full.hasDataForPackage(state.token, packageInfo) -> { Log.i(TAG, "Found full backup data for $packageName.") full.initializeState(0x00, state.token, "", packageInfo) state.currentPackage = packageName TYPE_FULL_STREAM } + else -> { Log.i(TAG, "No data found for $packageName. Skipping.") return nextRestorePackage() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 502ba39c..18f8326f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,8 @@ <string name="settings_expert_title">Expert settings</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_d2d_title">Device-to-device backups</string> + <string name="settings_expert_d2d_summary">Tell AOSP that our backups are being used for a D2D transfer. This forces backups for most apps, even when they disallow them.\n\nWarning: This is experimental, use at your own risk.\n\nSee more info on our FAQ.</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_error">Error: Could not save app log</string> diff --git a/app/src/main/res/xml/settings_expert.xml b/app/src/main/res/xml/settings_expert.xml index 11e8497c..0125bf4e 100644 --- a/app/src/main/res/xml/settings_expert.xml +++ b/app/src/main/res/xml/settings_expert.xml @@ -5,6 +5,12 @@ android:key="unlimited_quota" android:summary="@string/settings_expert_quota_summary" 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 android:icon="@drawable/ic_bug_report" android:key="logcat" diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt index b1ad45a5..cdf03aea 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt @@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.metadata.metadataModule import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule 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.restore.restoreModule import org.koin.android.ext.koin.androidContext @@ -25,6 +26,7 @@ class TestApp : App() { } private val appModule = module { single { Clock() } + single { SettingsManager(this@TestApp) } } override fun startKoin() = startKoin { diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt index 661677ab..521aac65 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -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.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED +import com.stevesoltys.seedvault.settings.SettingsManager import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -27,6 +28,7 @@ import io.mockk.verify import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.fail +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.koin.core.context.stopKoin @@ -51,8 +53,16 @@ class MetadataManagerTest { private val crypto: Crypto = mockk() private val metadataWriter: MetadataWriter = 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 token = Random.nextLong() @@ -69,6 +79,11 @@ class MetadataManagerTest { private val cacheInputStream: FileInputStream = mockk() private val encodedMetadata = getRandomByteArray() + @Before + fun beforeEachTest() { + every { settingsManager.d2dBackupsEnabled() } returns false + } + @After fun afterEachTest() { stopKoin() diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt index 35e81b28..0af1caa2 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt @@ -59,6 +59,10 @@ internal abstract class TransportTest { put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV)) } ) + protected val d2dMetadata = metadata.copy( + d2dBackup = true + ) + protected val salt = metadata.salt protected val name = getRandomString(12) protected val name2 = getRandomString(23) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index 4f704dee..88ba4c15 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -87,9 +87,18 @@ internal class RestoreCoordinatorTest : TransportTest() { val sets = restore.getAvailableRestoreSets() ?: fail() assertEquals(2, sets.size) - assertEquals(DEVICE_NAME_FOR_D2D_SET, sets[0].device) + assertEquals(metadata.deviceName, sets[0].device) assertEquals(metadata.deviceName, sets[0].name) 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