Add experimental support for forcing D2D transfer backups
This commit is contained in:
parent
57148943ad
commit
fa4c52fb83
30 changed files with 200 additions and 54 deletions
.github
app
build.gradle.kts
development/scripts
src
androidTest/java/com/stevesoltys/seedvault/e2e
LargeBackupTestBase.ktLargeRestoreTestBase.ktLargeTestBase.ktSeedvaultLargeTest.ktSeedvaultLargeTestResult.kt
main
test/java/com/stevesoltys/seedvault
4
.github/scripts/run_tests.sh
vendored
4
.github/scripts/run_tests.sh
vendored
|
@ -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
|
||||
|
||||
|
|
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ val backupModule = module {
|
|||
single {
|
||||
PackageService(
|
||||
context = androidContext(),
|
||||
backupManager = get(),
|
||||
settingsManager = get(),
|
||||
plugin = get()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue