Merge pull request #748 from seedvault-app/feature/cirrus-ci-tests

Run tests in Cirrus CI again
This commit is contained in:
Torsten Grote 2024-10-18 10:34:18 -03:00 committed by GitHub
commit 0fa14025b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 234 additions and 154 deletions

View file

@ -1,13 +1,66 @@
task: container:
name: Build with AOSP image: ghcr.io/cirruslabs/android-sdk:34
only_if: $CIRRUS_PR_LABELS =~ ".*aosp-build.*" kvm: true
timeout_in: 70m
container:
image: ubuntu:23.04
cpu: 8 cpu: 8
memory: 32G memory: 16G
build_script:
- ./.github/scripts/build_aosp.sh aosp_arm64 ap1a userdebug android-14.0.0_r29 instrumentation_tests_task:
name: "Cirrus CI Instrumentation Tests"
start_avd_background_script:
sdkmanager --install "system-images;android-34;default;x86_64" "emulator";
echo no | avdmanager create avd -n seedvault -k "system-images;android-34;default;x86_64";
$ANDROID_HOME/emulator/emulator
-avd seedvault
-no-audio
-no-boot-anim
-gpu swiftshader_indirect
-no-snapshot
-no-window
-writable-system;
provision_avd_background_script:
wget https://github.com/seedvault-app/seedvault-test-data/releases/download/3/backup.tar.gz;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb root;
sleep 5;
adb remount;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb root;
sleep 5;
adb remount;
sleep 5;
assemble_script:
./gradlew :app:assembleRelease :contacts:assembleRelease assembleAndroidTest
install_app_script:
timeout 180s bash -c 'while [[ -z $(adb shell mount | grep "/system " | grep "(rw,") ]]; do sleep 1; done;';
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb shell mkdir -p /sdcard/seedvault_baseline;
adb push backup.tar.gz /sdcard/seedvault_baseline/backup.tar.gz;
adb shell tar xzf /sdcard/seedvault_baseline/backup.tar.gz --directory=/sdcard/seedvault_baseline;
adb shell mkdir -p /system/priv-app/Seedvault;
adb push app/build/outputs/apk/release/app-release.apk /system/priv-app/Seedvault/Seedvault.apk;
adb push permissions_com.stevesoltys.seedvault.xml /system/etc/permissions/privapp-permissions-seedvault.xml;
adb push allowlist_com.stevesoltys.seedvault.xml /system/etc/sysconfig/allowlist-seedvault.xml;
adb shell mkdir -p /system/priv-app/ContactsBackup;
adb push contactsbackup/build/outputs/apk/release/contactsbackup-release.apk /system/priv-app/ContactsBackup/contactsbackup.apk;
adb push contactsbackup/default-permissions_org.calyxos.backup.contacts.xml /system/etc/default-permissions/default-permissions_org.calyxos.backup.contacts.xml;
adb shell bmgr enable true;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
run_large_tests_script: ./gradlew -Pandroid.testInstrumentationRunnerArguments.size=large :app:connectedAndroidTest
run_other_tests_script: ./gradlew -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest connectedAndroidTest
always: always:
seedvault_artifacts: pull_screenshots_script:
path: Seedvault.apk adb pull /sdcard/seedvault_test_results
screenshots_artifacts:
path: "seedvault_test_results/**/*.mp4"
logcat_artifacts:
path: "seedvault_test_results/**/*.log"

View file

