diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 00000000..d2f6c6c1
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,53 @@
+container:
+ image: ghcr.io/cirruslabs/android-sdk:33
+ kvm: true
+ cpu: 8
+ memory: 16G
+
+check_android_task:
+ skip: "!changesInclude('.cirrus.yml', '*.gradle', '*.gradle.kts', '**/*.gradle', '**/*.gradle.kts', '*.properties', '**/*.properties', '**/*.kt', '**/*.xml')"
+ create_avd_script:
+ start_avd_background_script:
+ sdkmanager --install "system-images;android-33;google_apis;x86_64";
+ echo no | avdmanager create avd -n seedvault -k "system-images;android-33;google_apis;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:
+ adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
+ wget --output-document etar.apk https://f-droid.org/repo/ws.xsoh.etar_35.apk;
+ adb install etar.apk
+
+ 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;
+ assemble_release_script:
+ ./gradlew :app:assembleRelease :app: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 /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 bmgr enable true;
+ 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 -Pinstrumented_test_size=large :app:connectedAndroidTest
+ run_medium_tests_script: ./gradlew -Pinstrumented_test_size=medium :app:connectedAndroidTest
+ always:
+ pull_screenshots_script:
+ adb pull /sdcard/Documents/screenshots
+ screenshots_artifacts:
+ path: "screenshots/**/*.png"
diff --git a/.idea/runConfigurations/app_emulator.xml b/.idea/runConfigurations/app_emulator.xml
index 0579cb25..60f48b57 100644
--- a/.idea/runConfigurations/app_emulator.xml
+++ b/.idea/runConfigurations/app_emulator.xml
@@ -17,7 +17,7 @@
-
+
@@ -37,7 +37,7 @@
-
+
@@ -62,7 +62,7 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index d08c8286..6323ce3f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,8 +21,15 @@ android {
minSdk 32 // leave at 32 for robolectric tests
targetSdk rootProject.ext.targetSdk
versionNameSuffix "-$gitDescribe"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
testInstrumentationRunnerArguments disableAnalytics: 'true'
+
+ if (project.hasProperty('instrumented_test_size')) {
+ final testSize = project.getProperty('instrumented_test_size')
+ println("Instrumented test size: $testSize")
+
+ testInstrumentationRunnerArguments size: testSize
+ }
}
buildTypes {
@@ -150,10 +157,13 @@ dependencies {
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit5_version"
+ androidTestImplementation rootProject.ext.aosp_libs
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
+
+ androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.5.3'
}
apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
@@ -210,3 +220,14 @@ tasks.register('installEmulatorRelease', Exec) {
environment "JAVA_HOME", System.properties['java.home']
}
}
+
+tasks.register('clearEmulatorAppData', Exec) {
+ group("emulator")
+
+ doFirst {
+ commandLine "${project.projectDir}/development/scripts/clear_app_data.sh"
+
+ environment "ANDROID_SDK_HOME", android.sdkDirectory.absolutePath
+ environment "JAVA_HOME", System.properties['java.home']
+ }
+}
diff --git a/app/development/scripts/clear_app_data.sh b/app/development/scripts/clear_app_data.sh
new file mode 100755
index 00000000..fbde98b6
--- /dev/null
+++ b/app/development/scripts/clear_app_data.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+# assert ANDROID_HOME is set
+if [ -z "$ANDROID_SDK_HOME" ]; then
+ echo "ANDROID_SDK_HOME is not set"
+ exit 1
+fi
+
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+DEVELOPMENT_DIR=$SCRIPT_DIR/..
+ROOT_PROJECT_DIR=$SCRIPT_DIR/../../..
+
+EMULATOR_DEVICE_NAME=$($ANDROID_SDK_HOME/platform-tools/adb devices | grep emulator | cut -f1)
+
+if [ -z "$EMULATOR_DEVICE_NAME" ]; then
+ echo "Emulator device name not found"
+ exit 1
+fi
+
+ADB="$ANDROID_SDK_HOME/platform-tools/adb -s $EMULATOR_DEVICE_NAME"
+
+$ADB shell pm clear com.stevesoltys.seedvault
diff --git a/app/development/scripts/provision_emulator.sh b/app/development/scripts/provision_emulator.sh
index 5c4bfd7f..b6c0623a 100755
--- a/app/development/scripts/provision_emulator.sh
+++ b/app/development/scripts/provision_emulator.sh
@@ -31,7 +31,6 @@ else
sleep 1
fi
-echo "Starting emulator..."
$SCRIPT_DIR/start_emulator.sh "$EMULATOR_NAME"
sleep 3
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
new file mode 100644
index 00000000..8335c720
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
@@ -0,0 +1,35 @@
+package com.stevesoltys.seedvault
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.stevesoltys.seedvault.restore.RestoreViewModel
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
+import io.mockk.spyk
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+private val spyBackupNotificationManager = spyk(
+ BackupNotificationManager(
+ InstrumentationRegistry.getInstrumentation()
+ .targetContext.applicationContext
+ )
+)
+
+class KoinInstrumentationTestApp : App() {
+
+ override fun appModules(): List {
+ val testModule = module {
+ single { spyBackupNotificationManager }
+
+ single {
+ spyk(
+ RestoreViewModel(
+ this@KoinInstrumentationTestApp,
+ get(), get(), get(), get(), get(), get()
+ )
+ )
+ }
+ }
+
+ return super.appModules().plus(testModule)
+ }
+}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestRunner.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestRunner.kt
new file mode 100644
index 00000000..1ee3fe99
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestRunner.kt
@@ -0,0 +1,20 @@
+package com.stevesoltys.seedvault
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+
+class KoinInstrumentationTestRunner : AndroidJUnitRunner() {
+
+ override fun newApplication(
+ classLoader: ClassLoader?,
+ className: String?,
+ context: Context?,
+ ): Application {
+ return super.newApplication(
+ classLoader,
+ KoinInstrumentationTestApp::class.java.name,
+ context
+ )
+ }
+}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
index 3188686b..f5dddd70 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
@@ -2,6 +2,7 @@ package com.stevesoltys.seedvault
import androidx.test.core.content.pm.PackageInfoBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
@@ -28,6 +29,7 @@ import org.koin.core.component.inject
@RunWith(AndroidJUnit4::class)
@Suppress("BlockingMethodInNonBlockingContext")
+@MediumTest
class PluginTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt
new file mode 100644
index 00000000..1542cafb
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt
@@ -0,0 +1,139 @@
+package com.stevesoltys.seedvault.e2e
+
+import androidx.test.filters.LargeTest
+import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
+import com.stevesoltys.seedvault.restore.RestoreViewModel
+import com.stevesoltys.seedvault.transport.backup.PackageService
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
+import io.mockk.every
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import org.koin.core.component.inject
+import java.util.concurrent.atomic.AtomicBoolean
+
+@LargeTest
+class BackupRestoreTest : LargeTestBase() {
+
+ private val packageService: PackageService by inject()
+
+ private val spyBackupNotificationManager: BackupNotificationManager by inject()
+
+ private val restoreViewModel: RestoreViewModel by inject()
+
+ companion object {
+ private const val BACKUP_TIMEOUT = 360 * 1000L
+ private const val RESTORE_TIMEOUT = 360 * 1000L
+ }
+
+ @Test
+ fun `backup and restore applications`() = run {
+ launchBackupActivity()
+ verifyCode()
+ chooseBackupLocation()
+
+ val eligiblePackages = launchAllEligibleApps()
+ performBackup(eligiblePackages)
+ uninstallPackages(eligiblePackages)
+ performRestore()
+
+ val packagesAfterRestore = getEligibleApps()
+ assert(eligiblePackages == packagesAfterRestore)
+ }
+
+ private fun getEligibleApps() = packageService.userApps
+ .map { it.packageName }.toSet()
+
+ private fun launchAllEligibleApps(): Set {
+ return getEligibleApps().onEach {
+ val intent = device.targetContext.packageManager.getLaunchIntentForPackage(it)
+
+ device.targetContext.startActivity(intent)
+ waitUntilIdle()
+ }
+ }
+
+ private fun performBackup(expectedPackages: Set) = run {
+ val backupResult = spyOnBackup(expectedPackages)
+ startBackup()
+ waitForBackupResult(backupResult)
+ screenshot("backup result")
+ }
+
+ private fun spyOnBackup(expectedPackages: Set): AtomicBoolean {
+ val finishedBackup = AtomicBoolean(false)
+
+ every {
+ spyBackupNotificationManager.onBackupFinished(any(), any())
+ } answers {
+ val success = firstArg()
+ assert(success) { "Backup failed." }
+
+ val packageCount = secondArg()
+ assert(packageCount == expectedPackages.size) {
+ "Expected ${expectedPackages.size} apps, got $packageCount."
+ }
+
+ this.callOriginal()
+ finishedBackup.set(true)
+ }
+
+ return finishedBackup
+ }
+
+ private fun waitForBackupResult(finishedBackup: AtomicBoolean) = run {
+ step("Wait for backup completion") {
+ runBlocking {
+ withTimeout(BACKUP_TIMEOUT) {
+ while (!finishedBackup.get()) {
+ delay(100)
+ }
+ }
+ }
+ }
+ }
+
+ private fun performRestore() = run {
+ step("Start restore and await completion") {
+ RestoreScreen {
+ startRestore()
+ waitForInstallResult()
+ screenshot("restore app apks result")
+
+ nextButton.click()
+ waitForRestoreResult()
+ screenshot("restore app data result")
+
+ finishButton.click()
+ }
+ }
+ }
+
+ private fun waitForInstallResult() = runBlocking {
+ withTimeout(RESTORE_TIMEOUT) {
+
+ while (restoreViewModel.installResult.value == null) {
+ delay(100)
+ }
+
+ val restoreResultValue = restoreViewModel.installResult.value!!
+ assert(!restoreResultValue.hasFailed) { "Failed to install packages" }
+ }
+ }
+
+ private fun waitForRestoreResult() = runBlocking {
+ withTimeout(RESTORE_TIMEOUT) {
+
+ while (restoreViewModel.restoreBackupResult.value == null) {
+ delay(100)
+ }
+
+ val restoreResultValue = restoreViewModel.restoreBackupResult.value!!
+
+ assert(!restoreResultValue.hasError()) {
+ "Restore failed: ${restoreResultValue.errorMsg}"
+ }
+ }
+ }
+}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
new file mode 100644
index 00000000..fc0c01ed
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
@@ -0,0 +1,110 @@
+package com.stevesoltys.seedvault.e2e
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
+import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen
+import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
+import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
+import org.junit.After
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.koin.core.component.KoinComponent
+import java.lang.Thread.sleep
+
+@RunWith(AndroidJUnit4::class)
+abstract class LargeTestBase : TestCase(), KoinComponent {
+
+ @Before
+ open fun setUp() {
+ // reset document picker state, and delete old backups
+ runCommand("pm clear com.google.android.documentsui")
+ runCommand("rm -Rf /sdcard/seedvault")
+ }
+
+ @After
+ open fun tearDown() {
+ screenshot("end")
+ }
+
+ protected fun launchBackupActivity() = run {
+ runCommand("am start -n ${device.targetContext.packageName}/.settings.SettingsActivity")
+ waitUntilIdle()
+ }
+
+ protected fun launchRestoreActivity() = run {
+ runCommand("am start -n ${device.targetContext.packageName}/.restore.RestoreActivity")
+ waitUntilIdle()
+ }
+
+ protected fun waitUntilIdle() {
+ device.uiDevice.waitForIdle()
+ sleep(3000)
+ }
+
+ protected fun verifyCode() = run {
+ RecoveryCodeScreen {
+ step("Confirm code") {
+ screenshot("confirm code")
+ confirmCodeButton.click()
+ }
+ step("Verify code") {
+ screenshot("verify code")
+ verifyCodeButton.scrollTo()
+ verifyCodeButton.click()
+ }
+ }
+ }
+
+ protected fun chooseBackupLocation() = run {
+ step("Choose backup location") {
+ waitUntilIdle()
+ screenshot("choose backup location")
+
+ DocumentPickerScreen {
+ createNewFolderButton.click()
+ textBox.text = "seedvault"
+ okButton.click()
+ useThisFolderButton.click()
+ allowButton.click()
+ }
+ }
+ }
+
+ protected fun startBackup() = run {
+ launchBackupActivity()
+
+ step("Run backup") {
+ BackupScreen {
+ backupMenu.clickAndWaitForNewWindow()
+ backupNowButton.clickAndWaitForNewWindow()
+ backupStatusButton.clickAndWaitForNewWindow()
+ }
+ }
+ }
+
+ protected fun startRestore() = run {
+ launchRestoreActivity()
+
+ step("Restore backup") {
+ RestoreScreen {
+ backupListItem.clickAndWaitForNewWindow()
+ }
+ }
+ }
+
+ protected fun uninstallPackages(packages: Set) {
+ packages.forEach { runCommand("pm uninstall $it") }
+ }
+
+ protected fun runCommand(command: String) {
+ InstrumentationRegistry.getInstrumentation().uiAutomation
+ .executeShellCommand(command)
+ .close()
+ }
+
+ protected fun screenshot(name: String) {
+ device.screenshots.take(name.replace(" ", "_"))
+ }
+}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt
new file mode 100644
index 00000000..bddc428a
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt
@@ -0,0 +1,20 @@
+package com.stevesoltys.seedvault.e2e.screen
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject
+import androidx.test.uiautomator.UiSelector
+
+abstract class UiDeviceScreen {
+
+ operator fun invoke(function: T.() -> Unit) {
+ function.invoke(this as T)
+ }
+
+ protected fun findObject(
+ block: UiSelector.() -> UiSelector,
+ ): UiObject = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ .findObject(
+ UiSelector().let { it.block() }
+ )
+}
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
new file mode 100644
index 00000000..a9ce5c5e
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt
@@ -0,0 +1,12 @@
+package com.stevesoltys.seedvault.e2e.screen.impl
+
+import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
+
+object BackupScreen : UiDeviceScreen() {
+
+ val backupMenu = findObject { description("More options") }
+
+ val backupNowButton = findObject { text("Backup now") }
+
+ val backupStatusButton = findObject { text("Backup status") }
+}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt
new file mode 100644
index 00000000..f6f101e4
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt
@@ -0,0 +1,17 @@
+package com.stevesoltys.seedvault.e2e.screen.impl
+
+import android.widget.EditText
+import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
+
+object DocumentPickerScreen : UiDeviceScreen() {
+
+ val createNewFolderButton = findObject { text("CREATE NEW FOLDER") }
+
+ val useThisFolderButton = findObject { text("USE THIS FOLDER") }
+
+ val textBox = findObject { className(EditText::class.java) }
+
+ val okButton = findObject { text("OK") }
+
+ val allowButton = findObject { text("ALLOW") }
+}
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
new file mode 100644
index 00000000..3ce61511
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt
@@ -0,0 +1,15 @@
+package com.stevesoltys.seedvault.e2e.screen.impl
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.stevesoltys.seedvault.R
+import io.github.kakaocup.kakao.text.KButton
+
+object RecoveryCodeScreen : KScreen() {
+
+ override val layoutId: Int? = null
+ override val viewClass: Class<*>? = null
+
+ val confirmCodeButton = KButton { withId(R.id.confirmCodeButton) }
+
+ val verifyCodeButton = KButton { withId(R.id.doneButton) }
+}
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
new file mode 100644
index 00000000..706fc7bf
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt
@@ -0,0 +1,12 @@
+package com.stevesoltys.seedvault.e2e.screen.impl
+
+import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
+
+object RestoreScreen : UiDeviceScreen() {
+
+ val backupListItem = findObject { textContains("Last backup 0 hr") }
+
+ val nextButton = findObject { text("Next") }
+
+ val finishButton = findObject { text("Finish") }
+}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
index d6bdf680..353d7680 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.provider.DocumentsContract.EXTRA_LOADING
import androidx.documentfile.provider.DocumentFile
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.assertReadEquals
import com.stevesoltys.seedvault.coAssertThrows
@@ -39,6 +40,7 @@ import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@Suppress("BlockingMethodInNonBlockingContext")
+@MediumTest
class DocumentsStorageTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt
index 64ea9c40..f140eb22 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt
@@ -2,12 +2,14 @@ package com.stevesoltys.seedvault.transport.backup
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@RunWith(AndroidJUnit4::class)
+@MediumTest
class PackageServiceTest : KoinComponent {
private val packageService: PackageService by inject()
diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt
index 69d3b6dc..c0354815 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/App.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt
@@ -84,21 +84,21 @@ open class App : Application() {
protected open fun startKoin() = startKoin {
androidLogger(Level.ERROR)
androidContext(this@App)
- modules(
- listOf(
- cryptoModule,
- headerModule,
- metadataModule,
- documentsProviderModule, // storage plugin
- backupModule,
- restoreModule,
- installModule,
- storageModule,
- appModule
- )
- )
+ modules(appModules())
}
+ open fun appModules() = listOf(
+ cryptoModule,
+ headerModule,
+ metadataModule,
+ documentsProviderModule, // storage plugin
+ backupModule,
+ restoreModule,
+ installModule,
+ storageModule,
+ appModule
+ )
+
private val settingsManager: SettingsManager by inject()
private val metadataManager: MetadataManager by inject()
diff --git a/gradle.properties b/gradle.properties
index c6adc6da..8af1ea32 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,8 @@
-org.gradle.jvmargs=-Xmx1g
+org.gradle.jvmargs=-Xmx4g
org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.parallel=true
+org.gradle.daemon=true
android.useAndroidX=true
android.enableJetifier=false
kotlin.code.style=official
diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle
index cb1f6a1e..e1323412 100644
--- a/gradle/dependencies.gradle
+++ b/gradle/dependencies.gradle
@@ -23,7 +23,7 @@ ext.aosp_libs = fileTree(include: [
'android.jar',
// out/target/common/obj/JAVA_LIBRARIES/core-libart.com.android.art_intermediates/classes.jar
'libcore.jar',
-], dir: "$projectDir/app/libs")
+], dir: "${rootProject.projectDir}/app/libs")
ext.kotlin_libs = [
std: [
diff --git a/settings.gradle b/settings.gradle
index ae995566..07243914 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -18,3 +18,18 @@ include ':app'
include ':contactsbackup'
include ':storage:lib'
include ':storage:demo'
+
+ext.isCiServer = System.getenv().containsKey("CIRRUS_CI")
+ext.isMasterBranch = System.getenv().getOrDefault("CIRRUS_BRANCH", "").matches("android[0-9]+")
+ext.buildCacheHost = System.getenv().getOrDefault("CIRRUS_HTTP_CACHE_HOST", "localhost:12321")
+
+buildCache {
+ local {
+ enabled = !isCiServer
+ }
+ remote(HttpBuildCache) {
+ url = "http://${buildCacheHost}/"
+ enabled = isCiServer
+ push = isMasterBranch
+ }
+}