Merge pull request #562 from seedvault-app/feature/d2d-transfer
Add experimental support for forcing D2D transfer backups
This commit is contained in:
commit
0319d733c1
34 changed files with 342 additions and 69 deletions
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
|
||||
|
||||
|
|
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
|
@ -1,10 +1,15 @@
|
|||
name: Build
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
@ -40,3 +45,10 @@ jobs:
|
|||
app/build/outputs/apk/debug/app-debug.apk
|
||||
contactsbackup/build/outputs/apk/debug/contactsbackup-debug.apk
|
||||
storage/demo/build/outputs/apk/debug/demo-debug.apk
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure()
|
||||
with:
|
||||
report_paths: '**/build/test-results/**/TEST-*.xml'
|
||||
|
||||
|
|
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,9 +1,11 @@
|
|||
package com.stevesoltys.seedvault
|
||||
|
||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
||||
import com.stevesoltys.seedvault.transport.backup.KVBackup
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
||||
|
@ -25,6 +27,9 @@ class KoinInstrumentationTestApp : App() {
|
|||
val testModule = module {
|
||||
val context = this@KoinInstrumentationTestApp
|
||||
|
||||
single { spyk(PackageService(context, get(), get(), get())) }
|
||||
single { spyk(SettingsManager(context)) }
|
||||
|
||||
single { spyk(BackupNotificationManager(context)) }
|
||||
single { spyk(FullBackup(get(), get(), get(), get())) }
|
||||
single { spyk(KVBackup(get(), get(), get(), get(), get())) }
|
||||
|
|
|
@ -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,17 @@ 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)
|
||||
|
||||
} else {
|
||||
println("Disabling D2D backups for test")
|
||||
settingsManager.setD2dBackupsEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -63,10 +75,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>,
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.settings.AppStatus
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
@ -14,10 +22,41 @@ class PackageServiceTest : KoinComponent {
|
|||
|
||||
private val packageService: PackageService by inject()
|
||||
|
||||
private val settingsManager: SettingsManager by inject()
|
||||
|
||||
private val storagePlugin: StoragePlugin by inject()
|
||||
|
||||
@Test
|
||||
fun testNotAllowedPackages() {
|
||||
val packages = packageService.notBackedUpPackages
|
||||
Log.e("TEST", "Packages: $packages")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldIncludeAppInBackup exempts plugin provider and blacklisted apps`() {
|
||||
val packageInfo = PackageInfo().apply {
|
||||
packageName = "com.example"
|
||||
}
|
||||
|
||||
val disabledAppStatus = mockk<AppStatus>().apply {
|
||||
every { packageName } returns packageInfo.packageName
|
||||
every { enabled } returns false
|
||||
}
|
||||
settingsManager.onAppBackupStatusChanged(disabledAppStatus)
|
||||
|
||||
// Should not backup blacklisted apps
|
||||
assertFalse(packageService.shouldIncludeAppInBackup(packageInfo.packageName))
|
||||
|
||||
val enabledAppStatus = mockk<AppStatus>().apply {
|
||||
every { packageName } returns packageInfo.packageName
|
||||
every { enabled } returns true
|
||||
}
|
||||
settingsManager.onAppBackupStatusChanged(enabledAppStatus)
|
||||
|
||||
// Should backup non-blacklisted apps
|
||||
assertTrue(packageService.shouldIncludeAppInBackup(packageInfo.packageName))
|
||||
|
||||
// Should not backup storage provider
|
||||
assertFalse(packageService.shouldIncludeAppInBackup(storagePlugin.providerPackageName!!))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,25 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
|||
createFileLauncher.launch(name)
|
||||
true
|
||||
}
|
||||
|
||||
val quotaPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_UNLIMITED_QUOTA)
|
||||
|
||||
quotaPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||
quotaPreference.isChecked = newValue as Boolean
|
||||
true
|
||||
}
|
||||
|
||||
val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
|
||||
|
||||
d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||
d2dPreference.isChecked = newValue as Boolean
|
||||
|
||||
// automatically enable unlimited quota when enabling D2D backups
|
||||
if (d2dPreference.isChecked) {
|
||||
quotaPreference?.isChecked = true
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
@ -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"
|
||||
|
@ -30,7 +31,8 @@ private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
|
|||
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
||||
|
||||
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
||||
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
||||
internal const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
||||
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
|
||||
|
||||
class SettingsManager(private val context: Context) {
|
||||
|
||||
|
@ -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(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.stevesoltys.seedvault.transport
|
||||
|
||||
import android.app.backup.BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
||||
import android.app.backup.BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER
|
||||
import android.app.backup.BackupTransport
|
||||
import android.app.backup.RestoreDescription
|
||||
import android.app.backup.RestoreSet
|
||||
|
@ -11,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
|
||||
|
@ -20,7 +22,8 @@ import org.koin.core.component.inject
|
|||
// If we ever change this, we should use a ComponentName like the other backup transports.
|
||||
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
|
||||
|
||||
const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
||||
const val DEFAULT_TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
||||
const val D2D_TRANSPORT_FLAGS = DEFAULT_TRANSPORT_FLAGS or FLAG_DEVICE_TO_DEVICE_TRANSFER
|
||||
|
||||
private const val TRANSPORT_DIRECTORY_NAME =
|
||||
"com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
|
||||
|
@ -35,6 +38,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
|||
|
||||
private val backupCoordinator by inject<BackupCoordinator>()
|
||||
private val restoreCoordinator by inject<RestoreCoordinator>()
|
||||
private val settingsManager by inject<SettingsManager>()
|
||||
|
||||
override fun transportDirName(): String {
|
||||
return TRANSPORT_DIRECTORY_NAME
|
||||
|
@ -54,7 +58,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
|||
* This allows the agent to decide what to do based on properties of the transport.
|
||||
*/
|
||||
override fun getTransportFlags(): Int {
|
||||
return TRANSPORT_FLAGS
|
||||
return if (settingsManager.d2dBackupsEnabled()) {
|
||||
D2D_TRANSPORT_FLAGS
|
||||
} else {
|
||||
DEFAULT_TRANSPORT_FLAGS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -143,12 +143,9 @@ internal class BackupCoordinator(
|
|||
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
||||
): Boolean {
|
||||
val packageName = targetPackage.packageName
|
||||
// Check that the app is not blacklisted by the user
|
||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.")
|
||||
// We need to exclude the DocumentsProvider used to store backup data.
|
||||
// Otherwise, it gets killed when we back it up, terminating our backup.
|
||||
return enabled && targetPackage.packageName != plugin.providerPackageName
|
||||
val shouldInclude = packageService.shouldIncludeAppInBackup(packageName)
|
||||
if (!shouldInclude) Log.i(TAG, "Excluding $packageName from backup.")
|
||||
return shouldInclude
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,9 @@ val backupModule = module {
|
|||
single {
|
||||
PackageService(
|
||||
context = androidContext(),
|
||||
backupManager = get()
|
||||
backupManager = get(),
|
||||
settingsManager = get(),
|
||||
plugin = get()
|
||||
)
|
||||
}
|
||||
single {
|
||||
|
|
|
@ -17,6 +17,8 @@ import android.util.Log
|
|||
import android.util.Log.INFO
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
||||
private val TAG = PackageService::class.java.simpleName
|
||||
|
||||
|
@ -29,6 +31,8 @@ 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
|
||||
|
@ -45,13 +49,16 @@ internal class PackageService(
|
|||
// log packages
|
||||
if (Log.isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Got ${packages.size} packages:")
|
||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
||||
Log.i(TAG, it.toString())
|
||||
}
|
||||
logPackages(packages)
|
||||
}
|
||||
|
||||
val eligibleApps =
|
||||
val eligibleApps = if (settingsManager.d2dBackupsEnabled()) {
|
||||
// if D2D is enabled, use the "new method" for filtering packages
|
||||
packages.filter(::shouldIncludeAppInBackup).toTypedArray()
|
||||
} else {
|
||||
// otherwise, use the BackupManager call.
|
||||
backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray())
|
||||
}
|
||||
|
||||
// log eligible packages
|
||||
if (Log.isLoggable(TAG, INFO)) {
|
||||
|
@ -66,6 +73,9 @@ internal class PackageService(
|
|||
return packageArray.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of packages that will not be backed up.
|
||||
*/
|
||||
val notBackedUpPackages: List<PackageInfo>
|
||||
@WorkerThread
|
||||
get() {
|
||||
|
@ -94,16 +104,23 @@ 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of apps that does not allow backup.
|
||||
* A list of apps that do not allow backup.
|
||||
*/
|
||||
val userNotAllowedApps: List<PackageInfo>
|
||||
@WorkerThread
|
||||
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
||||
!packageInfo.allowsBackup() && !packageInfo.isSystemApp()
|
||||
get() {
|
||||
// if D2D backups are enabled, all apps are allowed
|
||||
if (settingsManager.d2dBackupsEnabled()) return emptyList()
|
||||
|
||||
return packageManager.getInstalledPackages(0).filter { packageInfo ->
|
||||
!packageInfo.allowsBackup() &&
|
||||
!packageInfo.isSystemApp()
|
||||
}
|
||||
}
|
||||
|
||||
val expectedAppTotals: ExpectedAppTotals
|
||||
|
@ -128,12 +145,64 @@ internal class PackageService(
|
|||
null
|
||||
}
|
||||
|
||||
fun shouldIncludeAppInBackup(packageName: String): Boolean {
|
||||
// We do not use BackupManager.filterAppsEligibleForBackupForUser for D2D because it
|
||||
// always makes its determinations based on OperationType.BACKUP, never based on
|
||||
// OperationType.MIGRATION, and there are no alternative publicly-available APIs.
|
||||
// We don't need to use it, here, either; during a backup or migration, the system
|
||||
// will perform its own eligibility checks regardless. We merely need to filter out
|
||||
// apps that we, or the user, want to exclude.
|
||||
|
||||
// Check that the app is not excluded by user preference
|
||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||
|
||||
// We need to explicitly exclude DocumentsProvider and Seedvault.
|
||||
// Otherwise, they get killed while backing them up, terminating our backup.
|
||||
val excludedPackages = setOf(
|
||||
plugin.providerPackageName,
|
||||
context.packageName
|
||||
)
|
||||
|
||||
return enabled && !excludedPackages.contains(packageName)
|
||||
}
|
||||
|
||||
private fun logPackages(packages: List<String>) {
|
||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
||||
Log.i(TAG, it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun PackageInfo.allowsBackup(): Boolean {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||
|
||||
return if (settingsManager.d2dBackupsEnabled()) {
|
||||
/**
|
||||
* TODO: Consider ways of replicating the system's logic so that the user can have
|
||||
* advance knowledge of apps that the system will exclude, particularly apps targeting
|
||||
* SDK 30 or below.
|
||||
*
|
||||
* At backup time, the system will filter out any apps that *it* does not want to be
|
||||
* backed up. If the user has enabled D2D, *we* generally want to back up as much as
|
||||
* possible; part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup).
|
||||
* So, we return true.
|
||||
* See frameworks/base/services/backup/java/com/android/server/backup/utils/
|
||||
* BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
|
||||
*/
|
||||
true
|
||||
} else {
|
||||
applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag indicating whether or not this package should _not_ be backed up.
|
||||
*
|
||||
* This happens when the app has opted-out of backup, or when it is stopped.
|
||||
*/
|
||||
private fun PackageInfo.doesNotGetBackedUp(): Boolean {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
|
||||
return !allowsBackup() || isStopped()
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ExpectedAppTotals(
|
||||
|
@ -157,11 +226,6 @@ internal fun PackageInfo.isSystemApp(): Boolean {
|
|||
return applicationInfo.flags and FLAG_SYSTEM != 0
|
||||
}
|
||||
|
||||
internal fun PackageInfo.allowsBackup(): Boolean {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a system app that hasn't been updated.
|
||||
* We don't back up those APKs.
|
||||
|
@ -173,12 +237,6 @@ internal fun PackageInfo.isNotUpdatedSystemApp(): Boolean {
|
|||
return isSystemApp && !isUpdatedSystemApp
|
||||
}
|
||||
|
||||
internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
|
||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup
|
||||
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
|
||||
}
|
||||
|
||||
internal fun PackageInfo.isStopped(): Boolean {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||
return applicationInfo.flags and FLAG_STOPPED != 0
|
||||
|
|
|
@ -22,10 +22,19 @@ 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
|
||||
|
||||
/**
|
||||
* Device name used in AOSP to indicate that a restore set is part of a device-to-device migration.
|
||||
* See getBackupEligibilityRules in frameworks/base/services/backup/java/com/android/server/
|
||||
* backup/restore/ActiveRestoreSession.java. AOSP currently relies on this constant, and it is not
|
||||
* publicly exposed. Framework code indicates they intend to use a flag, instead, in the future.
|
||||
*/
|
||||
internal const val D2D_DEVICE_NAME = "D2D"
|
||||
|
||||
private data class RestoreCoordinatorState(
|
||||
val token: Long,
|
||||
val packages: Iterator<PackageInfo>,
|
||||
|
@ -92,7 +101,20 @@ internal class RestoreCoordinator(
|
|||
**/
|
||||
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||
return getAvailableMetadata()?.map { (_, metadata) ->
|
||||
RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token, TRANSPORT_FLAGS)
|
||||
|
||||
val transportFlags = if (metadata.d2dBackup) {
|
||||
D2D_TRANSPORT_FLAGS
|
||||
} else {
|
||||
DEFAULT_TRANSPORT_FLAGS
|
||||
}
|
||||
|
||||
val deviceName = if (metadata.d2dBackup) {
|
||||
D2D_DEVICE_NAME
|
||||
} else {
|
||||
metadata.deviceName
|
||||
}
|
||||
|
||||
RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
|
||||
}?.toTypedArray()
|
||||
}
|
||||
|
||||
|
@ -114,6 +136,10 @@ internal class RestoreCoordinator(
|
|||
*/
|
||||
fun beforeStartRestore(backupMetadata: BackupMetadata) {
|
||||
this.backupMetadata = backupMetadata
|
||||
|
||||
if (backupMetadata.d2dBackup) {
|
||||
settingsManager.setD2dBackupsEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -219,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)) {
|
||||
|
@ -228,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 ->
|
||||
|
@ -261,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">This forces backups for most apps, even when they disallow them. This is experimental, use at your own risk.\n\n1. To backup apps in D2D mode, you will need to run \"Backup now\" manually.\n\n2. Android may overwrite D2D backups for apps which normally allow backups.</string>
|
||||
<string name="settings_expert_logcat_title">Save app log</string>
|
||||
<string name="settings_expert_logcat_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
|
||||
|
@ -26,7 +27,10 @@ import io.mockk.mockk
|
|||
import io.mockk.verify
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
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 +55,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 +81,11 @@ class MetadataManagerTest {
|
|||
private val cacheInputStream: FileInputStream = mockk()
|
||||
private val encodedMetadata = getRandomByteArray()
|
||||
|
||||
@Before
|
||||
fun beforeEachTest() {
|
||||
every { settingsManager.d2dBackupsEnabled() } returns false
|
||||
}
|
||||
|
||||
@After
|
||||
fun afterEachTest() {
|
||||
stopKoin()
|
||||
|
@ -246,6 +263,23 @@ class MetadataManagerTest {
|
|||
manager.getPackageMetadata(packageName)
|
||||
)
|
||||
assertEquals(time, manager.getLastBackupTime())
|
||||
assertFalse(updatedMetadata.d2dBackup)
|
||||
|
||||
verify {
|
||||
cacheInputStream.close()
|
||||
cacheOutputStream.close()
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun `test onPackageBackedUp() with D2D enabled`() {
|
||||
expectReadFromCache()
|
||||
every { clock.time() } returns time
|
||||
expectModifyMetadata(initialMetadata)
|
||||
|
||||
every { settingsManager.d2dBackupsEnabled() } returns true
|
||||
|
||||
manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
|
||||
assertTrue(initialMetadata.d2dBackup)
|
||||
|
||||
verify {
|
||||
cacheInputStream.close()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -32,8 +32,6 @@ import io.mockk.mockk
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
@ -46,8 +44,8 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
private val kv = mockk<KVBackup>()
|
||||
private val full = mockk<FullBackup>()
|
||||
private val apkBackup = mockk<ApkBackup>()
|
||||
private val packageService: PackageService = mockk()
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val packageService = mockk<PackageService>()
|
||||
|
||||
private val backup = BackupCoordinator(
|
||||
context,
|
||||
|
@ -170,20 +168,6 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
verify { metadataOutputStream.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAppEligibleForBackup() exempts plugin provider and blacklisted apps`() {
|
||||
every {
|
||||
settingsManager.isBackupEnabled(packageInfo.packageName)
|
||||
} returns true andThen false andThen true
|
||||
every {
|
||||
plugin.providerPackageName
|
||||
} returns packageInfo.packageName andThen "new.package" andThen "new.package"
|
||||
|
||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
assertTrue(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearing KV backup data throws`() = runBlocking {
|
||||
every { settingsManager.getToken() } returns token
|
||||
|
|
|
@ -90,6 +90,15 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
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…
Reference in a new issue