From 1a432132dfd7e1b83d37deeca218d66d7129d4df Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:38:06 +0100 Subject: [PATCH 1/6] refactor(common): replace Timber with SLF4J `:common` will be moved to a `java-library`. SLF4J is bridged to Timber via slf4j-timber in the app, so logs will continue to work. Due to this, fewer classes will need to move to `:common:android` Using 1.7.30 as this is the current transitive dependency Assisted-by: Claude Opus 4.6 --- common/build.gradle.kts | 2 ++ .../main/java/com/ichi2/anki/common/utils/TestUtils.kt | 8 +++++--- .../java/com/ichi2/testutils/FileSystemUtils.kt | 10 ++++++---- gradle/libs.versions.toml | 3 +++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 38b932d6a78f..8530c35f79f2 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.google.material) implementation(libs.jakewharton.timber) + implementation(libs.slf4j.api) testImplementation(libs.junit.jupiter) testImplementation(libs.junit.vintage.engine) testImplementation(libs.hamcrest) @@ -60,6 +61,7 @@ dependencies { testFixturesImplementation(libs.hamcrest) testFixturesImplementation(libs.jakewharton.timber) + testFixturesImplementation(libs.slf4j.api) testFixturesImplementation(libs.androidx.annotation) // Required so the ExperimentalCoroutinesApi opt-in (applied globally) doesn't cause // an "unresolved" warning, which is treated as an error due to allWarningsAsErrors diff --git a/common/src/main/java/com/ichi2/anki/common/utils/TestUtils.kt b/common/src/main/java/com/ichi2/anki/common/utils/TestUtils.kt index 7204983b2502..2028dd55c566 100644 --- a/common/src/main/java/com/ichi2/anki/common/utils/TestUtils.kt +++ b/common/src/main/java/com/ichi2/anki/common/utils/TestUtils.kt @@ -13,7 +13,9 @@ */ package com.ichi2.anki.common.utils -import timber.log.Timber +import org.slf4j.LoggerFactory + +private val logger = LoggerFactory.getLogger("TestUtils") /** make default HTML / JS debugging true for debug build and disable for unit/android tests * isRunningAsUnitTest checks if we are in debug or testing environment by checking if org.junit.Test class @@ -25,9 +27,9 @@ val isRunningAsUnitTest: Boolean try { Class.forName("org.junit.Test") } catch (ignored: ClassNotFoundException) { - Timber.d("isRunningAsUnitTest: %b", false) + logger.debug("isRunningAsUnitTest: {}", false) return false } - Timber.d("isRunningAsUnitTest: %b", true) + logger.debug("isRunningAsUnitTest: {}", true) return true } diff --git a/common/src/testFixtures/java/com/ichi2/testutils/FileSystemUtils.kt b/common/src/testFixtures/java/com/ichi2/testutils/FileSystemUtils.kt index 9357988b0a55..062a3e822929 100644 --- a/common/src/testFixtures/java/com/ichi2/testutils/FileSystemUtils.kt +++ b/common/src/testFixtures/java/com/ichi2/testutils/FileSystemUtils.kt @@ -19,16 +19,18 @@ package com.ichi2.testutils import androidx.annotation.CheckResult import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert -import timber.log.Timber +import org.slf4j.LoggerFactory import java.io.File import kotlin.io.path.createTempDirectory import kotlin.io.path.pathString /** Utilities which assist testing changes to files/directories */ +private val logger = LoggerFactory.getLogger(FileSystemUtils::class.java) + @Suppress("unused") object FileSystemUtils { /** - * Prints a directory structure using [Timber.d] + * Prints a directory structure * @param description The prefix to print before the tree is listed * @param file the root of the tree to print * @@ -47,7 +49,7 @@ object FileSystemUtils { description: String, file: File, ) { - Timber.d("$description: $file\n${printDirectoryTree(file)}") + logger.debug("$description: $file\n${printDirectoryTree(file)}") } /** from https://stackoverflow.com/a/13130974/ */ @@ -135,7 +137,7 @@ fun createTransientFile( fun File.createTransientDirectory(name: String): File { File(this, name).also { directory -> directory.deleteOnExit() - Timber.d("test: creating $directory") + logger.debug("test: creating $directory") MatcherAssert.assertThat("directory should have been created", directory.mkdirs(), CoreMatchers.equalTo(true)) return directory } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57ffe7675e0b..2d0ae76a00da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -115,6 +115,8 @@ searchpreference = "2.7.3" seismic = "1.0.3" sharedPreferencesMock = "1.2.4" slackKeeper = "0.16.1" +# https://github.com/qos-ch/slf4j +slf4j = "1.7.30" slf4jTimber = "3.1" timber = "5.0.1" # https://github.com/Triple-T/gradle-play-publisher/releases @@ -176,6 +178,7 @@ android-lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobufKotlinLite" } search-preference = { module = "com.github.ByteHamster:SearchPreference", version.ref = "searchpreference" } seismic = { module = "com.squareup:seismic", version.ref = "seismic" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-timber = { module = "com.arcao:slf4j-timber", version.ref = "slf4jTimber" } jakewharton-timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" } From 1cdd553a54ff5e17dacc377d083ad9e2ac1b70f4 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:48:31 +0100 Subject: [PATCH 2/6] refactor(build): improve JVM opt-in flag usage It's better to remove the opt-in flag, rather than add the library --- build.gradle.kts | 4 +++- common/build.gradle.kts | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1a4dcf6806d8..3e3953315cba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -116,8 +116,10 @@ subprojects { compilerArgs += "-XXLanguage:+ExplicitBackingFields" } - if (project.name != "api") { + if (project.path !in listOf(":api", ":common")) { compilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + if (project.path != ":api") { compilerArgs += "-Xcontext-parameters" } freeCompilerArgs = compilerArgs diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 8530c35f79f2..eabda064a66d 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -63,7 +63,4 @@ dependencies { testFixturesImplementation(libs.jakewharton.timber) testFixturesImplementation(libs.slf4j.api) testFixturesImplementation(libs.androidx.annotation) - // Required so the ExperimentalCoroutinesApi opt-in (applied globally) doesn't cause - // an "unresolved" warning, which is treated as an error due to allWarningsAsErrors - testFixturesImplementation(libs.kotlinx.coroutines.core) } From 368b78803af84c006ecebfbbef57f1629ed82fff Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:02:03 +0100 Subject: [PATCH 3/6] arch: remove Android dependencies from common We want to move Android dependencies to ':common:android', these files can remain in a `jvm-library` if we fixup a few annotations Related to https://redirect.github.com/ankidroid/Anki-Android-Backend/issues/674 --- common/src/main/java/com/ichi2/anki/common/time/MockTime.kt | 2 -- common/src/main/java/com/ichi2/anki/common/time/TimeManager.kt | 2 -- .../src/test/java/com/ichi2/anki/common/time/TimeUtilsTest.kt | 2 +- .../src/test/java/com/ichi2/anki/common/utils/ext/FloatTest.kt | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/common/src/main/java/com/ichi2/anki/common/time/MockTime.kt b/common/src/main/java/com/ichi2/anki/common/time/MockTime.kt index 952acd7fb4bf..58e7700f876a 100644 --- a/common/src/main/java/com/ichi2/anki/common/time/MockTime.kt +++ b/common/src/main/java/com/ichi2/anki/common/time/MockTime.kt @@ -16,7 +16,6 @@ package com.ichi2.anki.common.time -import android.annotation.SuppressLint import androidx.annotation.VisibleForTesting import java.util.Calendar import java.util.GregorianCalendar @@ -91,7 +90,6 @@ open class MockTime( * @param milliseconds, from 0 to 999 * @return the time stamp of this instant in GMT calendar */ - @SuppressLint("DirectGregorianInstantiation") fun timeStamp( year: Int, month: Int, diff --git a/common/src/main/java/com/ichi2/anki/common/time/TimeManager.kt b/common/src/main/java/com/ichi2/anki/common/time/TimeManager.kt index 0e89ebf9b0e8..edeebc9ea2c5 100644 --- a/common/src/main/java/com/ichi2/anki/common/time/TimeManager.kt +++ b/common/src/main/java/com/ichi2/anki/common/time/TimeManager.kt @@ -16,7 +16,6 @@ package com.ichi2.anki.common.time -import android.annotation.SuppressLint import androidx.annotation.VisibleForTesting import java.util.Stack @@ -26,7 +25,6 @@ import java.util.Stack * * For later: move this into a DI container */ -@SuppressLint("DirectSystemTimeInstantiation") object TimeManager { @VisibleForTesting fun reset() { diff --git a/common/src/test/java/com/ichi2/anki/common/time/TimeUtilsTest.kt b/common/src/test/java/com/ichi2/anki/common/time/TimeUtilsTest.kt index c672653553ec..95bf7d5ed7be 100644 --- a/common/src/test/java/com/ichi2/anki/common/time/TimeUtilsTest.kt +++ b/common/src/test/java/com/ichi2/anki/common/time/TimeUtilsTest.kt @@ -15,9 +15,9 @@ */ package com.ichi2.anki.common.time -import android.icu.util.Calendar import org.junit.Assert.assertEquals import org.junit.Test +import java.util.Calendar import java.util.TimeZone import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds diff --git a/common/src/test/java/com/ichi2/anki/common/utils/ext/FloatTest.kt b/common/src/test/java/com/ichi2/anki/common/utils/ext/FloatTest.kt index cac72da0d5b0..1916e3be5be7 100644 --- a/common/src/test/java/com/ichi2/anki/common/utils/ext/FloatTest.kt +++ b/common/src/test/java/com/ichi2/anki/common/utils/ext/FloatTest.kt @@ -16,7 +16,6 @@ package com.ichi2.anki.common.utils.ext -import androidx.core.math.MathUtils.clamp import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.junit.Test From 5a1147c99956b9bd63413c6d1f55fd0d15efaafd Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:02:03 +0100 Subject: [PATCH 4/6] arch: split ':common:android' from ':common' In preparation for moving libanki to be pure JVM, we need :common to be a java-library. JSON is supported by both android and JVM (with slightly differing apis so it can remain in `:common` Assisted-by: Claude Opus 4.6 Related to https://redirect.github.com/ankidroid/Anki-Android-Backend/issues/674 --- AnkiDroid/build.gradle | 1 + AnkiDroid/jacoco.gradle | 21 +++++-- build.gradle.kts | 2 +- common/README.md | 40 +++--------- common/android/README.md | 19 ++++++ common/android/build.gradle.kts | 63 +++++++++++++++++++ common/{ => android}/consumer-rules.pro | 0 common/{ => android}/proguard-rules.pro | 0 common/android/src/main/AndroidManifest.xml | 4 ++ .../anki/common/utils/android/TestUtils.kt | 0 .../com/ichi2/anki/common/utils/ext/Intent.kt | 0 .../ichi2/anki/utils/android/ColorUtils.kt | 0 common/build.gradle.kts | 59 ++++------------- settings.gradle.kts | 1 + 14 files changed, 129 insertions(+), 81 deletions(-) create mode 100644 common/android/README.md create mode 100644 common/android/build.gradle.kts rename common/{ => android}/consumer-rules.pro (100%) rename common/{ => android}/proguard-rules.pro (100%) create mode 100644 common/android/src/main/AndroidManifest.xml rename common/{ => android}/src/main/java/com/ichi2/anki/common/utils/android/TestUtils.kt (100%) rename common/{ => android}/src/main/java/com/ichi2/anki/common/utils/ext/Intent.kt (100%) rename common/{ => android}/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt (100%) diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index 5621119a3fa7..8b67eb380d60 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -385,6 +385,7 @@ dependencies { // modules implementation project(":common") + implementation project(":common:android") implementation project(":compat") implementation project(":libanki") implementation project(":vbpd") diff --git a/AnkiDroid/jacoco.gradle b/AnkiDroid/jacoco.gradle index 5a80680688c3..bf110b2d6b33 100644 --- a/AnkiDroid/jacoco.gradle +++ b/AnkiDroid/jacoco.gradle @@ -126,8 +126,10 @@ tasks.register('jacocoTestReport', JacocoReport) { dependsOn("connectedPlay${rootProject.androidTestVariantName}AndroidTest") } -// modules do not yet support flavors (play/full) -def modulesToUnitTest = [":common", ":libanki", ":compat"] +// Android library modules (do not yet support flavors play/full) +def androidModulesToUnitTest = [":libanki", ":compat"] +// JVM modules: use 'test' task and 'classes/kotlin/main' class dir +def jvmModulesToUnitTest = [":common"] // A unit-test only report task tasks.register('jacocoUnitTestReport', JacocoReport) {report -> @@ -145,10 +147,15 @@ tasks.register('jacocoUnitTestReport', JacocoReport) {report -> includeUnitTestCoverage(report, getProject(), flavorClassDir) dependsOn('testPlayDebugUnitTest') - for (module in modulesToUnitTest) { + for (module in androidModulesToUnitTest) { includeUnitTestCoverage(report, project(module), moduleClassDir) dependsOn("${module}:testDebugUnitTest") } + + for (module in jvmModulesToUnitTest) { + includeUnitTestCoverage(report, project(module), 'classes/kotlin/main') + dependsOn("${module}:test") + } } gradle.projectsEvaluated { @@ -159,12 +166,18 @@ gradle.projectsEvaluated { task.outputs.cacheIf { false } } } - for (module in modulesToUnitTest) { + for (module in androidModulesToUnitTest) { project(module).tasks.named('testDebugUnitTest')?.configure {task -> task.outputs.upToDateWhen { false } task.outputs.cacheIf { false } } } + for (module in jvmModulesToUnitTest) { + project(module).tasks.named('test')?.configure {task -> + task.outputs.upToDateWhen { false } + task.outputs.cacheIf { false } + } + } } diff --git a/build.gradle.kts b/build.gradle.kts index 3e3953315cba..49e0f1a71ea7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -116,7 +116,7 @@ subprojects { compilerArgs += "-XXLanguage:+ExplicitBackingFields" } - if (project.path !in listOf(":api", ":common")) { + if (project.path !in listOf(":api", ":common", ":common:android")) { compilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" } if (project.path != ":api") { diff --git a/common/README.md b/common/README.md index ac91f9aa3251..714323123263 100644 --- a/common/README.md +++ b/common/README.md @@ -1,11 +1,10 @@ ## AnkiDroid Common -AnkiDroid Common is a [Gradle module](https://developer.android.com/topic/modularization) +AnkiDroid Common is a pure JVM [Gradle module](https://developer.android.com/topic/modularization) containing utility functions, and definitions for core functionality used by other modules within AnkiDroid. Common should be the base of the AnkiDroid dependency tree. -Common is used by `libAnki` (which has no Android dependencies), so dependencies on the Android -framework should be in packages named `android`. +Common has no Android dependencies. Code requiring Android APIs belongs in `:common:android`. This module is expected to define interfaces which are initialized in the `AnkiDroid` module @@ -19,48 +18,29 @@ These are to be initialized higher up the dependency tree, typically in `AnkiDro ### `com.ichi2.anki.common.utils` -Utility classes and methods without an Android dependency - +Utility classes and methods ### `com.ichi2.anki.common.utils.ext` Extension methods, universally applicable to the classes they extend -Examples: - -* `Int.kt` - `ifNotZero` -* `InputStream.kt` - `convertToString` - -### `com.ichi2.anki.common.utils.android` - -Utilities with a dependency on Android - ## Context -This is a work in progress. As discussed in +As discussed in [#12582](https://github.com/ankidroid/Anki-Android/issues/12582), AnkiDroid decided to split the codebase into two modules, `libAnki` (business logic) and `AnkiDroid` (code interacting with -Android APIs). - -At the time of writing, this split is not yet done. We expect to do it with the following steps: +Android APIs). `common` existed for logic which both `AnkiDroid` and `libAnki` depended on. -* `com.ichi2.compat` was deemed to be an easy module to split out to trial this refactor - but this had circular dependencies -* A `common` module was proposed to fix this -* To reduce the execution time of tests, `libAnki` should have no dependencies on Android - * A lint rule will be applied to `libAnki` from using Android dependencies - * The alternate: splitting modules based on architecture was deemed to be unwieldy +Later, `compat` was split out, also depending on `common`, solidifying `common` -The following were blockers for `compat` to be split out +[Backend - #647](https://github.com/ankidroid/Anki-Android-Backend/issues/674): `libAnki` is +intended to be converted to a `java-library`, so `:common` was split into `:common:android`, +ensuring that -* `isRobolectric` -* `CrashReportService` -* `showThemedToast` -* `TimeManager` (maybe) -* `@KotlinCleanup` (maybe) Discussed on Discord: https://discord.gg/qjzcRTx * Discussion: https://discord.com/channels/368267295601983490/701922522836369498/1243991110888591482 * Thread: https://discord.com/channels/368267295601983490/1244372448233914438 * https://github.com/ankidroid/Anki-Android/pull/16498 +* [#20547 - extract `:compat`](https://github.com/ankidroid/Anki-Android/issues/20547) diff --git a/common/android/README.md b/common/android/README.md new file mode 100644 index 000000000000..4c85ae09b361 --- /dev/null +++ b/common/android/README.md @@ -0,0 +1,19 @@ +## AnkiDroid Common (Android) + +Android-specific utilities which are generally applicable to AnkiDroid. + +Split from `:common` to ensure that `:common` is a `java-library`, to support fast, pure-JVM tests. + +## Packages + +### `com.ichi2.anki.common.utils.android` + +Android-specific utilities (e.g. `isRobolectric`) + +### `com.ichi2.anki.common.utils.ext` + +Extension methods on Android framework classes (e.g. `Intent`) + +### `com.ichi2.anki.utils.android` + +Color manipulation utilities (`darkenColor`, `lightenColorAbsolute`) \ No newline at end of file diff --git a/common/android/build.gradle.kts b/common/android/build.gradle.kts new file mode 100644 index 000000000000..f734184dad04 --- /dev/null +++ b/common/android/build.gradle.kts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import com.android.build.api.dsl.LibraryExtension + +plugins { + alias(libs.plugins.android.library) +} + +configure { + namespace = "com.ichi2.anki.common.android" + compileSdk = + libs.versions.compileSdk + .get() + .toInt() + + defaultConfig { + minSdk = + libs.versions.minSdk + .get() + .toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +apply(from = "../../lint.gradle") +apply(from = "../../jacocoSupport.gradle") + +dependencies { + implementation(project(":common")) + + implementation(libs.androidx.annotation) + implementation(libs.jakewharton.timber) +} diff --git a/common/consumer-rules.pro b/common/android/consumer-rules.pro similarity index 100% rename from common/consumer-rules.pro rename to common/android/consumer-rules.pro diff --git a/common/proguard-rules.pro b/common/android/proguard-rules.pro similarity index 100% rename from common/proguard-rules.pro rename to common/android/proguard-rules.pro diff --git a/common/android/src/main/AndroidManifest.xml b/common/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..e10007615799 --- /dev/null +++ b/common/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/common/src/main/java/com/ichi2/anki/common/utils/android/TestUtils.kt b/common/android/src/main/java/com/ichi2/anki/common/utils/android/TestUtils.kt similarity index 100% rename from common/src/main/java/com/ichi2/anki/common/utils/android/TestUtils.kt rename to common/android/src/main/java/com/ichi2/anki/common/utils/android/TestUtils.kt diff --git a/common/src/main/java/com/ichi2/anki/common/utils/ext/Intent.kt b/common/android/src/main/java/com/ichi2/anki/common/utils/ext/Intent.kt similarity index 100% rename from common/src/main/java/com/ichi2/anki/common/utils/ext/Intent.kt rename to common/android/src/main/java/com/ichi2/anki/common/utils/ext/Intent.kt diff --git a/common/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt b/common/android/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt similarity index 100% rename from common/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt rename to common/android/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt diff --git a/common/build.gradle.kts b/common/build.gradle.kts index eabda064a66d..13eae8fc688a 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,66 +1,33 @@ -import com.android.build.api.dsl.LibraryExtension import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.jvm) + `java-test-fixtures` } -configure { - // this cannot conflict with com.ichi2.anki - // but we can define files in 'com.ichi2.anki' inside 'common' - // even with this namespace - namespace = "com.ichi2.anki.common" - testFixtures.enable = true - compileSdk = - libs.versions.compileSdk - .get() - .toInt() - - defaultConfig { - minSdk = - libs.versions.minSdk - .get() - .toInt() - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} - buildTypes { - release { - isMinifyEnabled = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 } } -apply(from = "../lint.gradle") -apply(from = "../jacocoSupport.gradle") - dependencies { - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.google.material) - implementation(libs.jakewharton.timber) + implementation(libs.json) + implementation(libs.androidx.annotation) implementation(libs.slf4j.api) + testImplementation(libs.junit.jupiter) testImplementation(libs.junit.vintage.engine) testImplementation(libs.hamcrest) testImplementation(libs.junit.platform.launcher) - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.espresso.core) testImplementation(kotlin("test")) testFixturesImplementation(libs.hamcrest) - testFixturesImplementation(libs.jakewharton.timber) - testFixturesImplementation(libs.slf4j.api) testFixturesImplementation(libs.androidx.annotation) + testFixturesImplementation(libs.slf4j.api) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1d72485394a9..f372454ea9a4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ include( ":api", ":AnkiDroid", ":common", + ":common:android", ":compat", ":libanki", ":lint-rules", From 549649f748d8ef1fef2105f85c95cd333c1f7712 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:57:23 +0100 Subject: [PATCH 5/6] refactor: rename files in 'common' `com.ichi2.anki.utils.android` should be `com.ichi2.anki.common.utils.android` --- .../com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt | 4 ++-- common/android/README.md | 6 +----- .../com/ichi2/anki/{ => common}/utils/android/ColorUtils.kt | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) rename common/android/src/main/java/com/ichi2/anki/{ => common}/utils/android/ColorUtils.kt (98%) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt index 39d232d6ce82..e6e9a26f7150 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt @@ -39,11 +39,11 @@ import com.ichi2.anki.AnkiDroidApp.Companion.sharedPrefs import com.ichi2.anki.Flag import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.utils.android.darkenColor +import com.ichi2.anki.common.utils.android.lightenColorAbsolute import com.ichi2.anki.common.utils.ext.replaceWith import com.ichi2.anki.databinding.ItemCardBrowserBinding import com.ichi2.anki.databinding.ViewBrowserColumnCellBinding -import com.ichi2.anki.utils.android.darkenColor -import com.ichi2.anki.utils.android.lightenColorAbsolute import com.ichi2.themes.Themes import com.ichi2.utils.removeChildren import net.ankiweb.rsdroid.BackendException diff --git a/common/android/README.md b/common/android/README.md index 4c85ae09b361..19fbc3c405fb 100644 --- a/common/android/README.md +++ b/common/android/README.md @@ -12,8 +12,4 @@ Android-specific utilities (e.g. `isRobolectric`) ### `com.ichi2.anki.common.utils.ext` -Extension methods on Android framework classes (e.g. `Intent`) - -### `com.ichi2.anki.utils.android` - -Color manipulation utilities (`darkenColor`, `lightenColorAbsolute`) \ No newline at end of file +Extension methods on Android framework classes (e.g. `Intent`) \ No newline at end of file diff --git a/common/android/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt b/common/android/src/main/java/com/ichi2/anki/common/utils/android/ColorUtils.kt similarity index 98% rename from common/android/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt rename to common/android/src/main/java/com/ichi2/anki/common/utils/android/ColorUtils.kt index 7df6871d7748..cb45f02ec422 100644 --- a/common/android/src/main/java/com/ichi2/anki/utils/android/ColorUtils.kt +++ b/common/android/src/main/java/com/ichi2/anki/common/utils/android/ColorUtils.kt @@ -14,7 +14,7 @@ * this program. If not, see . */ -package com.ichi2.anki.utils.android +package com.ichi2.anki.common.utils.android import android.graphics.Color import androidx.annotation.ColorInt From da99bfbc7cb683f903d75bc903b26cac9f2bb33b Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 13 Apr 2026 04:05:20 +0100 Subject: [PATCH 6/6] test: setup `:common:android` for testing Otherwise it's easy to forget to add it to androidModulesToUnitTest Assisted-by: Claude Opus 4.6 - tests --- AnkiDroid/jacoco.gradle | 2 +- common/android/build.gradle.kts | 7 +++ .../common/utils/android/ColorUtilsTest.kt | 45 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt diff --git a/AnkiDroid/jacoco.gradle b/AnkiDroid/jacoco.gradle index bf110b2d6b33..c624a8971721 100644 --- a/AnkiDroid/jacoco.gradle +++ b/AnkiDroid/jacoco.gradle @@ -127,7 +127,7 @@ tasks.register('jacocoTestReport', JacocoReport) { } // Android library modules (do not yet support flavors play/full) -def androidModulesToUnitTest = [":libanki", ":compat"] +def androidModulesToUnitTest = [":common:android", ":libanki", ":compat"] // JVM modules: use 'test' task and 'classes/kotlin/main' class dir def jvmModulesToUnitTest = [":common"] diff --git a/common/android/build.gradle.kts b/common/android/build.gradle.kts index f734184dad04..f3140a8d5a0d 100644 --- a/common/android/build.gradle.kts +++ b/common/android/build.gradle.kts @@ -60,4 +60,11 @@ dependencies { implementation(libs.androidx.annotation) implementation(libs.jakewharton.timber) + + testImplementation(libs.kotlin.test) + testImplementation(libs.junit.jupiter) + testImplementation(libs.junit.platform.launcher) + testImplementation(libs.junit.vintage.engine) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.test.junit) } diff --git a/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt b/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt new file mode 100644 index 000000000000..7e1d4441333e --- /dev/null +++ b/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.anki.common.utils.android + +import android.graphics.Color +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +/** Tests for [darkenColor] and [lightenColorAbsolute] */ +@RunWith(AndroidJUnit4::class) +class ColorUtilsTest { + @Test + fun darkenColor_withNoChange_returnsSameColor() { + val white = Color.WHITE + assertEquals(white, darkenColor(white, factor = 1.0f)) + } + + @Test + fun darkenColor_withFullDarken_returnsBlack() { + val white = Color.WHITE + assertEquals(Color.BLACK, darkenColor(white, factor = 0.0f)) + } + + @Test + fun lightenColorAbsolute_withNoChange_returnsSameColor() { + val red = Color.RED + assertEquals(red, lightenColorAbsolute(red, amount = 0.0f)) + } +}