@ -32,13 +32,6 @@ android {
versionNameSuffix = "-${gitDescribe()}" versionNameSuffix = "-${gitDescribe()}"
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner" testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
testInstrumentationRunnerArguments["disableAnalytics"] = "true" testInstrumentationRunnerArguments["disableAnalytics"] = "true"
if (project.hasProperty("instrumented_test_size")) {
val testSize = project.property("instrumented_test_size").toString()
println("Instrumented test size: $testSize")
testInstrumentationRunnerArguments["size"] = testSize
}
} }
signingConfigs { signingConfigs {

View file

@ -9,7 +9,6 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
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
@ -21,8 +20,10 @@ import io.mockk.every
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.calyxos.seedvault.core.toHexString
import org.koin.core.component.get import org.koin.core.component.get
import java.io.ByteArrayOutputStream import java.security.DigestInputStream
import java.security.MessageDigest
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.fail import kotlin.test.fail
@ -154,7 +155,8 @@ internal interface LargeBackupTestBase : LargeTestBase {
private fun spyOnFullBackupData(backupResult: SeedvaultLargeTestResult) { private fun spyOnFullBackupData(backupResult: SeedvaultLargeTestResult) {
var packageName: String? = null var packageName: String? = null
var dataIntercept = ByteArrayOutputStream() val messageDigest = MessageDigest.getInstance("SHA-256")
var digestInputStream: DigestInputStream? = null
coEvery { coEvery {
spyFullBackup.performFullBackup(any(), any(), any()) spyFullBackup.performFullBackup(any(), any(), any())
@ -166,20 +168,19 @@ internal interface LargeBackupTestBase : LargeTestBase {
every { every {
spyInputFactory.getInputStream(any()) spyInputFactory.getInputStream(any())
} answers { } answers {
InputStreamIntercept( digestInputStream = DigestInputStream(callOriginal(), messageDigest)
inputStream = callOriginal(), digestInputStream!!
intercept = dataIntercept
)
} }
coEvery { coEvery {
spyFullBackup.finishBackup() spyFullBackup.finishBackup()
} answers { } answers {
val result = callOriginal() val result = callOriginal()
backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256() val digest = digestInputStream?.messageDigest ?: fail("No digestInputStream")
backupResult.full[packageName!!] = digest.digest().toHexString()
packageName = null packageName = null
dataIntercept = ByteArrayOutputStream() digest.reset()
result result
} }
} }
@ -192,9 +193,6 @@ internal interface LargeBackupTestBase : LargeTestBase {
every { every {
spyBackupNotificationManager.onBackupSuccess(any(), any(), any()) spyBackupNotificationManager.onBackupSuccess(any(), any(), any())
} answers { } answers {
val success = firstArg<Boolean>()
assert(success) { "Backup failed." }
callOriginal() callOriginal()
completed.set(true) completed.set(true)
} }

View file

@ -8,12 +8,13 @@ package com.stevesoltys.seedvault.e2e
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
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
import io.mockk.Call
import io.mockk.MockKAnswerScope
import io.mockk.clearMocks import io.mockk.clearMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
@ -22,8 +23,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.calyxos.seedvault.core.toHexString
import org.koin.core.component.get import org.koin.core.component.get
import java.io.ByteArrayOutputStream import java.security.DigestOutputStream
import java.security.MessageDigest
import kotlin.test.fail
internal interface LargeRestoreTestBase : LargeTestBase { internal interface LargeRestoreTestBase : LargeTestBase {
@ -161,14 +165,26 @@ internal interface LargeRestoreTestBase : LargeTestBase {
clearMocks(spyKVRestore) clearMocks(spyKVRestore)
coEvery { fun initializeStateBlock(
spyKVRestore.initializeState(any(), any(), any(), any()) packageInfoIndex: Int
} answers { ): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
packageName = arg<PackageInfo>(3).packageName packageName = arg<PackageInfo>(packageInfoIndex).packageName
restoreResult.kv[packageName!!] = mutableMapOf() restoreResult.kv[packageName!!] = mutableMapOf()
callOriginal() callOriginal()
} }
coEvery {
spyKVRestore.initializeState(any(), any(), any(), any())
} answers initializeStateBlock(1)
coEvery {
spyKVRestore.initializeStateV1(any(), any(), any(), any())
} answers initializeStateBlock(2)
coEvery {
spyKVRestore.initializeStateV0(any(), any())
} answers initializeStateBlock(1)
every { every {
spyOutputFactory.getBackupDataOutput(any()) spyOutputFactory.getBackupDataOutput(any())
} answers { } answers {
@ -182,47 +198,61 @@ internal interface LargeRestoreTestBase : LargeTestBase {
private fun spyOnFullRestoreData(restoreResult: SeedvaultLargeTestResult) { private fun spyOnFullRestoreData(restoreResult: SeedvaultLargeTestResult) {
var packageName: String? = null var packageName: String? = null
var dataIntercept = ByteArrayOutputStream() val messageDigest = MessageDigest.getInstance("SHA-256")
var digestOutputStream: DigestOutputStream? = null
clearMocks(spyFullRestore) clearMocks(spyFullRestore)
coEvery { fun initializeStateBlock(
spyFullRestore.initializeState(any(), any(), any()) packageInfoIndex: Int
} answers { ): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
packageName?.let { packageName?.let {
restoreResult.full[it] = dataIntercept.toByteArray().sha256() // sometimes finishRestore() doesn't get called, so get data from last package here
digestOutputStream?.messageDigest?.let { digest ->
restoreResult.full[packageName!!] = digest.digest().toHexString()
}
} }
packageName = arg<PackageInfo>(3).packageName packageName = arg<PackageInfo>(packageInfoIndex).packageName
dataIntercept = ByteArrayOutputStream()
callOriginal() callOriginal()
} }
coEvery {
spyFullRestore.initializeState(any(), any(), any())
} answers initializeStateBlock(1)
coEvery {
spyFullRestore.initializeStateV1(any(), any(), any())
} answers initializeStateBlock(2)
coEvery {
spyFullRestore.initializeStateV0(any(), any())
} answers initializeStateBlock(1)
every { every {
spyOutputFactory.getOutputStream(any()) spyOutputFactory.getOutputStream(any())
} answers { } answers {
OutputStreamIntercept( digestOutputStream = DigestOutputStream(callOriginal(), messageDigest)
outputStream = callOriginal(), digestOutputStream!!
intercept = dataIntercept
)
} }
every { every {
spyFullRestore.abortFullRestore() spyFullRestore.abortFullRestore()
} answers { } answers {
packageName = null packageName = null
dataIntercept = ByteArrayOutputStream() digestOutputStream?.messageDigest?.reset()
callOriginal() callOriginal()
} }
every { every {
spyFullRestore.finishRestore() spyFullRestore.finishRestore()
} answers { } answers {
restoreResult.full[packageName!!] = dataIntercept.toByteArray().sha256() val digest = digestOutputStream?.messageDigest ?: fail("No digestOutputStream")
restoreResult.full[packageName!!] = digest.digest().toHexString()
packageName = null packageName = null
dataIntercept = ByteArrayOutputStream() digest.reset()
callOriginal() callOriginal()
} }
} }

View file

@ -49,14 +49,14 @@ internal interface LargeTestBase : KoinComponent {
companion object { companion object {
private const val TEST_STORAGE_FOLDER = "seedvault_test" private const val TEST_STORAGE_FOLDER = "seedvault_test"
private const val TEST_VIDEO_FOLDER = "seedvault_test_results" private const val TEST_RESULT_FOLDER = "seedvault_test_results"
} }
val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath
val testStoragePath get() = "$externalStorageDir/$TEST_STORAGE_FOLDER" val testStoragePath get() = "$externalStorageDir/$TEST_STORAGE_FOLDER"
val testVideoPath get() = "$externalStorageDir/$TEST_VIDEO_FOLDER" val testResultPath get() = "$externalStorageDir/$TEST_RESULT_FOLDER"
val targetContext: Context val targetContext: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext get() = InstrumentationRegistry.getInstrumentation().targetContext
@ -123,7 +123,7 @@ internal interface LargeTestBase : KoinComponent {
keepRecordingScreen: AtomicBoolean, keepRecordingScreen: AtomicBoolean,
testName: String, testName: String,
) { ) {
val folder = testVideoPath val folder = testResultPath
runCommand("mkdir -p $folder") runCommand("mkdir -p $folder")
val fileName = testResultFilename(testName) val fileName = testResultFilename(testName)
@ -149,7 +149,7 @@ internal interface LargeTestBase : KoinComponent {
// write logcat to file // write logcat to file
val fileName = testResultFilename(testName) val fileName = testResultFilename(testName)
runCommand("logcat -d -f $testVideoPath/$fileName.log") runCommand("logcat -d -f $testResultPath/$fileName.log")
} }
fun uninstallPackages(packages: Collection<PackageInfo>) { fun uninstallPackages(packages: Collection<PackageInfo>) {
@ -162,7 +162,7 @@ internal interface LargeTestBase : KoinComponent {
fun clearTestBackups() { fun clearTestBackups() {
File(testStoragePath).deleteRecursively() File(testStoragePath).deleteRecursively()
File(testVideoPath).deleteRecursively() File(testResultPath).deleteRecursively()
} }
fun changeBackupLocation( fun changeBackupLocation(
@ -225,6 +225,7 @@ internal interface LargeTestBase : KoinComponent {
fun confirmCode() { fun confirmCode() {
RecoveryCodeScreen { RecoveryCodeScreen {
startNewBackupButton.click()
confirmCodeButton.click() confirmCodeButton.click()
verifyCodeButton.scrollTo().click() verifyCodeButton.scrollTo().click()

View file

@ -45,9 +45,8 @@ internal abstract class SeedvaultLargeTest :
clearTestBackups() clearTestBackups()
runCommand("bmgr enable true") runCommand("bmgr enable true")
sleep(60_000)
runCommand("bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport") runCommand("bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport")
sleep(60_000) sleep(5000)
startRecordingTest(keepRecordingScreen, name.methodName) startRecordingTest(keepRecordingScreen, name.methodName)
restoreBaselineBackup() restoreBaselineBackup()

View file

@ -5,11 +5,14 @@
package com.stevesoltys.seedvault.e2e.impl package com.stevesoltys.seedvault.e2e.impl
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
import com.stevesoltys.seedvault.metadata.PackageState import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.transport.backup.isStopped
import org.junit.Test import org.junit.Test
@LargeTest @LargeTest
@ -29,6 +32,9 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
changeBackupLocation() changeBackupLocation()
} }
launchStoppedApps()
launchBackupActivity()
val backupResult = performBackup() val backupResult = performBackup()
assertValidBackupMetadata(backupResult) assertValidBackupMetadata(backupResult)
@ -58,6 +64,28 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
} }
} }
private fun launchStoppedApps() {
val packageManager = targetContext.packageManager
val notBackedUp = packageService.notBackedUpPackages
notBackedUp.forEach { packageInfo ->
val i = packageManager.getLaunchIntentForPackage(packageInfo.packageName)?.apply {
addFlags(FLAG_ACTIVITY_NEW_TASK)
}
Log.i("TEST", "Launching $i")
try {
targetContext.startActivity(i)
} catch (e: Exception) {
Log.e("TEST", "Could not launch activity for ${packageInfo.packageName}", e)
}
waitUntilIdle()
}
waitUntilIdle()
notBackedUp.forEach { packageInfo ->
val pi = packageManager.getPackageInfo(packageInfo.packageName, 0)
Log.e("TEST", "${packageInfo.packageName} isStopped: ${pi.isStopped()}")
}
}
private fun assertValidResults( private fun assertValidResults(
backup: SeedvaultLargeTestResult, backup: SeedvaultLargeTestResult,
restore: SeedvaultLargeTestResult, restore: SeedvaultLargeTestResult,

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.e2e.io
import java.io.ByteArrayOutputStream
import java.io.InputStream
class InputStreamIntercept(
private val inputStream: InputStream,
private val intercept: ByteArrayOutputStream
) : InputStream() {
override fun read(): Int {
val byte = inputStream.read()
if (byte != -1) {
intercept.write(byte)
}
return byte
}
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
val bytesRead = inputStream.read(buffer, offset, length)
if (bytesRead != -1) {
intercept.write(buffer, offset, bytesRead)
}
return bytesRead
}
}

View file

@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.e2e.io
import java.io.ByteArrayOutputStream
import java.io.OutputStream
class OutputStreamIntercept(
private val outputStream: OutputStream,
private val intercept: ByteArrayOutputStream
) : OutputStream() {
override fun write(byte: Int) {
intercept.write(byte)
outputStream.write(byte)
}
override fun write(buffer: ByteArray, offset: Int, length: Int) {
intercept.write(buffer, offset, length)
outputStream.write(buffer, offset, length)
}
}

View file

@ -24,7 +24,7 @@ object BackupScreen : UiDeviceScreen<BackupScreen>() {
val internalStorageButton = findObject { textContains(Build.MODEL) } val internalStorageButton = findObject { textContains(Build.MODEL) }
val useAnywayButton = findObject { text("USE ANYWAY") } val useAnywayButton = findObject { text("Use anyway") }
val initializingText: BySelector = By.textContains("Initializing backup location") val initializingText: BySelector = By.textContains("Initializing backup location")
} }

View file

@ -9,6 +9,8 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
object RecoveryCodeScreen : UiDeviceScreen<RecoveryCodeScreen>() { object RecoveryCodeScreen : UiDeviceScreen<RecoveryCodeScreen>() {
val startNewBackupButton = findObject { text("Start new") }
val confirmCodeButton = findObject { text("Confirm code") } val confirmCodeButton = findObject { text("Confirm code") }
val verifyCodeButton = findObject { text("Verify") } val verifyCodeButton = findObject { text("Verify") }

View file

@ -9,7 +9,9 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
object RestoreScreen : UiDeviceScreen<RestoreScreen>() { object RestoreScreen : UiDeviceScreen<RestoreScreen>() {
val backupListItem = findObject { textContains("Last backup") } val backupListItem = findObject {
textContains("Android SDK") // device name of test backups
}
val appsSelectedButton = findObject { text("Restore backup") } val appsSelectedButton = findObject { text("Restore backup") }

View file

@ -13,7 +13,6 @@ import android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT
import android.app.backup.IBackupManagerMonitor import android.app.backup.IBackupManagerMonitor
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.util.Log.DEBUG
private val TAG = BackupMonitor::class.java.name private val TAG = BackupMonitor::class.java.name
@ -29,14 +28,11 @@ open class BackupMonitor : IBackupManagerMonitor.Stub() {
} }
open fun onEvent(id: Int, category: Int, packageName: String?, bundle: Bundle) { open fun onEvent(id: Int, category: Int, packageName: String?, bundle: Bundle) {
Log.d(TAG, "${packageName?.padEnd(64, ' ')} cat: $category id: $id")
if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) { if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) {
val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1) val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1)
Log.w(TAG, "Pre-flight error from $packageName: $preflightResult") Log.w(TAG, "Pre-flight error from $packageName: $preflightResult")
} }
if (!Log.isLoggable(TAG, DEBUG)) return
Log.d(TAG, "ID: $id")
Log.d(TAG, "CATEGORY: $category")
Log.d(TAG, "PACKAGE: $packageName")
} }
} }

View file

@ -25,7 +25,10 @@ class BackendManager(
backendFactory: BackendFactory, backendFactory: BackendFactory,
) { ) {
@Volatile
private var mBackend: Backend? private var mBackend: Backend?
@Volatile
private var mBackendProperties: BackendProperties<*>? private var mBackendProperties: BackendProperties<*>?
val backend: Backend val backend: Backend
@ -81,6 +84,8 @@ class BackendManager(
* IMPORTANT: Do no call this while current plugins are being used, * IMPORTANT: Do no call this while current plugins are being used,
* e.g. while backup/restore operation is still running. * e.g. while backup/restore operation is still running.
*/ */
@WorkerThread
@Synchronized
fun <T> changePlugins( fun <T> changePlugins(
backend: Backend, backend: Backend,
storageProperties: BackendProperties<T>, storageProperties: BackendProperties<T>,

View file

@ -90,6 +90,7 @@ internal class SafHandler(
return false return false
} }
@WorkerThread
fun setPlugin(safProperties: SafProperties) { fun setPlugin(safProperties: SafProperties) {
backendManager.changePlugins( backendManager.changePlugins(
backend = backendFactory.createSafBackend(safProperties), backend = backendFactory.createSafBackend(safProperties),

View file

@ -86,6 +86,7 @@ internal class WebDavHandler(
settingsManager.saveWebDavConfig(properties.config) settingsManager.saveWebDavConfig(properties.config)
} }
@WorkerThread
fun setPlugin(properties: WebDavProperties, backend: Backend) { fun setPlugin(properties: WebDavProperties, backend: Backend) {
backendManager.changePlugins( backendManager.changePlugins(
backend = backend, backend = backend,

View file

@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.repo
import android.content.Context import android.content.Context
import android.content.Context.MODE_APPEND import android.content.Context.MODE_APPEND
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.MemoryLogger import com.stevesoltys.seedvault.MemoryLogger
import com.stevesoltys.seedvault.proto.Snapshot import com.stevesoltys.seedvault.proto.Snapshot
import com.stevesoltys.seedvault.proto.Snapshot.Blob import com.stevesoltys.seedvault.proto.Snapshot.Blob
@ -90,6 +91,7 @@ class BlobCache(
* * changing to a different backup to prevent usage of blobs that don't exist there * * changing to a different backup to prevent usage of blobs that don't exist there
* * uploading a new snapshot to prevent the persistent cache from growing indefinitely * * uploading a new snapshot to prevent the persistent cache from growing indefinitely
*/ */
@WorkerThread
fun clearLocalCache() { fun clearLocalCache() {
log.info { "Clearing local cache..." } log.info { "Clearing local cache..." }
context.deleteFile(CACHE_FILE_NAME) context.deleteFile(CACHE_FILE_NAME)

View file

@ -161,6 +161,9 @@ internal class KVRestore(
} catch (e: AEADBadTagException) { } catch (e: AEADBadTagException) {
Log.e(TAG, "Decryption failed", e) Log.e(TAG, "Decryption failed", e)
TRANSPORT_ERROR TRANSPORT_ERROR
} catch (e: Exception) {
Log.e(TAG, "Unknown error", e)
TRANSPORT_ERROR
} finally { } finally {
dbManager.deleteDb(state.packageInfo.packageName, true) dbManager.deleteDb(state.packageInfo.packageName, true)
this.state = null this.state = null

View file

@ -107,10 +107,14 @@ class RecoveryCodeInputFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
view.requireViewById<Toolbar>(R.id.toolbar).apply { view.requireViewById<Toolbar>(R.id.toolbar).apply {
if (viewModel.isRestore) {
visibility = GONE
} else {
setNavigationOnClickListener { setNavigationOnClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed() requireActivity().onBackPressedDispatcher.onBackPressed()
} }
} }
}
if (viewModel.isRestore) { if (viewModel.isRestore) {
introText.setText(R.string.recovery_code_input_intro) introText.setText(R.string.recovery_code_input_intro)

View file

@ -10,6 +10,7 @@ import android.app.backup.IBackupManager
import android.app.job.JobInfo import android.app.job.JobInfo
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
@ -23,6 +24,7 @@ import com.stevesoltys.seedvault.worker.AppBackupWorker
import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
@ -46,9 +48,14 @@ internal class BackupStorageViewModel(
override val isRestoreOperation = false override val isRestoreOperation = false
@UiThread
override fun onSafUriSet(safProperties: SafProperties) { override fun onSafUriSet(safProperties: SafProperties) {
safHandler.save(safProperties) safHandler.save(safProperties)
viewModelScope.launch {
withContext(Dispatchers.IO) {
safHandler.setPlugin(safProperties) safHandler.setPlugin(safProperties)
}
withContext(Dispatchers.Main) { // UiThread
if (safProperties.isUsb) { if (safProperties.isUsb) {
// disable storage backup if new storage is on USB // disable storage backup if new storage is on USB
cancelBackupWorkers() cancelBackupWorkers()
@ -59,13 +66,21 @@ internal class BackupStorageViewModel(
} }
onStorageLocationSet(safProperties.isUsb) onStorageLocationSet(safProperties.isUsb)
} }
}
}
override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) { override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) {
webdavHandler.save(properties) webdavHandler.save(properties)
viewModelScope.launch {
withContext(Dispatchers.IO) {
webdavHandler.setPlugin(properties, backend) webdavHandler.setPlugin(properties, backend)
}
withContext(Dispatchers.Main) {
scheduleBackupWorkers() scheduleBackupWorkers()
onStorageLocationSet(isUsb = false) onStorageLocationSet(isUsb = false)
} }
}
}
private fun onStorageLocationSet(isUsb: Boolean) { private fun onStorageLocationSet(isUsb: Boolean) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {

View file

@ -124,6 +124,8 @@ class AppBackupWorker(
Result.retry() Result.retry()
} else { } else {
val result = doBackup() val result = doBackup()
// show error notification if backup wasn't successful (maybe only when no retry?)
if (result != Result.success()) nm.onBackupError()
// only allow retrying if rescheduling is allowed // only allow retrying if rescheduling is allowed
if (tags.contains(TAG_RESCHEDULE)) return result if (tags.contains(TAG_RESCHEDULE)) return result
else Result.success() else Result.success()

View file

@ -2,8 +2,6 @@
* SPDX-FileCopyrightText: 2020 The Calyx Institute * SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import java.io.FileInputStream
import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
@ -42,23 +40,18 @@ android {
isReturnDefaultValues = true isReturnDefaultValues = true
} }
// optional signingConfigs
// On userdebug builds, you can use the testkey here to update the system app
val keystorePropertiesFile = project.file("keystore.properties")
if (keystorePropertiesFile.exists()) {
val keystoreProperties = Properties()
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
signingConfigs { signingConfigs {
create("release") { create("aosp") {
keyAlias = keystoreProperties["keyAlias"] as String keyAlias = "android"
keyPassword = keystoreProperties["keyPassword"] as String keyPassword = "android"
storeFile = file(keystoreProperties["storeFile"] as String) storePassword = "android"
storePassword = keystoreProperties["storePassword"] as String storeFile = file("testkey.jks")
} }
} }
buildTypes.getByName("release").signingConfig = signingConfigs.getByName("release")
buildTypes.getByName("debug").signingConfig = signingConfigs.getByName("release") buildTypes {
getByName("release").signingConfig = signingConfigs.getByName("aosp")
getByName("debug").signingConfig = signingConfigs.getByName("aosp")
} }
} }
@ -74,6 +67,7 @@ dependencies {
androidTestImplementation(libs.kotlin.stdlib.jdk8) androidTestImplementation(libs.kotlin.stdlib.jdk8)
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation( androidTestImplementation(
"androidx.test.espresso:espresso-core:${libs.versions.espresso.get()}") "androidx.test.espresso:espresso-core:${libs.versions.espresso.get()}"
)
androidTestImplementation("io.mockk:mockk-android:${libs.versions.mockk.get()}") androidTestImplementation("io.mockk:mockk-android:${libs.versions.mockk.get()}")
} }

View file

@ -5,9 +5,12 @@
package org.calyxos.backup.contacts package org.calyxos.backup.contacts
import android.Manifest.permission.READ_CONTACTS
import android.Manifest.permission.WRITE_CONTACTS
import android.app.backup.BackupAgent import android.app.backup.BackupAgent
import android.app.backup.BackupAgent.TYPE_FILE import android.app.backup.BackupAgent.TYPE_FILE
import android.app.backup.FullBackupDataOutput import android.app.backup.FullBackupDataOutput
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@ -19,6 +22,7 @@ import org.calyxos.backup.contacts.ContactsBackupAgent.BACKUP_FILE
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File import java.io.File
@ -42,6 +46,9 @@ class BackupRestoreTest {
@Test @Test
fun testBackupAndRestore() { fun testBackupAndRestore() {
val hasReadPermission = context.checkSelfPermission(READ_CONTACTS) == PERMISSION_GRANTED
val hasWritePermission = context.checkSelfPermission(WRITE_CONTACTS) == PERMISSION_GRANTED
assumeTrue(hasReadPermission && hasWritePermission)
assertEquals( assertEquals(
"Test will remove *all* contacts and thus requires empty address book", "Test will remove *all* contacts and thus requires empty address book",
0, 0,

BIN
contactsbackup/testkey.jks Normal file

Binary file not shown.