Add experimental support for forcing D2D transfer backups
This commit is contained in:
parent
57148943ad
commit
fa4c52fb83
30 changed files with 200 additions and 54 deletions
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
|
||||||
|
|
||||||
|
|
|
||||||
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,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,14 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|
@ -63,10 +72,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>,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ServiceManager.getService
|
import android.os.ServiceManager.getService
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.SystemProperties
|
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import com.stevesoltys.seedvault.crypto.cryptoModule
|
import com.stevesoltys.seedvault.crypto.cryptoModule
|
||||||
import com.stevesoltys.seedvault.header.headerModule
|
import com.stevesoltys.seedvault.header.headerModule
|
||||||
|
|
@ -60,7 +59,6 @@ open class App : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
SystemProperties.set(BACKUP_D2D_PROPERTY, "true")
|
|
||||||
startKoin()
|
startKoin()
|
||||||
if (isDebugBuild()) {
|
if (isDebugBuild()) {
|
||||||
StrictMode.setThreadPolicy(
|
StrictMode.setThreadPolicy(
|
||||||
|
|
@ -123,8 +121,6 @@ const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
|
||||||
const val ANCESTRAL_RECORD_KEY = "@ancestral_record@"
|
const val ANCESTRAL_RECORD_KEY = "@ancestral_record@"
|
||||||
const val GLOBAL_METADATA_KEY = "@meta@"
|
const val GLOBAL_METADATA_KEY = "@meta@"
|
||||||
|
|
||||||
const val BACKUP_D2D_PROPERTY = "persist.backup.fake-d2d"
|
|
||||||
|
|
||||||
// TODO this doesn't work for LineageOS as they do public debug builds
|
// TODO this doesn't work for LineageOS as they do public debug builds
|
||||||
fun isDebugBuild() = Build.TYPE == "userdebug"
|
fun isDebugBuild() = Build.TYPE == "userdebug"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,13 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
||||||
createFileLauncher.launch(name)
|
createFileLauncher.launch(name)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
|
||||||
|
|
||||||
|
d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
d2dPreference.isChecked = newValue as Boolean
|
||||||
|
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"
|
||||||
|
|
@ -31,6 +32,7 @@ private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
||||||
|
|
||||||
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
||||||
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
private 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(
|
||||||
|
|
|
||||||
|
|
@ -12,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
|
||||||
|
|
@ -21,9 +22,8 @@ import org.koin.core.component.inject
|
||||||
// If we ever change this, we should use a ComponentName like the other backup transports.
|
// 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
|
||||||
|
|
||||||
// Since there seems to be consensus in the community to pose as device-to-device transport,
|
const val DEFAULT_TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
|
||||||
// we are pretending to be one here. This will back up opt-out apps that target at least API 31.
|
const val D2D_TRANSPORT_FLAGS = DEFAULT_TRANSPORT_FLAGS or FLAG_DEVICE_TO_DEVICE_TRANSFER
|
||||||
const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED 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"
|
||||||
|
|
@ -38,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
|
||||||
|
|
@ -57,7 +58,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
||||||
* This allows the agent to decide what to do based on properties of the transport.
|
* 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ val backupModule = module {
|
||||||
single {
|
single {
|
||||||
PackageService(
|
PackageService(
|
||||||
context = androidContext(),
|
context = androidContext(),
|
||||||
|
backupManager = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
plugin = get()
|
plugin = get()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
||||||
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
||||||
|
|
@ -11,6 +12,7 @@ import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.GET_INSTRUMENTATION
|
import android.content.pm.PackageManager.GET_INSTRUMENTATION
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Log.INFO
|
import android.util.Log.INFO
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
|
@ -28,11 +30,13 @@ 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 settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val plugin: StoragePlugin,
|
private val plugin: StoragePlugin,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
private val packageManager: PackageManager = context.packageManager
|
||||||
|
private val myUserId = UserHandle.myUserId()
|
||||||
|
|
||||||
val eligiblePackages: Array<String>
|
val eligiblePackages: Array<String>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|
@ -48,13 +52,7 @@ internal class PackageService(
|
||||||
logPackages(packages)
|
logPackages(packages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do not use BackupManager.filterAppsEligibleForBackupForUser because it
|
val eligibleApps = packages.filter(::shouldIncludeAppInBackup).toTypedArray()
|
||||||
// always makes its determinations based on OperationType.BACKUP, never based on
|
|
||||||
// OperationType.MIGRATION, and there are no alternative publicly-available APIs.
|
|
||||||
// We don't need to use it, here, either; during a backup or migration, the system
|
|
||||||
// will perform its own eligibility checks regardless. We merely need to filter out
|
|
||||||
// apps that we, or the user, want to exclude.
|
|
||||||
val eligibleApps = packages.filter(::shouldIncludeAppInBackup)
|
|
||||||
|
|
||||||
// log eligible packages
|
// log eligible packages
|
||||||
if (Log.isLoggable(TAG, INFO)) {
|
if (Log.isLoggable(TAG, INFO)) {
|
||||||
|
|
@ -97,7 +95,8 @@ 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(settingsManager.d2dBackupsEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,7 +105,8 @@ internal class PackageService(
|
||||||
val userNotAllowedApps: List<PackageInfo>
|
val userNotAllowedApps: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
||||||
!packageInfo.allowsBackup() && !packageInfo.isSystemApp()
|
!packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) &&
|
||||||
|
!packageInfo.isSystemApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
val expectedAppTotals: ExpectedAppTotals
|
val expectedAppTotals: ExpectedAppTotals
|
||||||
|
|
@ -132,11 +132,24 @@ internal class PackageService(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldIncludeAppInBackup(packageName: String): Boolean {
|
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
|
// Check that the app is not excluded by user preference
|
||||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||||
// We also need to exclude the DocumentsProvider used to store backup data.
|
|
||||||
// Otherwise, it gets killed when we back it up, terminating our backup.
|
// We need to explicitly exclude DocumentsProvider and Seedvault.
|
||||||
return enabled && packageName != plugin.providerPackageName
|
// 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>) {
|
||||||
|
|
@ -168,18 +181,24 @@ internal fun PackageInfo.isSystemApp(): Boolean {
|
||||||
return applicationInfo.flags and FLAG_SYSTEM != 0
|
return applicationInfo.flags and FLAG_SYSTEM != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PackageInfo.allowsBackup(): Boolean {
|
internal fun PackageInfo.allowsBackup(d2dBackup: Boolean): Boolean {
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||||
|
|
||||||
// TODO: Consider ways of replicating the system's logic so that the user can have advance
|
return if (d2dBackup) {
|
||||||
// knowledge of apps that the system will exclude, particularly apps targeting SDK 30 or below.
|
// 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.
|
// At backup time, the system will filter out any apps that *it* does not want to be
|
||||||
// Now that we have switched to D2D, *we* generally want to back up as much as possible;
|
// backed up. If the user has enabled D2D, *we* generally want to back up as much as
|
||||||
// part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup). So, we return true.
|
// possible; part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup).
|
||||||
// See frameworks/base/services/backup/java/com/android/server/backup/utils/
|
// So, we return true.
|
||||||
// BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
|
// See frameworks/base/services/backup/java/com/android/server/backup/utils/
|
||||||
return true
|
// BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
import com.stevesoltys.seedvault.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
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ import java.io.IOException
|
||||||
* backup/restore/ActiveRestoreSession.java. AOSP currently relies on this constant, and it is not
|
* 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.
|
* publicly exposed. Framework code indicates they intend to use a flag, instead, in the future.
|
||||||
*/
|
*/
|
||||||
internal const val DEVICE_NAME_FOR_D2D_SET = "D2D"
|
internal const val D2D_DEVICE_NAME = "D2D"
|
||||||
|
|
||||||
private data class RestoreCoordinatorState(
|
private data class RestoreCoordinatorState(
|
||||||
val token: Long,
|
val token: Long,
|
||||||
|
|
@ -100,8 +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 /* name */, DEVICE_NAME_FOR_D2D_SET /* device */,
|
|
||||||
metadata.token, TRANSPORT_FLAGS)
|
val transportFlags = if (metadata.d2dBackup) {
|
||||||
|
D2D_TRANSPORT_FLAGS
|
||||||
|
} else {
|
||||||
|
DEFAULT_TRANSPORT_FLAGS
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceName = if (metadata.d2dBackup) {
|
||||||
|
D2D_DEVICE_NAME
|
||||||
|
} else {
|
||||||
|
metadata.deviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
|
||||||
}?.toTypedArray()
|
}?.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -228,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)) {
|
||||||
|
|
@ -237,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 ->
|
||||||
|
|
@ -270,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">Tell AOSP that our backups are being used for a D2D transfer. This forces backups for most apps, even when they disallow them.\n\nWarning: This is experimental, use at your own risk.\n\nSee more info on our FAQ.</string>
|
||||||
<string name="settings_expert_logcat_title">Save app log</string>
|
<string name="settings_expert_logcat_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
|
||||||
|
|
@ -27,6 +28,7 @@ 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.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 +53,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 +79,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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,18 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
val sets = restore.getAvailableRestoreSets() ?: fail()
|
val sets = restore.getAvailableRestoreSets() ?: fail()
|
||||||
assertEquals(2, sets.size)
|
assertEquals(2, sets.size)
|
||||||
assertEquals(DEVICE_NAME_FOR_D2D_SET, sets[0].device)
|
assertEquals(metadata.deviceName, sets[0].device)
|
||||||
assertEquals(metadata.deviceName, sets[0].name)
|
assertEquals(metadata.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…
Add table
Reference in a new issue