Merge pull request #748 from seedvault-app/feature/cirrus-ci-tests
Run tests in Cirrus CI again
This commit is contained in:
commit
0fa14025b3
24 changed files with 234 additions and 154 deletions
77
.cirrus.yml
77
.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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Boolean>()
|
||||
assert(success) { "Backup failed." }
|
||||
|
||||
callOriginal()
|
||||
completed.set(true)
|
||||
}
|
||||
|
|
|
@ -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<PackageInfo>(3).packageName
|
||||
fun initializeStateBlock(
|
||||
packageInfoIndex: Int
|
||||
): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
|
||||
packageName = arg<PackageInfo>(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<Unit, Unit>.(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<PackageInfo>(3).packageName
|
||||
dataIntercept = ByteArrayOutputStream()
|
||||
packageName = arg<PackageInfo>(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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<PackageInfo>) {
|
||||
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ object BackupScreen : UiDeviceScreen<BackupScreen>() {
|
|||
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
|
|||
|
||||
object RecoveryCodeScreen : UiDeviceScreen<RecoveryCodeScreen>() {
|
||||
|
||||
val startNewBackupButton = findObject { text("Start new") }
|
||||
|
||||
val confirmCodeButton = findObject { text("Confirm code") }
|
||||
|
||||
val verifyCodeButton = findObject { text("Verify") }
|
||||
|
|
|
@ -9,7 +9,9 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
|
|||
|
||||
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") }
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <T> changePlugins(
|
||||
backend: Backend,
|
||||
storageProperties: BackendProperties<T>,
|
||||
|
|
|
@ -90,6 +90,7 @@ internal class SafHandler(
|
|||
return false
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun setPlugin(safProperties: SafProperties) {
|
||||
backendManager.changePlugins(
|
||||
backend = backendFactory.createSafBackend(safProperties),
|
||||
|
|
|
@ -86,6 +86,7 @@ internal class WebDavHandler(
|
|||
settingsManager.saveWebDavConfig(properties.config)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun setPlugin(properties: WebDavProperties, backend: Backend) {
|
||||
backendManager.changePlugins(
|
||||
backend = backend,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -107,8 +107,12 @@ class RecoveryCodeInputFragment : Fragment() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view.requireViewById<Toolbar>(R.id.toolbar).apply {
|
||||
setNavigationOnClickListener {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
if (viewModel.isRestore) {
|
||||
visibility = GONE
|
||||
} else {
|
||||
setNavigationOnClickListener {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()}")
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
BIN
contactsbackup/testkey.jks
Normal file
BIN
contactsbackup/testkey.jks
Normal file
Binary file not shown.
Loading…
Reference in a new issue