1
0
Fork 0

Add experimental support for forcing D2D transfer backups

This commit is contained in:
Steve Soltys 2023-12-30 15:53:35 -05:00
parent 57148943ad
commit fa4c52fb83
30 changed files with 200 additions and 54 deletions

View file

@ -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

View file

@ -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()

View file

@ -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 {

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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(

View file

@ -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()) {

View file

@ -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>,

View file

@ -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"

View file

@ -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 {
/**

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

View file

@ -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()) }
}

View file

@ -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)

View file

@ -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) {

View file

@ -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

View file

@ -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> {

View file

@ -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() {

View file

@ -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 {

View file

@ -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(

View file

@ -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
}
}
/**

View file

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

View file

@ -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
}
}
/**

View file

@ -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()

View file

@ -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>

View file

@ -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"

View file

@ -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 {

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.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()

View file

@ -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)

View file

@ -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