diff --git a/.cirrus.yml b/.cirrus.yml index d150203a..380e5ddc 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,13 +1,66 @@ -task: - name: Build with AOSP - only_if: $CIRRUS_PR_LABELS =~ ".*aosp-build.*" - timeout_in: 70m - container: - image: ubuntu:23.04 - cpu: 8 - memory: 32G - build_script: - - ./.github/scripts/build_aosp.sh aosp_arm64 ap1a userdebug android-14.0.0_r29 +container: + image: ghcr.io/cirruslabs/android-sdk:34 + kvm: true + cpu: 8 + memory: 16G + +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: - seedvault_artifacts: - path: Seedvault.apk + pull_screenshots_script: + adb pull /sdcard/seedvault_test_results + screenshots_artifacts: + path: "seedvault_test_results/**/*.mp4" + logcat_artifacts: + path: "seedvault_test_results/**/*.log" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6a577a10..c1bb3a49 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,13 +32,6 @@ android { versionNameSuffix = "-${gitDescribe()}" testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner" 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 { diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt index ab4e1fdf..45eb730a 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt @@ -9,7 +9,6 @@ import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor import androidx.test.uiautomator.Until 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.transport.backup.FullBackup import com.stevesoltys.seedvault.transport.backup.InputFactory @@ -21,8 +20,10 @@ import io.mockk.every import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import org.calyxos.seedvault.core.toHexString 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 kotlin.test.fail @@ -154,7 +155,8 @@ internal interface LargeBackupTestBase : LargeTestBase { private fun spyOnFullBackupData(backupResult: SeedvaultLargeTestResult) { var packageName: String? = null - var dataIntercept = ByteArrayOutputStream() + val messageDigest = MessageDigest.getInstance("SHA-256") + var digestInputStream: DigestInputStream? = null coEvery { spyFullBackup.performFullBackup(any(), any(), any()) @@ -166,20 +168,19 @@ internal interface LargeBackupTestBase : LargeTestBase { every { spyInputFactory.getInputStream(any()) } answers { - InputStreamIntercept( - inputStream = callOriginal(), - intercept = dataIntercept - ) + digestInputStream = DigestInputStream(callOriginal(), messageDigest) + digestInputStream!! } coEvery { spyFullBackup.finishBackup() } answers { val result = callOriginal() - backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256() + val digest = digestInputStream?.messageDigest ?: fail("No digestInputStream") + backupResult.full[packageName!!] = digest.digest().toHexString() packageName = null - dataIntercept = ByteArrayOutputStream() + digest.reset() result } } @@ -192,9 +193,6 @@ internal interface LargeBackupTestBase : LargeTestBase { every { spyBackupNotificationManager.onBackupSuccess(any(), any(), any()) } answers { - val success = firstArg() - assert(success) { "Backup failed." } - callOriginal() completed.set(true) } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt index a2384191..133a27d2 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt @@ -8,12 +8,13 @@ package com.stevesoltys.seedvault.e2e import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor 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.RestoreScreen import com.stevesoltys.seedvault.transport.restore.FullRestore import com.stevesoltys.seedvault.transport.restore.KVRestore import com.stevesoltys.seedvault.transport.restore.OutputFactory +import io.mockk.Call +import io.mockk.MockKAnswerScope import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every @@ -22,8 +23,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import org.calyxos.seedvault.core.toHexString 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 { @@ -161,14 +165,26 @@ internal interface LargeRestoreTestBase : LargeTestBase { clearMocks(spyKVRestore) - coEvery { - spyKVRestore.initializeState(any(), any(), any(), any()) - } answers { - packageName = arg(3).packageName + fun initializeStateBlock( + packageInfoIndex: Int + ): MockKAnswerScope.(Call) -> Unit = { + packageName = arg(packageInfoIndex).packageName restoreResult.kv[packageName!!] = mutableMapOf() 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 { spyOutputFactory.getBackupDataOutput(any()) } answers { @@ -182,47 +198,61 @@ internal interface LargeRestoreTestBase : LargeTestBase { private fun spyOnFullRestoreData(restoreResult: SeedvaultLargeTestResult) { var packageName: String? = null - var dataIntercept = ByteArrayOutputStream() + val messageDigest = MessageDigest.getInstance("SHA-256") + var digestOutputStream: DigestOutputStream? = null clearMocks(spyFullRestore) - coEvery { - spyFullRestore.initializeState(any(), any(), any()) - } answers { + fun initializeStateBlock( + packageInfoIndex: Int + ): MockKAnswerScope.(Call) -> Unit = { 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(3).packageName - dataIntercept = ByteArrayOutputStream() + packageName = arg(packageInfoIndex).packageName 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 { spyOutputFactory.getOutputStream(any()) } answers { - OutputStreamIntercept( - outputStream = callOriginal(), - intercept = dataIntercept - ) + digestOutputStream = DigestOutputStream(callOriginal(), messageDigest) + digestOutputStream!! } every { spyFullRestore.abortFullRestore() } answers { packageName = null - dataIntercept = ByteArrayOutputStream() + digestOutputStream?.messageDigest?.reset() callOriginal() } every { spyFullRestore.finishRestore() } answers { - restoreResult.full[packageName!!] = dataIntercept.toByteArray().sha256() + val digest = digestOutputStream?.messageDigest ?: fail("No digestOutputStream") + restoreResult.full[packageName!!] = digest.digest().toHexString() packageName = null - dataIntercept = ByteArrayOutputStream() + digest.reset() callOriginal() } } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt index 467b5a28..822d2e9e 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt @@ -49,14 +49,14 @@ internal interface LargeTestBase : KoinComponent { companion object { 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 testStoragePath get() = "$externalStorageDir/$TEST_STORAGE_FOLDER" - val testVideoPath get() = "$externalStorageDir/$TEST_VIDEO_FOLDER" + val testResultPath get() = "$externalStorageDir/$TEST_RESULT_FOLDER" val targetContext: Context get() = InstrumentationRegistry.getInstrumentation().targetContext @@ -123,7 +123,7 @@ internal interface LargeTestBase : KoinComponent { keepRecordingScreen: AtomicBoolean, testName: String, ) { - val folder = testVideoPath + val folder = testResultPath runCommand("mkdir -p $folder") val fileName = testResultFilename(testName) @@ -149,7 +149,7 @@ internal interface LargeTestBase : KoinComponent { // write logcat to file val fileName = testResultFilename(testName) - runCommand("logcat -d -f $testVideoPath/$fileName.log") + runCommand("logcat -d -f $testResultPath/$fileName.log") } fun uninstallPackages(packages: Collection) { @@ -162,7 +162,7 @@ internal interface LargeTestBase : KoinComponent { fun clearTestBackups() { File(testStoragePath).deleteRecursively() - File(testVideoPath).deleteRecursively() + File(testResultPath).deleteRecursively() } fun changeBackupLocation( @@ -225,6 +225,7 @@ internal interface LargeTestBase : KoinComponent { fun confirmCode() { RecoveryCodeScreen { + startNewBackupButton.click() confirmCodeButton.click() verifyCodeButton.scrollTo().click() diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt index 0c83cfa1..8e5c79f7 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt @@ -45,9 +45,8 @@ internal abstract class SeedvaultLargeTest : clearTestBackups() runCommand("bmgr enable true") - sleep(60_000) runCommand("bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport") - sleep(60_000) + sleep(5000) startRecordingTest(keepRecordingScreen, name.methodName) restoreBaselineBackup() diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt index 3017a83d..12fc3fb7 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt @@ -5,11 +5,14 @@ package com.stevesoltys.seedvault.e2e.impl +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.util.Log import androidx.test.filters.LargeTest import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult import com.stevesoltys.seedvault.metadata.PackageState +import com.stevesoltys.seedvault.transport.backup.isStopped import org.junit.Test @LargeTest @@ -29,6 +32,9 @@ internal class BackupRestoreTest : SeedvaultLargeTest() { changeBackupLocation() } + launchStoppedApps() + launchBackupActivity() + val backupResult = performBackup() 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( backup: SeedvaultLargeTestResult, restore: SeedvaultLargeTestResult, diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/io/InputStreamIntercept.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/io/InputStreamIntercept.kt deleted file mode 100644 index 30c4fdf5..00000000 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/io/InputStreamIntercept.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/io/OutputStreamIntercept.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/io/OutputStreamIntercept.kt deleted file mode 100644 index d82f0b06..00000000 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/io/OutputStreamIntercept.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt index 17cdfc64..65e9683b 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt @@ -24,7 +24,7 @@ object BackupScreen : UiDeviceScreen() { 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") } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt index 244ca24e..391283c1 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt @@ -9,6 +9,8 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen object RecoveryCodeScreen : UiDeviceScreen() { + val startNewBackupButton = findObject { text("Start new") } + val confirmCodeButton = findObject { text("Confirm code") } val verifyCodeButton = findObject { text("Verify") } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt index e17e6d1e..59686d63 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt @@ -9,7 +9,9 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen object RestoreScreen : UiDeviceScreen() { - val backupListItem = findObject { textContains("Last backup") } + val backupListItem = findObject { + textContains("Android SDK") // device name of test backups + } val appsSelectedButton = findObject { text("Restore backup") } diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt index b9c6f70e..79df7f18 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt @@ -13,7 +13,6 @@ import android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT import android.app.backup.IBackupManagerMonitor import android.os.Bundle import android.util.Log -import android.util.Log.DEBUG 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) { + Log.d(TAG, "${packageName?.padEnd(64, ' ')} cat: $category id: $id") if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) { val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1) 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") } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/backend/BackendManager.kt b/app/src/main/java/com/stevesoltys/seedvault/backend/BackendManager.kt index a4f2d01f..ebc6d57e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/backend/BackendManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/backend/BackendManager.kt @@ -25,7 +25,10 @@ class BackendManager( backendFactory: BackendFactory, ) { + @Volatile private var mBackend: Backend? + + @Volatile private var mBackendProperties: BackendProperties<*>? val backend: Backend @@ -81,6 +84,8 @@ class BackendManager( * IMPORTANT: Do no call this while current plugins are being used, * e.g. while backup/restore operation is still running. */ + @WorkerThread + @Synchronized fun changePlugins( backend: Backend, storageProperties: BackendProperties, diff --git a/app/src/main/java/com/stevesoltys/seedvault/backend/saf/SafHandler.kt b/app/src/main/java/com/stevesoltys/seedvault/backend/saf/SafHandler.kt index b296bc57..d5610a4c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/backend/saf/SafHandler.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/backend/saf/SafHandler.kt @@ -90,6 +90,7 @@ internal class SafHandler( return false } + @WorkerThread fun setPlugin(safProperties: SafProperties) { backendManager.changePlugins( backend = backendFactory.createSafBackend(safProperties), diff --git a/app/src/main/java/com/stevesoltys/seedvault/backend/webdav/WebDavHandler.kt b/app/src/main/java/com/stevesoltys/seedvault/backend/webdav/WebDavHandler.kt index 07e80888..6ead5393 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/backend/webdav/WebDavHandler.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/backend/webdav/WebDavHandler.kt @@ -86,6 +86,7 @@ internal class WebDavHandler( settingsManager.saveWebDavConfig(properties.config) } + @WorkerThread fun setPlugin(properties: WebDavProperties, backend: Backend) { backendManager.changePlugins( backend = backend, diff --git a/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt b/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt index 66204289..16085512 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt @@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.repo import android.content.Context import android.content.Context.MODE_APPEND +import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.MemoryLogger import com.stevesoltys.seedvault.proto.Snapshot 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 * * uploading a new snapshot to prevent the persistent cache from growing indefinitely */ + @WorkerThread fun clearLocalCache() { log.info { "Clearing local cache..." } context.deleteFile(CACHE_FILE_NAME) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt index 5b645e11..97a323ee 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt @@ -161,6 +161,9 @@ internal class KVRestore( } catch (e: AEADBadTagException) { Log.e(TAG, "Decryption failed", e) TRANSPORT_ERROR + } catch (e: Exception) { + Log.e(TAG, "Unknown error", e) + TRANSPORT_ERROR } finally { dbManager.deleteDb(state.packageInfo.packageName, true) this.state = null diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt index bfa8db60..77ec609d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt @@ -107,8 +107,12 @@ class RecoveryCodeInputFragment : Fragment() { super.onViewCreated(view, savedInstanceState) view.requireViewById(R.id.toolbar).apply { - setNavigationOnClickListener { - requireActivity().onBackPressedDispatcher.onBackPressed() + if (viewModel.isRestore) { + visibility = GONE + } else { + setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index c57c7250..fd39b6b2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -10,6 +10,7 @@ import android.app.backup.IBackupManager import android.app.job.JobInfo import android.os.UserHandle import android.util.Log +import androidx.annotation.UiThread import androidx.lifecycle.viewModelScope import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import com.stevesoltys.seedvault.R @@ -23,6 +24,7 @@ import com.stevesoltys.seedvault.worker.AppBackupWorker import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.backup.BackupJobService import org.calyxos.seedvault.core.backends.Backend @@ -46,25 +48,38 @@ internal class BackupStorageViewModel( override val isRestoreOperation = false + @UiThread override fun onSafUriSet(safProperties: SafProperties) { safHandler.save(safProperties) - safHandler.setPlugin(safProperties) - if (safProperties.isUsb) { - // disable storage backup if new storage is on USB - cancelBackupWorkers() - } else { - // enable it, just in case the previous storage was on USB, - // also to update the network requirement of the new storage - scheduleBackupWorkers() + viewModelScope.launch { + withContext(Dispatchers.IO) { + safHandler.setPlugin(safProperties) + } + withContext(Dispatchers.Main) { // UiThread + if (safProperties.isUsb) { + // disable storage backup if new storage is on USB + cancelBackupWorkers() + } else { + // enable it, just in case the previous storage was on USB, + // also to update the network requirement of the new storage + scheduleBackupWorkers() + } + onStorageLocationSet(safProperties.isUsb) + } } - onStorageLocationSet(safProperties.isUsb) } override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) { webdavHandler.save(properties) - webdavHandler.setPlugin(properties, backend) - scheduleBackupWorkers() - onStorageLocationSet(isUsb = false) + viewModelScope.launch { + withContext(Dispatchers.IO) { + webdavHandler.setPlugin(properties, backend) + } + withContext(Dispatchers.Main) { + scheduleBackupWorkers() + onStorageLocationSet(isUsb = false) + } + } } private fun onStorageLocationSet(isUsb: Boolean) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index dd07d77e..5ba281f6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -124,6 +124,8 @@ class AppBackupWorker( Result.retry() } else { 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 if (tags.contains(TAG_RESCHEDULE)) return result else Result.success() diff --git a/contactsbackup/build.gradle.kts b/contactsbackup/build.gradle.kts index adbb4712..a59e8c0f 100644 --- a/contactsbackup/build.gradle.kts +++ b/contactsbackup/build.gradle.kts @@ -2,8 +2,6 @@ * SPDX-FileCopyrightText: 2020 The Calyx Institute * SPDX-License-Identifier: Apache-2.0 */ -import java.io.FileInputStream -import java.util.Properties plugins { alias(libs.plugins.android.application) @@ -42,23 +40,18 @@ android { 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 { - create("release") { - keyAlias = keystoreProperties["keyAlias"] as String - keyPassword = keystoreProperties["keyPassword"] as String - storeFile = file(keystoreProperties["storeFile"] as String) - storePassword = keystoreProperties["storePassword"] as String - } + signingConfigs { + create("aosp") { + keyAlias = "android" + keyPassword = "android" + storePassword = "android" + 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("androidx.test.ext:junit:1.1.5") 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()}") } diff --git a/contactsbackup/src/androidTest/java/org/calyxos/backup/contacts/BackupRestoreTest.kt b/contactsbackup/src/androidTest/java/org/calyxos/backup/contacts/BackupRestoreTest.kt index bd6f5237..c1e13529 100644 --- a/contactsbackup/src/androidTest/java/org/calyxos/backup/contacts/BackupRestoreTest.kt +++ b/contactsbackup/src/androidTest/java/org/calyxos/backup/contacts/BackupRestoreTest.kt @@ -5,9 +5,12 @@ 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.TYPE_FILE import android.app.backup.FullBackupDataOutput +import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor.MODE_READ_ONLY 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.assertNull import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import java.io.File @@ -42,6 +46,9 @@ class BackupRestoreTest { @Test fun testBackupAndRestore() { + val hasReadPermission = context.checkSelfPermission(READ_CONTACTS) == PERMISSION_GRANTED + val hasWritePermission = context.checkSelfPermission(WRITE_CONTACTS) == PERMISSION_GRANTED + assumeTrue(hasReadPermission && hasWritePermission) assertEquals( "Test will remove *all* contacts and thus requires empty address book", 0, diff --git a/contactsbackup/testkey.jks b/contactsbackup/testkey.jks new file mode 100644 index 00000000..6de45e18 Binary files /dev/null and b/contactsbackup/testkey.jks differ