Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ dependencies {

// modules
implementation project(":common")
implementation project(":common:android")
implementation project(":compat")
implementation project(":libanki")
implementation project(":vbpd")
Expand Down
21 changes: 17 additions & 4 deletions AnkiDroid/jacoco.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [":common:android", ":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 ->
Expand All @@ -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 {
Expand All @@ -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 }
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ subprojects {
compilerArgs += "-XXLanguage:+ExplicitBackingFields"
}

if (project.name != "api") {
if (project.path !in listOf(":api", ":common", ":common:android")) {
compilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
if (project.path != ":api") {
compilerArgs += "-Xcontext-parameters"
}
freeCompilerArgs = compilerArgs
Expand Down
40 changes: 10 additions & 30 deletions common/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
15 changes: 15 additions & 0 deletions common/android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 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`)
70 changes: 70 additions & 0 deletions common/android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/

import com.android.build.api.dsl.LibraryExtension

plugins {
alias(libs.plugins.android.library)
}

configure<LibraryExtension> {
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)

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)
}
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions common/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.utils.android
package com.ichi2.anki.common.utils.android

import android.graphics.Color
import androidx.annotation.ColorInt
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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))
}
}
60 changes: 13 additions & 47 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,67 +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<LibraryExtension> {
// 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.json)
implementation(libs.androidx.annotation)
implementation(libs.slf4j.api)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.google.material)
implementation(libs.jakewharton.timber)
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.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)
testFixturesImplementation(libs.slf4j.api)
}
2 changes: 0 additions & 2 deletions common/src/main/java/com/ichi2/anki/common/time/MockTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading