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
|
sleep 10
|
||||||
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
|
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
|
||||||
|
|
||||||
|
D2D_BACKUP_TEST=$1
|
||||||
|
|
||||||
large_test_exit_code=0
|
large_test_exit_code=0
|
||||||
./gradlew --stacktrace -Pinstrumented_test_size=large :app:connectedAndroidTest || large_test_exit_code=$?
|
./gradlew --stacktrace -Pinstrumented_test_size=large -Pd2d_backup_test="$D2D_BACKUP_TEST" :app:connectedAndroidTest || large_test_exit_code=$?
|
||||||
|
|
||||||
adb pull /sdcard/seedvault_test_results
|
adb pull /sdcard/seedvault_test_results
|
||||||
|
|
||||||
|
|
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
|
@ -1,10 +1,15 @@
|
||||||
name: Build
|
name: Build
|
||||||
on: [push, pull_request]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
checks: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
|
@ -40,3 +45,10 @@ jobs:
|
||||||
app/build/outputs/apk/debug/app-debug.apk
|
app/build/outputs/apk/debug/app-debug.apk
|
||||||
contactsbackup/build/outputs/apk/debug/contactsbackup-debug.apk
|
contactsbackup/build/outputs/apk/debug/contactsbackup-debug.apk
|
||||||
storage/demo/build/outputs/apk/debug/demo-debug.apk
|
storage/demo/build/outputs/apk/debug/demo-debug.apk
|
||||||
|
|
||||||
|
- name: Publish Test Report
|
||||||
|
uses: mikepenz/action-junit-report@v4
|
||||||
|
if: success() || failure()
|
||||||
|
with:
|
||||||
|
report_paths: '**/build/test-results/**/TEST-*.xml'
|
||||||
|
|
||||||
|
|
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -20,6 +20,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
android_target: [ 33, 34 ]
|
android_target: [ 33, 34 ]
|
||||||
emulator_type: [ default ]
|
emulator_type: [ default ]
|
||||||
|
d2d_backup_test: [ true, false ]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -52,7 +53,7 @@ jobs:
|
||||||
disable-animations: true
|
disable-animations: true
|
||||||
script: |
|
script: |
|
||||||
./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64"
|
./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64"
|
||||||
./.github/scripts/run_tests.sh
|
./.github/scripts/run_tests.sh ${{ matrix.d2d_backup_test }}
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: always()
|
if: always()
|
||||||
|
|
|
@ -24,14 +24,17 @@ android {
|
||||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||||
versionNameSuffix = "-${gitDescribe()}"
|
versionNameSuffix = "-${gitDescribe()}"
|
||||||
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
|
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
|
||||||
testInstrumentationRunnerArguments(mapOf("disableAnalytics" to "true"))
|
testInstrumentationRunnerArguments["disableAnalytics"] = "true"
|
||||||
|
|
||||||
if (project.hasProperty("instrumented_test_size")) {
|
if (project.hasProperty("instrumented_test_size")) {
|
||||||
val testSize = project.property("instrumented_test_size").toString()
|
val testSize = project.property("instrumented_test_size").toString()
|
||||||
println("Instrumented test size: $testSize")
|
println("Instrumented test size: $testSize")
|
||||||
|
|
||||||
testInstrumentationRunnerArguments(mapOf("size" to testSize))
|
testInstrumentationRunnerArguments["size"] = testSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val d2dBackupTest = project.findProperty("d2d_backup_test")?.toString() ?: "true"
|
||||||
|
testInstrumentationRunnerArguments["d2d_backup_test"] = d2dBackupTest
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
|
@ -84,7 +84,7 @@ echo "Downloading and extracting test backup to '/sdcard/seedvault_baseline'..."
|
||||||
|
|
||||||
if [ ! -f backup.tar.gz ]; then
|
if [ ! -f backup.tar.gz ]; then
|
||||||
echo "Downloading test backup..."
|
echo "Downloading test backup..."
|
||||||
wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/1/backup.tar.gz
|
wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/3/backup.tar.gz
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$ADB root
|
$ADB root
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package com.stevesoltys.seedvault
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
||||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackup
|
import com.stevesoltys.seedvault.transport.backup.KVBackup
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
||||||
|
@ -25,6 +27,9 @@ class KoinInstrumentationTestApp : App() {
|
||||||
val testModule = module {
|
val testModule = module {
|
||||||
val context = this@KoinInstrumentationTestApp
|
val context = this@KoinInstrumentationTestApp
|
||||||
|
|
||||||
|
single { spyk(PackageService(context, get(), get(), get())) }
|
||||||
|
single { spyk(SettingsManager(context)) }
|
||||||
|
|
||||||
single { spyk(BackupNotificationManager(context)) }
|
single { spyk(BackupNotificationManager(context)) }
|
||||||
single { spyk(FullBackup(get(), get(), get(), get())) }
|
single { spyk(FullBackup(get(), get(), get(), get())) }
|
||||||
single { spyk(KVBackup(get(), get(), get(), get(), get())) }
|
single { spyk(KVBackup(get(), get(), get(), get(), get())) }
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.e2e
|
package com.stevesoltys.seedvault.e2e
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
|
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
|
||||||
|
@ -26,8 +25,6 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
private const val BACKUP_TIMEOUT = 360 * 1000L
|
private const val BACKUP_TIMEOUT = 360 * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupManager: IBackupManager get() = get()
|
|
||||||
|
|
||||||
val spyBackupNotificationManager: BackupNotificationManager get() = get()
|
val spyBackupNotificationManager: BackupNotificationManager get() = get()
|
||||||
|
|
||||||
val spyFullBackup: FullBackup get() = get()
|
val spyFullBackup: FullBackup get() = get()
|
||||||
|
|
|
@ -173,6 +173,10 @@ internal interface LargeRestoreTestBase : LargeTestBase {
|
||||||
coEvery {
|
coEvery {
|
||||||
spyFullRestore.initializeState(any(), any(), any(), any())
|
spyFullRestore.initializeState(any(), any(), any(), any())
|
||||||
} answers {
|
} answers {
|
||||||
|
packageName?.let {
|
||||||
|
restoreResult.full[it] = dataIntercept.toByteArray().sha256()
|
||||||
|
}
|
||||||
|
|
||||||
packageName = arg<PackageInfo>(3).packageName
|
packageName = arg<PackageInfo>(3).packageName
|
||||||
dataIntercept = ByteArrayOutputStream()
|
dataIntercept = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.stevesoltys.seedvault.e2e
|
package com.stevesoltys.seedvault.e2e
|
||||||
|
|
||||||
import android.app.UiAutomation
|
import android.app.UiAutomation
|
||||||
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
@ -72,6 +73,8 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
val spyMetadataManager: MetadataManager get() = get()
|
val spyMetadataManager: MetadataManager get() = get()
|
||||||
|
|
||||||
|
val backupManager: IBackupManager get() = get()
|
||||||
|
|
||||||
val spyRestoreViewModel: RestoreViewModel
|
val spyRestoreViewModel: RestoreViewModel
|
||||||
get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null")
|
get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null")
|
||||||
|
|
||||||
|
@ -79,6 +82,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null")
|
get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null")
|
||||||
|
|
||||||
fun resetApplicationState() {
|
fun resetApplicationState() {
|
||||||
|
backupManager.setAutoRestore(false)
|
||||||
settingsManager.setNewToken(null)
|
settingsManager.setNewToken(null)
|
||||||
documentsStorage.reset(null)
|
documentsStorage.reset(null)
|
||||||
|
|
||||||
|
@ -95,6 +99,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDocumentPickerAppData()
|
clearDocumentPickerAppData()
|
||||||
|
device.executeShellCommand("rm -R $externalStorageDir/.SeedVaultAndroidBackup")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun waitUntilIdle() {
|
fun waitUntilIdle() {
|
||||||
|
@ -157,6 +162,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
fun clearTestBackups() {
|
fun clearTestBackups() {
|
||||||
File(testStoragePath).deleteRecursively()
|
File(testStoragePath).deleteRecursively()
|
||||||
|
File(testVideoPath).deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeBackupLocation(
|
fun changeBackupLocation(
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -40,6 +41,17 @@ internal abstract class SeedvaultLargeTest :
|
||||||
|
|
||||||
startRecordingTest(keepRecordingScreen, name.methodName)
|
startRecordingTest(keepRecordingScreen, name.methodName)
|
||||||
restoreBaselineBackup()
|
restoreBaselineBackup()
|
||||||
|
|
||||||
|
val arguments = InstrumentationRegistry.getArguments()
|
||||||
|
|
||||||
|
if (arguments.getString("d2d_backup_test") == "true") {
|
||||||
|
println("Enabling D2D backups for test")
|
||||||
|
settingsManager.setD2dBackupsEnabled(true)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
println("Disabling D2D backups for test")
|
||||||
|
settingsManager.setD2dBackupsEnabled(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -63,10 +75,14 @@ internal abstract class SeedvaultLargeTest :
|
||||||
val extDir = externalStorageDir
|
val extDir = externalStorageDir
|
||||||
|
|
||||||
device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup")
|
device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup")
|
||||||
device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
|
device.executeShellCommand(
|
||||||
".SeedVaultAndroidBackup $extDir")
|
"cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
|
||||||
device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
|
".SeedVaultAndroidBackup $extDir"
|
||||||
"recovery-code.txt $extDir")
|
)
|
||||||
|
device.executeShellCommand(
|
||||||
|
"cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
|
||||||
|
"recovery-code.txt $extDir"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backupFile.exists()) {
|
if (backupFile.exists()) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains maps of (package name -> SHA-256 hashes) of application data.
|
* Contains maps of (package name -> SHA-256 hashes) of application data.
|
||||||
|
@ -12,8 +13,9 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
* For full backups, the mapping is: Map<PackageName, SHA-256>
|
* For full backups, the mapping is: Map<PackageName, SHA-256>
|
||||||
* For K/V backups, the mapping is: Map<PackageName, Map<Key, SHA-256>>
|
* For K/V backups, the mapping is: Map<PackageName, Map<Key, SHA-256>>
|
||||||
*/
|
*/
|
||||||
data class SeedvaultLargeTestResult(
|
internal data class SeedvaultLargeTestResult(
|
||||||
val backupResults: Map<String, PackageMetadata?> = emptyMap(),
|
val backupResults: Map<String, PackageMetadata?> = emptyMap(),
|
||||||
|
val restoreResults: Map<String, AppRestoreResult?> = emptyMap(),
|
||||||
val full: MutableMap<String, String>,
|
val full: MutableMap<String, String>,
|
||||||
val kv: MutableMap<String, MutableMap<String, String>>,
|
val kv: MutableMap<String, MutableMap<String, String>>,
|
||||||
val userApps: List<PackageInfo>,
|
val userApps: List<PackageInfo>,
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.settings.AppStatus
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
@ -14,10 +22,41 @@ class PackageServiceTest : KoinComponent {
|
||||||
|
|
||||||
private val packageService: PackageService by inject()
|
private val packageService: PackageService by inject()
|
||||||
|
|
||||||
|
private val settingsManager: SettingsManager by inject()
|
||||||
|
|
||||||
|
private val storagePlugin: StoragePlugin by inject()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNotAllowedPackages() {
|
fun testNotAllowedPackages() {
|
||||||
val packages = packageService.notBackedUpPackages
|
val packages = packageService.notBackedUpPackages
|
||||||
Log.e("TEST", "Packages: $packages")
|
Log.e("TEST", "Packages: $packages")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `shouldIncludeAppInBackup exempts plugin provider and blacklisted apps`() {
|
||||||
|
val packageInfo = PackageInfo().apply {
|
||||||
|
packageName = "com.example"
|
||||||
|
}
|
||||||
|
|
||||||
|
val disabledAppStatus = mockk<AppStatus>().apply {
|
||||||
|
every { packageName } returns packageInfo.packageName
|
||||||
|
every { enabled } returns false
|
||||||
|
}
|
||||||
|
settingsManager.onAppBackupStatusChanged(disabledAppStatus)
|
||||||
|
|
||||||
|
// Should not backup blacklisted apps
|
||||||
|
assertFalse(packageService.shouldIncludeAppInBackup(packageInfo.packageName))
|
||||||
|
|
||||||
|
val enabledAppStatus = mockk<AppStatus>().apply {
|
||||||
|
every { packageName } returns packageInfo.packageName
|
||||||
|
every { enabled } returns true
|
||||||
|
}
|
||||||
|
settingsManager.onAppBackupStatusChanged(enabledAppStatus)
|
||||||
|
|
||||||
|
// Should backup non-blacklisted apps
|
||||||
|
assertTrue(packageService.shouldIncludeAppInBackup(packageInfo.packageName))
|
||||||
|
|
||||||
|
// Should not backup storage provider
|
||||||
|
assertFalse(packageService.shouldIncludeAppInBackup(storagePlugin.providerPackageName!!))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ data class BackupMetadata(
|
||||||
internal val androidVersion: Int = Build.VERSION.SDK_INT,
|
internal val androidVersion: Int = Build.VERSION.SDK_INT,
|
||||||
internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
|
internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
|
||||||
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
|
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
|
||||||
|
internal var d2dBackup: Boolean = false,
|
||||||
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
|
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ internal const val JSON_METADATA_TIME = "time"
|
||||||
internal const val JSON_METADATA_SDK_INT = "sdk_int"
|
internal const val JSON_METADATA_SDK_INT = "sdk_int"
|
||||||
internal const val JSON_METADATA_INCREMENTAL = "incremental"
|
internal const val JSON_METADATA_INCREMENTAL = "incremental"
|
||||||
internal const val JSON_METADATA_NAME = "name"
|
internal const val JSON_METADATA_NAME = "name"
|
||||||
|
internal const val JSON_METADATA_D2D_BACKUP = "d2d_backup"
|
||||||
|
|
||||||
enum class PackageState {
|
enum class PackageState {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.isSystemApp
|
import com.stevesoltys.seedvault.transport.backup.isSystemApp
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -35,6 +36,7 @@ internal class MetadataManager(
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
private val metadataWriter: MetadataWriter,
|
private val metadataWriter: MetadataWriter,
|
||||||
private val metadataReader: MetadataReader,
|
private val metadataReader: MetadataReader,
|
||||||
|
private val settingsManager: SettingsManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
|
private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
|
||||||
|
@ -135,6 +137,8 @@ internal class MetadataManager(
|
||||||
modifyMetadata(metadataOutputStream) {
|
modifyMetadata(metadataOutputStream) {
|
||||||
val now = clock.time()
|
val now = clock.time()
|
||||||
metadata.time = now
|
metadata.time = now
|
||||||
|
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
|
||||||
|
|
||||||
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
||||||
metadata.packageMetadataMap[packageName]!!.time = now
|
metadata.packageMetadataMap[packageName]!!.time = now
|
||||||
metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA
|
metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA
|
||||||
|
|
|
@ -4,7 +4,7 @@ import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val metadataModule = module {
|
val metadataModule = module {
|
||||||
single { MetadataManager(androidContext(), get(), get(), get(), get()) }
|
single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) }
|
||||||
single<MetadataWriter> { MetadataWriterImpl(get()) }
|
single<MetadataWriter> { MetadataWriterImpl(get()) }
|
||||||
single<MetadataReader> { MetadataReaderImpl(get()) }
|
single<MetadataReader> { MetadataReaderImpl(get()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
|
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
|
||||||
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
|
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
|
||||||
deviceName = meta.getString(JSON_METADATA_NAME),
|
deviceName = meta.getString(JSON_METADATA_NAME),
|
||||||
packageMetadataMap = packageMetadataMap
|
d2dBackup = meta.optBoolean(JSON_METADATA_D2D_BACKUP, false),
|
||||||
|
packageMetadataMap = packageMetadataMap,
|
||||||
)
|
)
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
throw SecurityException(e)
|
throw SecurityException(e)
|
||||||
|
|
|
@ -35,6 +35,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
|
||||||
put(JSON_METADATA_SDK_INT, metadata.androidVersion)
|
put(JSON_METADATA_SDK_INT, metadata.androidVersion)
|
||||||
put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
|
put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
|
||||||
put(JSON_METADATA_NAME, metadata.deviceName)
|
put(JSON_METADATA_NAME, metadata.deviceName)
|
||||||
|
put(JSON_METADATA_D2D_BACKUP, metadata.d2dBackup)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
|
for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
|
||||||
|
|
|
@ -23,6 +23,9 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) {
|
||||||
val deviceName: String
|
val deviceName: String
|
||||||
get() = backupMetadata.deviceName
|
get() = backupMetadata.deviceName
|
||||||
|
|
||||||
|
val d2dBackup: Boolean
|
||||||
|
get() = backupMetadata.d2dBackup
|
||||||
|
|
||||||
val packageMetadataMap: PackageMetadataMap
|
val packageMetadataMap: PackageMetadataMap
|
||||||
get() = backupMetadata.packageMetadataMap
|
get() = backupMetadata.packageMetadataMap
|
||||||
|
|
||||||
|
|
|
@ -55,9 +55,16 @@ internal class AppListRetriever(
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun getAppList(): List<AppListItem> {
|
fun getAppList(): List<AppListItem> {
|
||||||
return listOf(AppSectionTitle(R.string.backup_section_system)) + getSpecialApps() +
|
|
||||||
listOf(AppSectionTitle(R.string.backup_section_user)) + getUserApps() +
|
val appListSections = linkedMapOf(
|
||||||
listOf(AppSectionTitle(R.string.backup_section_not_allowed)) + getNotAllowedApps()
|
AppSectionTitle(R.string.backup_section_system) to getSpecialApps(),
|
||||||
|
AppSectionTitle(R.string.backup_section_user) to getUserApps(),
|
||||||
|
AppSectionTitle(R.string.backup_section_not_allowed) to getNotAllowedApps()
|
||||||
|
).filter { it.value.isNotEmpty() }
|
||||||
|
|
||||||
|
return appListSections.flatMap { (sectionTitle, appList) ->
|
||||||
|
listOf(sectionTitle) + appList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSpecialApps(): List<AppListItem> {
|
private fun getSpecialApps(): List<AppListItem> {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
|
@ -14,6 +15,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||||
private val packageService: PackageService by inject()
|
private val packageService: PackageService by inject()
|
||||||
|
|
||||||
// TODO set mimeType when upgrading androidx lib
|
// TODO set mimeType when upgrading androidx lib
|
||||||
private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri ->
|
private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri ->
|
||||||
viewModel.onLogcatUriReceived(uri)
|
viewModel.onLogcatUriReceived(uri)
|
||||||
|
@ -23,6 +25,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
||||||
permitDiskReads {
|
permitDiskReads {
|
||||||
setPreferencesFromResource(R.xml.settings_expert, rootKey)
|
setPreferencesFromResource(R.xml.settings_expert, rootKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>("logcat")?.setOnPreferenceClickListener {
|
findPreference<Preference>("logcat")?.setOnPreferenceClickListener {
|
||||||
val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver"
|
val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver"
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
|
@ -30,6 +33,25 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
||||||
createFileLauncher.launch(name)
|
createFileLauncher.launch(name)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val quotaPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_UNLIMITED_QUOTA)
|
||||||
|
|
||||||
|
quotaPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
quotaPreference.isChecked = newValue as Boolean
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
|
||||||
|
|
||||||
|
d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
d2dPreference.isChecked = newValue as Boolean
|
||||||
|
|
||||||
|
// automatically enable unlimited quota when enabling D2D backups
|
||||||
|
if (d2dPreference.isChecked) {
|
||||||
|
quotaPreference?.isChecked = true
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
@ -89,7 +89,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
autoRestore = findPreference("auto_restore")!!
|
autoRestore = findPreference(PREF_KEY_AUTO_RESTORE)!!
|
||||||
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||||
val enabled = newValue as Boolean
|
val enabled = newValue as Boolean
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentSkipListSet
|
||||||
|
|
||||||
internal const val PREF_KEY_TOKEN = "token"
|
internal const val PREF_KEY_TOKEN = "token"
|
||||||
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
|
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
|
||||||
|
internal const val PREF_KEY_AUTO_RESTORE = "auto_restore"
|
||||||
|
|
||||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
||||||
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
||||||
|
@ -30,7 +31,8 @@ private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
|
||||||
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
||||||
|
|
||||||
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
||||||
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
internal const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
||||||
|
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
|
||||||
|
|
||||||
class SettingsManager(private val context: Context) {
|
class SettingsManager(private val context: Context) {
|
||||||
|
|
||||||
|
@ -151,6 +153,14 @@ class SettingsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false)
|
fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false)
|
||||||
|
|
||||||
|
fun d2dBackupsEnabled() = prefs.getBoolean(PREF_KEY_D2D_BACKUPS, false)
|
||||||
|
|
||||||
|
fun setD2dBackupsEnabled(enabled: Boolean) {
|
||||||
|
prefs.edit()
|
||||||
|
.putBoolean(PREF_KEY_D2D_BACKUPS, enabled)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Storage(
|
data class Storage(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.stevesoltys.seedvault.transport
|
package com.stevesoltys.seedvault.transport
|
||||||
|
|
||||||
import android.app.backup.BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
import android.app.backup.BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
||||||
|
import android.app.backup.BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER
|
||||||
import android.app.backup.BackupTransport
|
import android.app.backup.BackupTransport
|
||||||
import android.app.backup.RestoreDescription
|
import android.app.backup.RestoreDescription
|
||||||
import android.app.backup.RestoreSet
|
import android.app.backup.RestoreSet
|
||||||
|
@ -11,6 +12,7 @@ import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
import com.stevesoltys.seedvault.settings.SettingsActivity
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -20,7 +22,8 @@ import org.koin.core.component.inject
|
||||||
// If we ever change this, we should use a ComponentName like the other backup transports.
|
// If we ever change this, we should use a ComponentName like the other backup transports.
|
||||||
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
|
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
|
||||||
|
|
||||||
const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
const val DEFAULT_TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
||||||
|
const val D2D_TRANSPORT_FLAGS = DEFAULT_TRANSPORT_FLAGS or FLAG_DEVICE_TO_DEVICE_TRANSFER
|
||||||
|
|
||||||
private const val TRANSPORT_DIRECTORY_NAME =
|
private const val TRANSPORT_DIRECTORY_NAME =
|
||||||
"com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
|
"com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
|
||||||
|
@ -35,6 +38,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
|
|
||||||
private val backupCoordinator by inject<BackupCoordinator>()
|
private val backupCoordinator by inject<BackupCoordinator>()
|
||||||
private val restoreCoordinator by inject<RestoreCoordinator>()
|
private val restoreCoordinator by inject<RestoreCoordinator>()
|
||||||
|
private val settingsManager by inject<SettingsManager>()
|
||||||
|
|
||||||
override fun transportDirName(): String {
|
override fun transportDirName(): String {
|
||||||
return TRANSPORT_DIRECTORY_NAME
|
return TRANSPORT_DIRECTORY_NAME
|
||||||
|
@ -54,7 +58,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
* This allows the agent to decide what to do based on properties of the transport.
|
* This allows the agent to decide what to do based on properties of the transport.
|
||||||
*/
|
*/
|
||||||
override fun getTransportFlags(): Int {
|
override fun getTransportFlags(): Int {
|
||||||
return TRANSPORT_FLAGS
|
return if (settingsManager.d2dBackupsEnabled()) {
|
||||||
|
D2D_TRANSPORT_FLAGS
|
||||||
|
} else {
|
||||||
|
DEFAULT_TRANSPORT_FLAGS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -143,12 +143,9 @@ internal class BackupCoordinator(
|
||||||
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val packageName = targetPackage.packageName
|
val packageName = targetPackage.packageName
|
||||||
// Check that the app is not blacklisted by the user
|
val shouldInclude = packageService.shouldIncludeAppInBackup(packageName)
|
||||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
if (!shouldInclude) Log.i(TAG, "Excluding $packageName from backup.")
|
||||||
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.")
|
return shouldInclude
|
||||||
// We need to exclude the DocumentsProvider used to store backup data.
|
|
||||||
// Otherwise, it gets killed when we back it up, terminating our backup.
|
|
||||||
return enabled && targetPackage.packageName != plugin.providerPackageName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,7 +8,9 @@ val backupModule = module {
|
||||||
single {
|
single {
|
||||||
PackageService(
|
PackageService(
|
||||||
context = androidContext(),
|
context = androidContext(),
|
||||||
backupManager = get()
|
backupManager = get(),
|
||||||
|
settingsManager = get(),
|
||||||
|
plugin = get()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
|
|
|
@ -17,6 +17,8 @@ import android.util.Log
|
||||||
import android.util.Log.INFO
|
import android.util.Log.INFO
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
|
||||||
private val TAG = PackageService::class.java.simpleName
|
private val TAG = PackageService::class.java.simpleName
|
||||||
|
|
||||||
|
@ -29,6 +31,8 @@ private const val LOG_MAX_PACKAGES = 100
|
||||||
internal class PackageService(
|
internal class PackageService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
|
private val plugin: StoragePlugin,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
private val packageManager: PackageManager = context.packageManager
|
||||||
|
@ -45,13 +49,16 @@ internal class PackageService(
|
||||||
// log packages
|
// log packages
|
||||||
if (Log.isLoggable(TAG, INFO)) {
|
if (Log.isLoggable(TAG, INFO)) {
|
||||||
Log.i(TAG, "Got ${packages.size} packages:")
|
Log.i(TAG, "Got ${packages.size} packages:")
|
||||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
logPackages(packages)
|
||||||
Log.i(TAG, it.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val eligibleApps =
|
val eligibleApps = if (settingsManager.d2dBackupsEnabled()) {
|
||||||
|
// if D2D is enabled, use the "new method" for filtering packages
|
||||||
|
packages.filter(::shouldIncludeAppInBackup).toTypedArray()
|
||||||
|
} else {
|
||||||
|
// otherwise, use the BackupManager call.
|
||||||
backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray())
|
backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
// log eligible packages
|
// log eligible packages
|
||||||
if (Log.isLoggable(TAG, INFO)) {
|
if (Log.isLoggable(TAG, INFO)) {
|
||||||
|
@ -66,6 +73,9 @@ internal class PackageService(
|
||||||
return packageArray.toTypedArray()
|
return packageArray.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of packages that will not be backed up.
|
||||||
|
*/
|
||||||
val notBackedUpPackages: List<PackageInfo>
|
val notBackedUpPackages: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() {
|
get() {
|
||||||
|
@ -94,16 +104,23 @@ internal class PackageService(
|
||||||
val userApps: List<PackageInfo>
|
val userApps: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
|
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
|
||||||
packageInfo.isUserVisible(context) && packageInfo.allowsBackup()
|
packageInfo.isUserVisible(context) &&
|
||||||
|
packageInfo.allowsBackup()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of apps that does not allow backup.
|
* A list of apps that do not allow backup.
|
||||||
*/
|
*/
|
||||||
val userNotAllowedApps: List<PackageInfo>
|
val userNotAllowedApps: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
get() {
|
||||||
!packageInfo.allowsBackup() && !packageInfo.isSystemApp()
|
// if D2D backups are enabled, all apps are allowed
|
||||||
|
if (settingsManager.d2dBackupsEnabled()) return emptyList()
|
||||||
|
|
||||||
|
return packageManager.getInstalledPackages(0).filter { packageInfo ->
|
||||||
|
!packageInfo.allowsBackup() &&
|
||||||
|
!packageInfo.isSystemApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val expectedAppTotals: ExpectedAppTotals
|
val expectedAppTotals: ExpectedAppTotals
|
||||||
|
@ -128,12 +145,64 @@ internal class PackageService(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldIncludeAppInBackup(packageName: String): Boolean {
|
||||||
|
// We do not use BackupManager.filterAppsEligibleForBackupForUser for D2D because it
|
||||||
|
// always makes its determinations based on OperationType.BACKUP, never based on
|
||||||
|
// OperationType.MIGRATION, and there are no alternative publicly-available APIs.
|
||||||
|
// We don't need to use it, here, either; during a backup or migration, the system
|
||||||
|
// will perform its own eligibility checks regardless. We merely need to filter out
|
||||||
|
// apps that we, or the user, want to exclude.
|
||||||
|
|
||||||
|
// Check that the app is not excluded by user preference
|
||||||
|
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||||
|
|
||||||
|
// We need to explicitly exclude DocumentsProvider and Seedvault.
|
||||||
|
// Otherwise, they get killed while backing them up, terminating our backup.
|
||||||
|
val excludedPackages = setOf(
|
||||||
|
plugin.providerPackageName,
|
||||||
|
context.packageName
|
||||||
|
)
|
||||||
|
|
||||||
|
return enabled && !excludedPackages.contains(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
private fun logPackages(packages: List<String>) {
|
private fun logPackages(packages: List<String>) {
|
||||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
||||||
Log.i(TAG, it.toString())
|
Log.i(TAG, it.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun PackageInfo.allowsBackup(): Boolean {
|
||||||
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||||
|
|
||||||
|
return if (settingsManager.d2dBackupsEnabled()) {
|
||||||
|
/**
|
||||||
|
* TODO: Consider ways of replicating the system's logic so that the user can have
|
||||||
|
* advance knowledge of apps that the system will exclude, particularly apps targeting
|
||||||
|
* SDK 30 or below.
|
||||||
|
*
|
||||||
|
* At backup time, the system will filter out any apps that *it* does not want to be
|
||||||
|
* backed up. If the user has enabled D2D, *we* generally want to back up as much as
|
||||||
|
* possible; part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup).
|
||||||
|
* So, we return true.
|
||||||
|
* See frameworks/base/services/backup/java/com/android/server/backup/utils/
|
||||||
|
* BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
|
||||||
|
*/
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag indicating whether or not this package should _not_ be backed up.
|
||||||
|
*
|
||||||
|
* This happens when the app has opted-out of backup, or when it is stopped.
|
||||||
|
*/
|
||||||
|
private fun PackageInfo.doesNotGetBackedUp(): Boolean {
|
||||||
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
|
||||||
|
return !allowsBackup() || isStopped()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class ExpectedAppTotals(
|
internal data class ExpectedAppTotals(
|
||||||
|
@ -157,11 +226,6 @@ internal fun PackageInfo.isSystemApp(): Boolean {
|
||||||
return applicationInfo.flags and FLAG_SYSTEM != 0
|
return applicationInfo.flags and FLAG_SYSTEM != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PackageInfo.allowsBackup(): Boolean {
|
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
|
||||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this is a system app that hasn't been updated.
|
* Returns true if this is a system app that hasn't been updated.
|
||||||
* We don't back up those APKs.
|
* We don't back up those APKs.
|
||||||
|
@ -173,12 +237,6 @@ internal fun PackageInfo.isNotUpdatedSystemApp(): Boolean {
|
||||||
return isSystemApp && !isUpdatedSystemApp
|
return isSystemApp && !isUpdatedSystemApp
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
|
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
|
|
||||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup
|
|
||||||
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun PackageInfo.isStopped(): Boolean {
|
internal fun PackageInfo.isStopped(): Boolean {
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||||
return applicationInfo.flags and FLAG_STOPPED != 0
|
return applicationInfo.flags and FLAG_STOPPED != 0
|
||||||
|
|
|
@ -22,10 +22,19 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_FLAGS
|
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
||||||
|
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device name used in AOSP to indicate that a restore set is part of a device-to-device migration.
|
||||||
|
* See getBackupEligibilityRules in frameworks/base/services/backup/java/com/android/server/
|
||||||
|
* backup/restore/ActiveRestoreSession.java. AOSP currently relies on this constant, and it is not
|
||||||
|
* publicly exposed. Framework code indicates they intend to use a flag, instead, in the future.
|
||||||
|
*/
|
||||||
|
internal const val D2D_DEVICE_NAME = "D2D"
|
||||||
|
|
||||||
private data class RestoreCoordinatorState(
|
private data class RestoreCoordinatorState(
|
||||||
val token: Long,
|
val token: Long,
|
||||||
val packages: Iterator<PackageInfo>,
|
val packages: Iterator<PackageInfo>,
|
||||||
|
@ -92,7 +101,20 @@ internal class RestoreCoordinator(
|
||||||
**/
|
**/
|
||||||
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||||
return getAvailableMetadata()?.map { (_, metadata) ->
|
return getAvailableMetadata()?.map { (_, metadata) ->
|
||||||
RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token, TRANSPORT_FLAGS)
|
|
||||||
|
val transportFlags = if (metadata.d2dBackup) {
|
||||||
|
D2D_TRANSPORT_FLAGS
|
||||||
|
} else {
|
||||||
|
DEFAULT_TRANSPORT_FLAGS
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceName = if (metadata.d2dBackup) {
|
||||||
|
D2D_DEVICE_NAME
|
||||||
|
} else {
|
||||||
|
metadata.deviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
|
||||||
}?.toTypedArray()
|
}?.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +136,10 @@ internal class RestoreCoordinator(
|
||||||
*/
|
*/
|
||||||
fun beforeStartRestore(backupMetadata: BackupMetadata) {
|
fun beforeStartRestore(backupMetadata: BackupMetadata) {
|
||||||
this.backupMetadata = backupMetadata
|
this.backupMetadata = backupMetadata
|
||||||
|
|
||||||
|
if (backupMetadata.d2dBackup) {
|
||||||
|
settingsManager.setD2dBackupsEnabled(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -219,6 +245,7 @@ internal class RestoreCoordinator(
|
||||||
TYPE_KEY_VALUE
|
TYPE_KEY_VALUE
|
||||||
} else throw IOException("No data found for $packageName. Skipping.")
|
} else throw IOException("No data found for $packageName. Skipping.")
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupType.FULL -> {
|
BackupType.FULL -> {
|
||||||
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
|
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
|
||||||
if (plugin.hasData(state.token, name)) {
|
if (plugin.hasData(state.token, name)) {
|
||||||
|
@ -228,6 +255,7 @@ internal class RestoreCoordinator(
|
||||||
TYPE_FULL_STREAM
|
TYPE_FULL_STREAM
|
||||||
} else throw IOException("No data found for $packageName. Skipping...")
|
} else throw IOException("No data found for $packageName. Skipping...")
|
||||||
}
|
}
|
||||||
|
|
||||||
null -> {
|
null -> {
|
||||||
Log.i(TAG, "No backup type found for $packageName. Skipping...")
|
Log.i(TAG, "No backup type found for $packageName. Skipping...")
|
||||||
state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s ->
|
state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s ->
|
||||||
|
@ -261,12 +289,14 @@ internal class RestoreCoordinator(
|
||||||
state.currentPackage = packageName
|
state.currentPackage = packageName
|
||||||
TYPE_KEY_VALUE
|
TYPE_KEY_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
full.hasDataForPackage(state.token, packageInfo) -> {
|
full.hasDataForPackage(state.token, packageInfo) -> {
|
||||||
Log.i(TAG, "Found full backup data for $packageName.")
|
Log.i(TAG, "Found full backup data for $packageName.")
|
||||||
full.initializeState(0x00, state.token, "", packageInfo)
|
full.initializeState(0x00, state.token, "", packageInfo)
|
||||||
state.currentPackage = packageName
|
state.currentPackage = packageName
|
||||||
TYPE_FULL_STREAM
|
TYPE_FULL_STREAM
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.i(TAG, "No data found for $packageName. Skipping.")
|
Log.i(TAG, "No data found for $packageName. Skipping.")
|
||||||
return nextRestorePackage()
|
return nextRestorePackage()
|
||||||
|
|
|
@ -49,6 +49,8 @@
|
||||||
<string name="settings_expert_title">Expert settings</string>
|
<string name="settings_expert_title">Expert settings</string>
|
||||||
<string name="settings_expert_quota_title">Unlimited app quota</string>
|
<string name="settings_expert_quota_title">Unlimited app quota</string>
|
||||||
<string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string>
|
<string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string>
|
||||||
|
<string name="settings_expert_d2d_title">Device-to-device backups</string>
|
||||||
|
<string name="settings_expert_d2d_summary">This forces backups for most apps, even when they disallow them. This is experimental, use at your own risk.\n\n1. To backup apps in D2D mode, you will need to run \"Backup now\" manually.\n\n2. Android may overwrite D2D backups for apps which normally allow backups.</string>
|
||||||
<string name="settings_expert_logcat_title">Save app log</string>
|
<string name="settings_expert_logcat_title">Save app log</string>
|
||||||
<string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string>
|
<string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string>
|
||||||
<string name="settings_expert_logcat_error">Error: Could not save app log</string>
|
<string name="settings_expert_logcat_error">Error: Could not save app log</string>
|
||||||
|
|
|
@ -5,6 +5,12 @@
|
||||||
android:key="unlimited_quota"
|
android:key="unlimited_quota"
|
||||||
android:summary="@string/settings_expert_quota_summary"
|
android:summary="@string/settings_expert_quota_summary"
|
||||||
android:title="@string/settings_expert_quota_title" />
|
android:title="@string/settings_expert_quota_title" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:id="@+id/d2d_backup_preference"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="d2d_backups"
|
||||||
|
android:summary="@string/settings_expert_d2d_summary"
|
||||||
|
android:title="@string/settings_expert_d2d_title" />
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_bug_report"
|
android:icon="@drawable/ic_bug_report"
|
||||||
android:key="logcat"
|
android:key="logcat"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.header.headerModule
|
||||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
import com.stevesoltys.seedvault.metadata.metadataModule
|
||||||
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
||||||
import com.stevesoltys.seedvault.restore.install.installModule
|
import com.stevesoltys.seedvault.restore.install.installModule
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.backupModule
|
import com.stevesoltys.seedvault.transport.backup.backupModule
|
||||||
import com.stevesoltys.seedvault.transport.restore.restoreModule
|
import com.stevesoltys.seedvault.transport.restore.restoreModule
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
@ -25,6 +26,7 @@ class TestApp : App() {
|
||||||
}
|
}
|
||||||
private val appModule = module {
|
private val appModule = module {
|
||||||
single { Clock() }
|
single { Clock() }
|
||||||
|
single { SettingsManager(this@TestApp) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startKoin() = startKoin {
|
override fun startKoin() = startKoin {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -26,7 +27,10 @@ import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Assert.fail
|
import org.junit.Assert.fail
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.core.context.stopKoin
|
import org.koin.core.context.stopKoin
|
||||||
|
@ -51,8 +55,16 @@ class MetadataManagerTest {
|
||||||
private val crypto: Crypto = mockk()
|
private val crypto: Crypto = mockk()
|
||||||
private val metadataWriter: MetadataWriter = mockk()
|
private val metadataWriter: MetadataWriter = mockk()
|
||||||
private val metadataReader: MetadataReader = mockk()
|
private val metadataReader: MetadataReader = mockk()
|
||||||
|
private val settingsManager: SettingsManager = mockk()
|
||||||
|
|
||||||
private val manager = MetadataManager(context, clock, crypto, metadataWriter, metadataReader)
|
private val manager = MetadataManager(
|
||||||
|
context = context,
|
||||||
|
clock = clock,
|
||||||
|
crypto = crypto,
|
||||||
|
metadataWriter = metadataWriter,
|
||||||
|
metadataReader = metadataReader,
|
||||||
|
settingsManager = settingsManager
|
||||||
|
)
|
||||||
|
|
||||||
private val time = 42L
|
private val time = 42L
|
||||||
private val token = Random.nextLong()
|
private val token = Random.nextLong()
|
||||||
|
@ -69,6 +81,11 @@ class MetadataManagerTest {
|
||||||
private val cacheInputStream: FileInputStream = mockk()
|
private val cacheInputStream: FileInputStream = mockk()
|
||||||
private val encodedMetadata = getRandomByteArray()
|
private val encodedMetadata = getRandomByteArray()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun beforeEachTest() {
|
||||||
|
every { settingsManager.d2dBackupsEnabled() } returns false
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun afterEachTest() {
|
fun afterEachTest() {
|
||||||
stopKoin()
|
stopKoin()
|
||||||
|
@ -246,6 +263,23 @@ class MetadataManagerTest {
|
||||||
manager.getPackageMetadata(packageName)
|
manager.getPackageMetadata(packageName)
|
||||||
)
|
)
|
||||||
assertEquals(time, manager.getLastBackupTime())
|
assertEquals(time, manager.getLastBackupTime())
|
||||||
|
assertFalse(updatedMetadata.d2dBackup)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
cacheInputStream.close()
|
||||||
|
cacheOutputStream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun `test onPackageBackedUp() with D2D enabled`() {
|
||||||
|
expectReadFromCache()
|
||||||
|
every { clock.time() } returns time
|
||||||
|
expectModifyMetadata(initialMetadata)
|
||||||
|
|
||||||
|
every { settingsManager.d2dBackupsEnabled() } returns true
|
||||||
|
|
||||||
|
manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
|
||||||
|
assertTrue(initialMetadata.d2dBackup)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
cacheInputStream.close()
|
cacheInputStream.close()
|
||||||
|
|
|
@ -59,6 +59,10 @@ internal abstract class TransportTest {
|
||||||
put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV))
|
put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
protected val d2dMetadata = metadata.copy(
|
||||||
|
d2dBackup = true
|
||||||
|
)
|
||||||
|
|
||||||
protected val salt = metadata.salt
|
protected val salt = metadata.salt
|
||||||
protected val name = getRandomString(12)
|
protected val name = getRandomString(12)
|
||||||
protected val name2 = getRandomString(23)
|
protected val name2 = getRandomString(23)
|
||||||
|
|
|
@ -32,8 +32,6 @@ import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
@ -46,8 +44,8 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
private val kv = mockk<KVBackup>()
|
private val kv = mockk<KVBackup>()
|
||||||
private val full = mockk<FullBackup>()
|
private val full = mockk<FullBackup>()
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
private val packageService: PackageService = mockk()
|
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
private val notificationManager = mockk<BackupNotificationManager>()
|
||||||
|
private val packageService = mockk<PackageService>()
|
||||||
|
|
||||||
private val backup = BackupCoordinator(
|
private val backup = BackupCoordinator(
|
||||||
context,
|
context,
|
||||||
|
@ -170,20 +168,6 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
verify { metadataOutputStream.close() }
|
verify { metadataOutputStream.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `isAppEligibleForBackup() exempts plugin provider and blacklisted apps`() {
|
|
||||||
every {
|
|
||||||
settingsManager.isBackupEnabled(packageInfo.packageName)
|
|
||||||
} returns true andThen false andThen true
|
|
||||||
every {
|
|
||||||
plugin.providerPackageName
|
|
||||||
} returns packageInfo.packageName andThen "new.package" andThen "new.package"
|
|
||||||
|
|
||||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
|
||||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
|
||||||
assertTrue(backup.isAppEligibleForBackup(packageInfo, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clearing KV backup data throws`() = runBlocking {
|
fun `clearing KV backup data throws`() = runBlocking {
|
||||||
every { settingsManager.getToken() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
|
|
|
@ -90,6 +90,15 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
assertEquals(metadata.deviceName, sets[0].device)
|
assertEquals(metadata.deviceName, sets[0].device)
|
||||||
assertEquals(metadata.deviceName, sets[0].name)
|
assertEquals(metadata.deviceName, sets[0].name)
|
||||||
assertEquals(metadata.token, sets[0].token)
|
assertEquals(metadata.token, sets[0].token)
|
||||||
|
|
||||||
|
every { metadataReader.readMetadata(inputStream, token) } returns d2dMetadata
|
||||||
|
every { metadataReader.readMetadata(inputStream, token + 1) } returns d2dMetadata
|
||||||
|
|
||||||
|
val d2dSets = restore.getAvailableRestoreSets() ?: fail()
|
||||||
|
assertEquals(2, d2dSets.size)
|
||||||
|
assertEquals(D2D_DEVICE_NAME, d2dSets[0].device)
|
||||||
|
assertEquals(metadata.deviceName, d2dSets[0].name)
|
||||||
|
assertEquals(metadata.token, d2dSets[0].token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue