diff --git a/.github/adb_retry.sh b/.github/adb_retry.sh
new file mode 100644
index 000000000000..b0bb58341a28
--- /dev/null
+++ b/.github/adb_retry.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+set -euo pipefail
+
+# ADB can report the device offline just after boot (#22033/#22042).
+# Retry only connection failures reported before the command is dispatched.
+# Only use for short-lived commands: stderr is captured until they exit.
+stderr_file=$(mktemp)
+trap 'rm -f "$stderr_file"' EXIT
+
+for ((attempt = 1; attempt <= 5; attempt++)); do
+ # Keep retry messages out of stdout so getprop results remain usable.
+ if adb "$@" 2>"$stderr_file"; then
+ cat "$stderr_file" >&2
+ exit 0
+ else
+ adb_status=$?
+ fi
+ cat "$stderr_file" >&2
+
+ if ((attempt == 5)); then
+ exit "$adb_status"
+ fi
+ case "$(<"$stderr_file")" in
+ "adb: device offline"|"error: device offline") ;;
+ *) exit "$adb_status" ;;
+ esac
+ printf 'ADB device offline; retrying in 1 second (attempt %s/5).\n' "$((attempt + 1))" >&2
+ sleep 1
+done
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 496db9c043f1..1e0c21720c21 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -21,6 +21,10 @@ updates:
- "dependencies"
cooldown:
default-days: 3
+ # Missing Google Maven release dates cause Dependabot to close older AndroidX updates.
+ # https://github.com/dependabot/dependabot-core/issues/16055
+ exclude:
+ - "androidx.*"
ignore:
# Ignore all Rust backend updates, these are always done as manual pulls
- dependency-name: ankiBackend
diff --git a/.github/disable_art_metrics.sh b/.github/disable_art_metrics.sh
index caa12017231a..c9430f9cfd12 100644
--- a/.github/disable_art_metrics.sh
+++ b/.github/disable_art_metrics.sh
@@ -3,21 +3,9 @@
set -euo pipefail
-# Issue 22033: ADB reported "device offline" after sys.boot_completed=1.
-# The underlying cause is unknown; fixing it would likely remove the need for retries.
-# Retries may help if the disconnect is transient; this is unverified in CI.
-# Only use for commands safe to repeat: a failed ADB call may already have
-# executed the command on the device.
+# Only use for shell commands safe to repeat, such as reading or setting a value.
retry_repeatable_adb_shell_cmd() {
- local attempt output
- for ((attempt = 1; attempt <= 5; attempt++)); do
- if output=$(adb shell "$@"); then
- printf '%s\n' "$output"
- return 0
- fi
- sleep 1
- done
- return 1
+ bash "$(dirname "${BASH_SOURCE[0]}")/adb_retry.sh" shell "$@"
}
# Both CI images use BE2A.250530.026.F3. Revisit this workaround when that changes.
diff --git a/.github/report_emulator_test_failures.py b/.github/report_emulator_test_failures.py
new file mode 100644
index 000000000000..c686509c268a
--- /dev/null
+++ b/.github/report_emulator_test_failures.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""Report failures and skipped tests from Android's JUnit XML using the standard library."""
+
+import html
+import os
+from pathlib import Path
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+
+def read_ignored_tests(log_file):
+ ignored = set()
+ failed = set()
+ if log_file is None or not log_file.is_file():
+ return ignored
+ with log_file.open(encoding="utf-8", errors="replace") as log:
+ for line in log:
+ match = re.search(r"\bTestRunner: (ignored|failed): (.+)\(([^()]+)\)\s*$", line)
+ if match:
+ status, method, classname = match.groups()
+ (ignored if status == "ignored" else failed).add(f"{classname}.{method}")
+ return ignored - failed
+
+
+def main(results_dir, log_file=None):
+ failures = []
+ skipped = []
+ ignored = read_ignored_tests(log_file)
+ for report in sorted(results_dir.rglob("TEST-*.xml")):
+ try:
+ root = ET.parse(report).getroot()
+ except (ET.ParseError, OSError) as error:
+ failures.append(f"Could not read {report}: {error}")
+ continue
+
+ for test in root.iter("testcase"):
+ for result in test:
+ if result.tag not in ("failure", "error", "skipped"):
+ continue
+ name = f"{test.get('classname', '')}.{test.get('name', '')}"
+ details = "".join(result.itertext()).strip()
+ message = result.get("message", "")
+ # Android can report assumption violations as failures or errors.
+ # Only match the top-level exception, not mentions in a real failure.
+ exception_type = result.get("type") or (details or message).partition("\n")[0].partition(":")[0]
+ # The Android test engine also writes @Ignore as an empty failure.
+ # Require the runner's ignore event before treating an empty failure as skipped.
+ is_ignored = name in ignored and not (details or message or result.get("type"))
+ if message and message not in details:
+ details = f"{message}\n{details}".strip()
+ if result.tag == "skipped" or is_ignored or exception_type in (
+ "org.junit.AssumptionViolatedException",
+ "org.junit.internal.AssumptionViolatedException",
+ ):
+ skipped.append(f"{name}\n{details or result.get('type', 'Test skipped')}")
+ else:
+ failures.append(f"{name}\n{details or result.get('type', 'Test failed')}")
+
+ for failure in failures:
+ if os.environ.get("GITHUB_ACTIONS") == "true":
+ # GitHub renders the escaped message as a multiline error in the log.
+ message = failure.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
+ print(f"::error::{message}")
+ else:
+ print(failure, end="\n\n")
+
+ if skipped:
+ title = f"Skipped tests ({len(skipped)})"
+ if os.environ.get("GITHUB_ACTIONS") == "true":
+ print(f"::group::{title}")
+ else:
+ print(title, end="\n\n")
+ for skip in skipped:
+ print(skip, end="\n\n")
+ if os.environ.get("GITHUB_ACTIONS") == "true":
+ print("::endgroup::")
+
+ if (failures or skipped) and os.environ.get("GITHUB_STEP_SUMMARY"):
+ with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
+ if failures:
+ summary.write("### Emulator test failures\n\n")
+ for failure in failures:
+ summary.write(f"
{html.escape(failure)}\n\n")
+ if skipped:
+ summary.write(f"{title}
\n\n")
+ for skip in skipped:
+ summary.write(f"{html.escape(skip)}\n\n")
+ summary.write(" \n\n")
+
+ return 1 if failures else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(Path(sys.argv[1]), Path(sys.argv[2]) if len(sys.argv) > 2 else None))
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index f84b47fe80f7..1f6c1854e51c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,6 +7,8 @@ on:
# Ignore merge queue branches on push; avoids merge_group+push concurrency race since ref is same
branches-ignore:
- 'gh-readonly-queue/**'
+ # Dependabot branches are already checked by the pull_request trigger.
+ - 'dependabot/**'
merge_group:
permissions:
diff --git a/.github/workflows/tests_emulator.yml b/.github/workflows/tests_emulator.yml
index d7e47c7dbb02..7efb3ac6c300 100644
--- a/.github/workflows/tests_emulator.yml
+++ b/.github/workflows/tests_emulator.yml
@@ -13,6 +13,8 @@ on:
# Ignore merge queue branches on push; avoids merge_group+push concurrency race since ref is same
branches-ignore:
- 'gh-readonly-queue/**'
+ # Dependabot branches are already checked by the pull_request trigger.
+ - 'dependabot/**'
merge_group:
permissions:
@@ -176,7 +178,7 @@ jobs:
script: |
touch adb-log.txt
$ANDROID_HOME/platform-tools/adb logcat '*:D' >> adb-log.txt &
- adb logcat --clear
+ bash .github/adb_retry.sh logcat --clear
echo "Generated AVD snapshot for caching."
# This step is separate so pure install time may be calculated as a step
@@ -200,10 +202,10 @@ jobs:
script: |
touch adb-log.txt
$ANDROID_HOME/platform-tools/adb logcat '*:D' >> adb-log.txt &
- $ANDROID_HOME/platform-tools/adb shell su root "setprop ctl.restart zygote"
+ bash .github/adb_retry.sh shell su root "setprop ctl.restart zygote"
sleep 10
sleep $FIRST_BOOT_DELAY
- adb logcat --clear
+ bash .github/adb_retry.sh logcat --clear
echo "First boot warmup completed."
- name: Run Emulator Tests
@@ -223,11 +225,15 @@ jobs:
$ANDROID_HOME/platform-tools/adb logcat '*:D' >> adb-log.txt &
bash .github/disable_art_metrics.sh
# Fix "Speech Recognition and Synthesis from Google keeps stopping" causing RootViewWithoutFocusException
- adb shell settings put global hide_error_dialogs 1
- adb emu screenrecord start --time-limit 1800 video.webm
+ bash .github/adb_retry.sh shell settings put global hide_error_dialogs 1
+ bash .github/adb_retry.sh emu screenrecord start --time-limit 1800 video.webm
sleep 5
./gradlew uninstallAll jacocoAndroidTestReport --daemon
+ - name: Report Emulator Test Results
+ if: always() && hashFiles('AnkiDroid/build/outputs/androidTest-results/connected/**/TEST-*.xml') != ''
+ run: python3 .github/report_emulator_test_failures.py AnkiDroid/build/outputs/androidTest-results/connected adb-log.txt
+
- name: Configure NDK tools PATH for alignment checking
run: |
echo "Using pre-installed NDK tools for alignment checking"
@@ -300,4 +306,4 @@ jobs:
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
verbose: true
- fail_ci_if_error: ${{ github.repository == 'ankidroid/Anki-Android' }} # optional (default = false)
\ No newline at end of file
+ fail_ci_if_error: ${{ github.repository == 'ankidroid/Anki-Android' }} # optional (default = false)
diff --git a/.github/workflows/tests_unit.yml b/.github/workflows/tests_unit.yml
index 12076f5cd7a3..989a1bc4d091 100644
--- a/.github/workflows/tests_unit.yml
+++ b/.github/workflows/tests_unit.yml
@@ -26,6 +26,8 @@ on:
# Ignore merge queue branches on push; avoids merge_group+push concurrency race since ref is same
branches-ignore:
- 'gh-readonly-queue/**'
+ # Dependabot branches are already checked by the pull_request trigger.
+ - 'dependabot/**'
merge_group:
permissions:
diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle
index 62ec003b5091..8ac6af4f8f68 100644
--- a/AnkiDroid/build.gradle
+++ b/AnkiDroid/build.gradle
@@ -767,6 +767,7 @@ dependencies {
testImplementation testFixtures(project(":common:android"))
testImplementation testFixtures(project(":compat"))
testImplementation testFixtures(project(":anki-common"))
+ androidTestImplementation testFixtures(project(":anki-common"))
// A path for a testing library which provide Parameterized Test
testImplementation libs.junit.jupiter
@@ -804,6 +805,8 @@ dependencies {
// for testing flows
testImplementation libs.cashapp.turbine
+ androidTestImplementation testFixtures(project(":anki-common"))
+
// May need a resolution strategy for support libs to our versions
androidTestImplementation libs.androidx.espresso.core
androidTestImplementation(libs.androidx.espresso.contrib) {
diff --git a/AnkiDroid/robolectricDownloader.gradle b/AnkiDroid/robolectricDownloader.gradle
index bfbd1c8e73d4..83756ce8ed29 100644
--- a/AnkiDroid/robolectricDownloader.gradle
+++ b/AnkiDroid/robolectricDownloader.gradle
@@ -21,7 +21,7 @@
def robolectricAndroidSdkVersions = [
// [androidVersion: "6.0.1_r3", frameworkSdkBuildVersion: "r1"],
// [androidVersion: "7.0.0_r1", frameworkSdkBuildVersion: "r1"],
-// [androidVersion: "7.1.0_r7", frameworkSdkBuildVersion: "r1"],
+ [androidVersion: "7.1.0_r7", frameworkSdkBuildVersion: "r1"],
// [androidVersion: "8.0.0_r4", frameworkSdkBuildVersion: "r1"],
[androidVersion: "8.1.0", frameworkSdkBuildVersion: "4611349"],
[androidVersion: "9", frameworkSdkBuildVersion: "4913185-2"],
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckOptionsTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckOptionsTest.kt
index 7afce61074cf..44ba727c7a9b 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckOptionsTest.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckOptionsTest.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt
index b68d670d32e9..8934159a6657 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/DeckPickerTest.kt
@@ -20,11 +20,9 @@ import android.annotation.SuppressLint
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
-import com.ichi2.anki.TestUtils.activityInstance
import com.ichi2.anki.TestUtils.isTablet
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
@@ -33,7 +31,7 @@ import com.ichi2.anki.testutil.discardPreliminaryViews
import com.ichi2.anki.testutil.grantPermissions
import com.ichi2.anki.testutil.notificationPermission
import com.ichi2.anki.testutil.tapOnCountLayouts
-import org.hamcrest.Matchers.instanceOf
+import com.ichi2.anki.testutil.useResumedActivity
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@@ -65,11 +63,10 @@ class DeckPickerTest : InstrumentedTest() {
// Go to RecyclerView item having "Test Deck" and click on the counts layout
tapOnCountLayouts("Default")
- // Check if currently open Activity is StudyOptionsActivity
- assertThat(
- activityInstance,
- instanceOf(StudyOptionsActivity::class.java),
- )
+ // ActivityScenarioRule only owns DeckPicker. Close Study Options before collection cleanup.
+ useResumedActivity {
+ onView(withId(R.id.studyoptions_frame)).check(matches(isDisplayed()))
+ }
}
@Test
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentScrollTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentScrollTest.kt
index 5a1cb93361d0..38c832c4de07 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentScrollTest.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentScrollTest.kt
@@ -2,6 +2,7 @@
package com.ichi2.anki
+import android.webkit.WebView
import androidx.core.content.edit
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
@@ -290,7 +291,18 @@ class ReviewerFragmentScrollTest : InstrumentedTest() {
private val SafeWebViewLayout.pageScale: Double
get() = evaluateScript("window.visualViewport.scale").toDouble()
+ @Suppress("DEPRECATION") // Native zoom limits indicate whether WebView has applied the viewport.
+ private val SafeWebViewLayout.canZoomIn: Boolean
+ get() {
+ var canZoom = false
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ canZoom = (getChildAt(0) as WebView).canZoomIn()
+ }
+ return canZoom
+ }
+
private fun SafeWebViewLayout.zoomIn() {
+ waitUntil(message = { "WebView did not become ready to zoom" }) { canZoomIn }
val originalScale = pageScale
InstrumentationRegistry.getInstrumentation().runOnMainSync {
zoomBy(2f)
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/pages/DeckOptionsTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/pages/DeckOptionsTest.kt
index 27f6818edd55..26e2eb0bbb76 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/pages/DeckOptionsTest.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/pages/DeckOptionsTest.kt
@@ -6,18 +6,22 @@ import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
+import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.common.preferences.sharedPrefs
import com.ichi2.anki.libanki.Consts
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.testutil.waitUntil
+import com.ichi2.testutils.ext.defaultDeckNewCardsPerDay
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
+import org.json.JSONObject
import org.junit.After
import org.junit.Before
import org.junit.Test
+import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@@ -63,6 +67,50 @@ class DeckOptionsTest : InstrumentedTest() {
}
}
+ @Test
+ fun optimizingAllPresetsSavesAndReloadsOptions() {
+ withDeckOptions {
+ saveAndOptimize(newPerDay = 43)
+
+ waitUntil(timeout = 30.seconds, message = { "optimization did not save the changed limit" }) {
+ col.defaultDeckNewCardsPerDay == 43
+ }
+ val optionsReloaded = AtomicBoolean(false)
+ waitUntil(timeout = 30.seconds, message = { "options did not reload after optimization" }) {
+ // Reloading can discard an evaluation callback. Retry without waiting for each one.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ webViewLayout.evaluateJavascript(
+ "globalThis.beforeOptimization === undefined && Array.from(document.querySelectorAll('input[type=number]')).find(input => input.offsetParent !== null)?.value === '43'",
+ ) {
+ if (it == "true") optionsReloaded.set(true)
+ }
+ }
+ optionsReloaded.get()
+ }
+ assertFalse(requireActivity().isFinishing)
+ }
+ }
+
+ private fun DeckOptions.saveAndOptimize(newPerDay: Int) {
+ val script =
+ """
+ globalThis.beforeOptimization = true;
+ window.confirm = () => true;
+ const input = Array.from(document.querySelectorAll('input[type="number"]')).find(input => input.offsetParent !== null);
+ input.focus();
+ input.value = '$newPerDay';
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ input.dispatchEvent(new Event('change', { bubbles: true }));
+ Array.from(document.querySelectorAll('button'))
+ .find(button => button.textContent.trim() === ${JSONObject.quote(TR.deckConfigSaveAndOptimize())}).click();
+ """.trimIndent()
+ // The action navigates away from its JavaScript context, which can discard the
+ // evaluation callback. Observe the persisted settings and the new page instead.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ webViewLayout.evaluateJavascript(script)
+ }
+ }
+
private fun withDeckOptions(block: DeckOptions.() -> Unit) {
val intent = DeckOptions.getIntent(testContext, Consts.DEFAULT_DECK_ID)
ActivityScenario.launch(intent).use { scenario ->
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewreminders/ReviewRemindersTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewreminders/ReviewRemindersTest.kt
index 691d64f1fc74..837f0ac8079e 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewreminders/ReviewRemindersTest.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/reviewreminders/ReviewRemindersTest.kt
@@ -14,6 +14,8 @@ import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
@@ -25,6 +27,7 @@ import com.google.android.material.chip.Chip
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.R
+import com.ichi2.anki.TestUtils.clickChildViewWithId
import com.ichi2.anki.common.time.TimeManager
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.tests.InstrumentedTest
@@ -366,7 +369,13 @@ class ReviewRemindersTest : InstrumentedTest() {
private fun selectDeck(deckName: String) {
onView(withId(R.id.add_edit_reminder_deck_name)).perform(click())
onView(withText(deckName)).checkWithTimeout(matches(isDisplayed()))
- onView(withText(deckName)).perform(click())
+ // .click() can open "Create subdeck", so invoke the handler directly
+ onView(withId(R.id.decks)).perform(
+ actionOnItem(
+ hasDescendant(withText(deckName)),
+ clickChildViewWithId(R.id.DeckPickerHoriz),
+ ),
+ )
onView(withId(R.id.add_edit_reminder_deck_name)).checkWithTimeout(matches(withText(deckName)))
}
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt
index 5b414f92a46b..ec4627c4b966 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt
@@ -12,7 +12,6 @@ import android.database.Cursor
import android.database.CursorWindow
import android.net.Uri
import anki.cards.FsrsMemoryState
-import anki.collection.OpChanges
import anki.notetypes.StockNotetype
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.Flag
@@ -37,6 +36,7 @@ import com.ichi2.anki.libanki.backend.BackendUtils
import com.ichi2.anki.libanki.exception.ConfirmModSchemaException
import com.ichi2.anki.libanki.getStockNotetype
import com.ichi2.anki.libanki.sched.Scheduler
+import com.ichi2.anki.observability.ChangeCounter
import com.ichi2.anki.observability.ChangeManager
import com.ichi2.anki.provider.pureAnswer
import com.ichi2.anki.testutil.DatabaseUtils.cursorFillWindow
@@ -2475,7 +2475,7 @@ class ContentProviderTest : InstrumentedTest() {
@Test
fun testInsertNotifiesUI() {
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.subscribe(counter)
try {
val mid = noteTypeId
@@ -2504,7 +2504,7 @@ class ContentProviderTest : InstrumentedTest() {
ContentValues().apply {
put(FlashCardsContract.Note.TAGS, "new_tag")
}
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.subscribe(counter)
try {
contentResolver.update(uri, values, null, null)
@@ -2516,7 +2516,7 @@ class ContentProviderTest : InstrumentedTest() {
@Test
fun testUpdateNonExistentNoteDoesNotNotifyUI() {
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.awaitPendingOpChanges()
ChangeManager.subscribe(counter)
try {
@@ -2531,7 +2531,7 @@ class ContentProviderTest : InstrumentedTest() {
}
ChangeManager.awaitPendingOpChanges()
- assertEquals("UI should not be notified if update is failed", 0, counter.count)
+ assertEquals("UI should not be notified if update is failed", 0, counter.changeCount)
} finally {
ChangeManager.unsubscribe(counter)
}
@@ -2541,7 +2541,7 @@ class ContentProviderTest : InstrumentedTest() {
fun testDeleteNotifiesUI() {
val noteId = createdNotes.first().lastPathSegment!!.toLong()
val uri = Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, noteId.toString())
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.subscribe(counter)
try {
contentResolver.delete(uri, null, null)
@@ -2553,7 +2553,7 @@ class ContentProviderTest : InstrumentedTest() {
@Test
fun testDeleteNonExistentNoteDoesNotNotifyUI() {
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.awaitPendingOpChanges()
ChangeManager.subscribe(counter)
try {
@@ -2562,7 +2562,7 @@ class ContentProviderTest : InstrumentedTest() {
assertEquals("It should return 0 for non-existent note", 0, deletedCount)
ChangeManager.awaitPendingOpChanges()
- assertEquals("UI should not be notify if nothing was deleted", 0, counter.count)
+ assertEquals("UI should not be notify if nothing was deleted", 0, counter.changeCount)
} finally {
ChangeManager.unsubscribe(counter)
}
@@ -2570,7 +2570,7 @@ class ContentProviderTest : InstrumentedTest() {
@Test
fun testBulkInsertNotifiesUI() {
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.subscribe(counter)
try {
val mid = noteTypeId
@@ -2595,14 +2595,14 @@ class ContentProviderTest : InstrumentedTest() {
@Test
fun testBulkInsertEmptyListDoesNotNotifyUI() {
- val counter = TestSubscriber()
+ val counter = ChangeCounter()
ChangeManager.awaitPendingOpChanges()
ChangeManager.subscribe(counter)
try {
contentResolver.bulkInsert(FlashCardsContract.Note.CONTENT_URI, emptyArray())
ChangeManager.awaitPendingOpChanges()
- assertEquals("UI should not be notified for empty bulk insert", 0, counter.count)
+ assertEquals("UI should not be notified for empty bulk insert", 0, counter.changeCount)
} finally {
ChangeManager.unsubscribe(counter)
}
@@ -2692,26 +2692,14 @@ class ContentProviderTest : InstrumentedTest() {
}
}
- // TODO: PERF: use TestChangeSubscriber once we've moved to testFixtures
- private class TestSubscriber : ChangeManager.Subscriber {
- var count = 0
-
- override fun opExecuted(
- changes: OpChanges,
- handler: Any?,
- ) {
- count++
- }
- }
-
- private fun assertNotificationReceived(subscriber: TestSubscriber) {
+ private fun assertNotificationReceived(subscriber: ChangeCounter) {
val timeout = 5000L
val startTime = TimeManager.time.intTimeMS()
- while (subscriber.count == 0 && TimeManager.time.intTimeMS() - startTime < timeout) {
+ while (!subscriber.hasChanges && TimeManager.time.intTimeMS() - startTime < timeout) {
Thread.sleep(100)
}
- assertTrue("UI should be notified of the change", subscriber.count > 0)
+ assertTrue("UI should be notified of the change", subscriber.hasChanges)
}
private val contentResolver: ContentResolver
diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt
index d97a39d08564..0fb755e84d1f 100644
--- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt
+++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/DatabaseUtils.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2021 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
diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml
index ba73f828ee54..97ed6a7ac06e 100644
--- a/AnkiDroid/src/main/AndroidManifest.xml
+++ b/AnkiDroid/src/main/AndroidManifest.xml
@@ -78,6 +78,9 @@
+
+
+
-
diff --git a/AnkiDroid/src/main/res/menu/study_options_fragment.xml b/AnkiDroid/src/main/res/menu/study_options_fragment.xml
index 66154daea5e4..907c836edb99 100644
--- a/AnkiDroid/src/main/res/menu/study_options_fragment.xml
+++ b/AnkiDroid/src/main/res/menu/study_options_fragment.xml
@@ -6,7 +6,7 @@
xmlns:tools="http://schemas.android.com/tools">
diff --git a/AnkiDroid/src/main/res/values/02-strings.xml b/AnkiDroid/src/main/res/values/02-strings.xml
index e67c70bc9d96..90f9c52bbcfc 100644
--- a/AnkiDroid/src/main/res/values/02-strings.xml
+++ b/AnkiDroid/src/main/res/values/02-strings.xml
@@ -17,7 +17,7 @@
Card deck:
- Deck:
+ Deck:
Type:
Tags: %1$s
Cards: %1$s
diff --git a/AnkiDroid/src/main/res/values/10-preferences.xml b/AnkiDroid/src/main/res/values/10-preferences.xml
index 6b0b79bae373..4a052bc521d8 100644
--- a/AnkiDroid/src/main/res/values/10-preferences.xml
+++ b/AnkiDroid/src/main/res/values/10-preferences.xml
@@ -10,7 +10,8 @@
- Disabled
+ Enabled
+ Disabled
- %d min
- %d mins
@@ -62,7 +63,7 @@
Display audio filenames in the card browser question/answer fields
AnkiDroid directory
Fullscreen mode
- Off
+ Off
Hide the system bars
Hide the system bars and answer buttons
Answer buttons position
@@ -216,11 +217,85 @@
Media
Misc
-
+
+
+
+ Review reminders
+ App-wide
+ Deck-specific
+ An error occurred while loading your review reminders, corrupted reminders have been deleted. Details:
+
+
+ Schedule reminders
Add reminder
- Add review reminder
- Edit review reminder
You have no review reminders
+ Troubleshoot
+ Notification settings
+ List of scheduled review reminders
+ Deck not found
+ Reminders are unavailable
+ Fix
+ Successfully added new review reminder
+ Successfully deleted review reminder
+ Successfully edited review reminder
+
+
+ Add review reminder
+ Edit review reminder
+ Time:
+ Deck:
+ Advanced settings
+ Card threshold:
+ Do not send me notifications if there are less than this many cards due
+ Only notify me if no reviews have been done today
+ Please enter a whole number of cards
+ The threshold must be at least 0
+ Delete this reminder?
+ This action cannot be undone.
+
+
+ Reminders are unavailable.
+ Reminders may not work correctly.
+ Your reminders should work as expected.
+ Different devices have different notification restriction settings. Check your system settings or search online if issues persist.
+
+ Notification permission
+ Notification channel
+ Do not disturb
+ Battery optimization
+ Power saving mode
+ Alarms & reminders permission
+
+ Granted
+ Denied
+ On
+ Off
+ Unrestricted
+ Optimized
+ Restricted
+
+ The review reminder notification channel must be enabled
+ Do Not Disturb may mute reminder notifications
+ Battery optimization may delay reminders
+ Background usage is disabled. Reminders may not be delivered
+ Power saving mode may prevent timely delivery of reminders
+ Required to schedule reminders at exact times
+
+ Grant permission
+ Enable notification channel
+ Disable battery optimization
+ Open battery settings
+
+
+ It\'s time to study your cards
+ It\'s time to study \"%s\"
+ Snooze %s
+
+ - %d card due
+ - %d cards due
+
Select tags
diff --git a/AnkiDroid/src/main/res/values/12-dont-translate.xml b/AnkiDroid/src/main/res/values/12-dont-translate.xml
index 44edfbfc7dbc..d9752ab15525 100644
--- a/AnkiDroid/src/main/res/values/12-dont-translate.xml
+++ b/AnkiDroid/src/main/res/values/12-dont-translate.xml
@@ -28,10 +28,6 @@ If you want to use a string in your code that can't be translated, please use:
This is a string with a non breakable-space using unicode.
This is a string with a non breakable-space using entity.
-
-
- Schedule reminders
- Review Reminders
More
Browse
diff --git a/AnkiDroid/src/main/res/values/constants.xml b/AnkiDroid/src/main/res/values/constants.xml
index 8df7dc8f6ecc..9408f7e78338 100644
--- a/AnkiDroid/src/main/res/values/constants.xml
+++ b/AnkiDroid/src/main/res/values/constants.xml
@@ -300,8 +300,8 @@
- - App-wide
- - Deck-specific
+ - @string/review_reminders_summary_app_wide
+ - @string/review_reminders_summary_deck_specific
diff --git a/AnkiDroid/src/main/res/values/theme_light.xml b/AnkiDroid/src/main/res/values/theme_light.xml
index b42e088ed263..bf1736b81336 100644
--- a/AnkiDroid/src/main/res/values/theme_light.xml
+++ b/AnkiDroid/src/main/res/values/theme_light.xml
@@ -7,6 +7,7 @@
- @color/material_light_blue_700
- @color/white
- @color/black
+ - #716C76
- @color/material_blue_grey_700
- @color/white
- @color/white
diff --git a/AnkiDroid/src/main/res/xml/preference_headers.xml b/AnkiDroid/src/main/res/xml/preference_headers.xml
index bffa1c5bfcce..c87fa8769e49 100644
--- a/AnkiDroid/src/main/res/xml/preference_headers.xml
+++ b/AnkiDroid/src/main/res/xml/preference_headers.xml
@@ -76,7 +76,7 @@
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AbstractFlashcardViewerKeyboardInputTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AbstractFlashcardViewerKeyboardInputTest.kt
index 85fe9346bdca..b67e51136aaf 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/AbstractFlashcardViewerKeyboardInputTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/AbstractFlashcardViewerKeyboardInputTest.kt
@@ -1,18 +1,5 @@
-/*
- Copyright (c) 2020 David Allison
+// SPDX-License-Identifier: GPL-3.0-or-later
- 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
import android.view.KeyEvent
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ActivityStartupUnderBackupTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ActivityStartupUnderBackupTest.kt
index a2c665c9e92c..ce66c018cdda 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/ActivityStartupUnderBackupTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/ActivityStartupUnderBackupTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki
import android.app.Activity
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppFailureTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppFailureTest.kt
index 256c27c0c16d..545518617840 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppFailureTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppFailureTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt
index cb534da1836a..4f3a5a99c162 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki
import androidx.lifecycle.LifecycleEventObserver
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/BackupManagerSimpleTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/BackupManagerSimpleTest.kt
index 2e0e0e797fc0..b6c97e84ce46 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/BackupManagerSimpleTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/BackupManagerSimpleTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2022 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt
index 2c77fffdf5f2..74654b969e72 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt
@@ -108,6 +108,7 @@ import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.sameInstance
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.greaterThan
@@ -505,6 +506,29 @@ class CardBrowserTest : RobolectricTest() {
}
}
+ @Test
+ fun cardCountIsShownAfterRecreation() {
+ ensureCollectionLoadIsSynchronous()
+ addBasicNote("dog", "barks")
+ addBasicNote("cat", "meows")
+
+ fun CardBrowser.subtitle() = findViewById(R.id.subtitle).text.toString()
+
+ ActivityScenario.launch(CardBrowser::class.java).use { scenario ->
+ advanceRobolectricLooper()
+ scenario.onActivity { browser ->
+ assertThat("card count before recreation", browser.subtitle(), equalTo("2 cards shown"))
+ }
+
+ scenario.recreate()
+ advanceRobolectricLooper()
+
+ scenario.onActivity { browser ->
+ assertThat("card count after recreation", browser.subtitle(), equalTo("2 cards shown"))
+ }
+ }
+ }
+
@Test
fun tagWithBracketsDisplaysProperly() =
runTest {
@@ -1786,6 +1810,43 @@ class CardBrowserTest : RobolectricTest() {
assertMenusEqual(expectedMenuItems, menu)
}
+ @Test // Issue 19737 - recreating the editor is expensive enough to ANR when repeated
+ fun `pane editor is not recreated when a search returns the same row`() =
+ withBrowser(noteCount = 2, fragmented = true) {
+ val editorBeforeRefresh = assertNotNull(fragment, "note editor fragment")
+
+ // every `opExecuted` calls refreshBrowserUI() -> a new search
+ // the pane row is unchanged, so the pane editor does not need to be recreated
+ searchCards()
+ advanceRobolectricLooper()
+
+ assertThat("the pane editor is reused", fragment, sameInstance(editorBeforeRefresh))
+ }
+
+ @Test // Issue 19737
+ fun `pane editor refreshes when the note changes outside the editor`() =
+ withBrowser(noteCount = 1, fragmented = true) {
+ val editor = assertNotNull(fragment, "note editor fragment")
+ assertThat(editor.getFieldForTest(0).fieldText, equalTo("0"))
+
+ // simulate 'find & replace' in the browser: the note changes,
+ // then opExecuted calls refreshBrowserUI() -> a new search
+ val cardId = assertNotNull(viewModel.paneRow?.toCardId(viewModel.cardsOrNotes))
+ withCol {
+ val note = getCard(cardId).note(this)
+ note.setField(0, "replaced")
+ updateNote(note)
+ }
+ searchCards()
+ advanceRobolectricLooper()
+
+ assertThat(
+ "the pane editor shows the updated note",
+ fragment?.getFieldForTest(0)?.fieldText,
+ equalTo("replaced"),
+ )
+ }
+
@Test
fun `deck chip performs a search`() {
// The deck chip uses `DeckSelectionListener`, which uses a different code path
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DayRolloverHandlerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DayRolloverHandlerTest.kt
new file mode 100644
index 000000000000..74bcff80e0dc
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/DayRolloverHandlerTest.kt
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.ichi2.anki.observability.ChangeManager
+import com.ichi2.widget.DayRolloverAlarm
+import com.ichi2.widget.WidgetStatus
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.runs
+import io.mockk.unmockkAll
+import io.mockk.verify
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runCurrent
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DayRolloverHandlerTest : RobolectricTest() {
+ @After
+ fun clearMocks() {
+ unmockkAll()
+ }
+
+ /** Issue 22041: overlapping initial time events must not flip the reviewer back to the question. */
+ @Test
+ fun `overlapping initial time changes do not report a day rollover`() =
+ runTest {
+ val scheduler = col.sched
+ mockkObject(scheduler, DayRolloverAlarm, WidgetStatus)
+ every { scheduler.dayCutoff } returns 86400L
+ val subscriber = mockk(relaxed = true)
+ ChangeManager.subscribe(subscriber)
+ every { WidgetStatus.updateInBackground(any()) } just runs
+ every { DayRolloverAlarm.scheduleNext(any()) } answers {
+ // Let the second event check the same cutoff before the first event finishes.
+ runCurrent()
+ }
+
+ repeat(2) { launch { DayRolloverHandler.handleTimeChange() } }
+ advanceUntilIdle()
+
+ verify(exactly = 0) { subscriber.opExecuted(any(), any()) }
+ verify(exactly = 1) { DayRolloverAlarm.scheduleNext(any()) }
+ }
+}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerImportTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerImportTest.kt
index b53d461f11b5..5c8c9039a222 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerImportTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerImportTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki
import android.content.Intent
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/EmptyApplicationCategory.kt b/AnkiDroid/src/test/java/com/ichi2/anki/EmptyApplicationCategory.kt
index 4d86b0dac3a8..6ec31e8089bf 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/EmptyApplicationCategory.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/EmptyApplicationCategory.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/FlagTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/FlagTest.kt
index d628bd66bafa..2a2067d75dc7 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/FlagTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/FlagTest.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/FormatDeckDescriptionTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/FormatDeckDescriptionTest.kt
index 6b083ff131dc..c3672e996cc4 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/FormatDeckDescriptionTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/FormatDeckDescriptionTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityWithConflictTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityWithConflictTest.kt
index d09d7736f31d..84a03fd83722 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityWithConflictTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityWithConflictTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki
import android.content.Context
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt
index e98a26542a53..2a3c83e8ce94 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteEditorTest.kt
@@ -7,15 +7,19 @@ package com.ichi2.anki
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
+import android.content.ComponentName
import android.content.Intent
+import android.content.IntentFilter
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
+import android.provider.MediaStore
import android.util.SparseArray
import android.view.View
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
+import androidx.core.view.isVisible
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
@@ -314,6 +318,7 @@ class NoteEditorTest : RobolectricTest() {
@Test
fun testHandleMultimediaActionsDisplaysBottomSheet() {
+ installCameraApp()
val intent = NoteEditorDestination.AddNote().toIntent()
ActivityScenario.launchActivityForResult(intent).use { scenario ->
scenario.onNoteEditor { noteEditor ->
@@ -329,6 +334,18 @@ class NoteEditorTest : RobolectricTest() {
}
}
+ @Test
+ fun `multimedia camera option is hidden without a camera app`() {
+ val intent = NoteEditorDestination.AddNote().toIntent()
+ ActivityScenario.launchActivityForResult(intent).use { scenario ->
+ scenario.onNoteEditor { noteEditor ->
+ noteEditor.showMultimediaBottomSheet()
+
+ onView(withId(R.id.multimedia_action_camera)).inRoot(isDialog()).check(matches(not(isDisplayed())))
+ }
+ }
+ }
+
@Test
fun copyNoteCopiesDeckId() {
val currentDid = addDeck("Basic::Test")
@@ -549,6 +566,19 @@ class NoteEditorTest : RobolectricTest() {
}
}
+ @Test
+ fun `image occlusion camera button is hidden without a camera app`() {
+ val editor = getNoteEditorAdding(NoteType.IMAGE_OCCLUSION).build()
+ assertFalse(editor.occlusionCameraButton.isVisible, "camera button visible")
+ }
+
+ @Test
+ fun `image occlusion camera button is shown with a camera app`() {
+ installCameraApp()
+ val editor = getNoteEditorAdding(NoteType.IMAGE_OCCLUSION).build()
+ assertTrue(editor.occlusionCameraButton.isVisible, "camera button visible")
+ }
+
@Test
fun `shows 'Default' as selected when attempting to add to a filtered deck`() {
val testDeckId = addDeck("TestDeckA")
@@ -900,6 +930,14 @@ class NoteEditorTest : RobolectricTest() {
}
}
+ private fun installCameraApp() {
+ val camera = ComponentName("com.android.camera2", "com.android.camera.CaptureActivity")
+ shadowOf(targetContext.packageManager).apply {
+ addActivityIfNotPresent(camera)
+ addIntentFilterForActivity(camera, IntentFilter(MediaStore.ACTION_IMAGE_CAPTURE).apply { addCategory(Intent.CATEGORY_DEFAULT) })
+ }
+ }
+
private fun getCopyNoteIntent(editor: NoteEditorFragment): Bundle {
val editorShadow = shadowOf(editor.requireActivity())
editor.copyNote()
@@ -1071,6 +1109,9 @@ private class NoteEditorFieldAccessor(
private val NoteEditorFragment.fields: NoteEditorFieldAccessor
get() = NoteEditorFieldAccessor(this)
+private val NoteEditorFragment.occlusionCameraButton: View
+ get() = requireView().findViewById(R.id.CameraForOcclusionButton)
+
private fun NoteEditorFragment.isSticky(index: Int) = this.toggleStickyText.containsKey(index)
private var NoteEditorFragment.noteType: NotetypeJson
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ReadTextTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ReadTextTest.kt
index 4ca7509dc865..288dd9fca37b 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/ReadTextTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/ReadTextTest.kt
@@ -1,18 +1,5 @@
-/*
- Copyright (c) 2020 David Allison
+// SPDX-License-Identifier: GPL-3.0-or-later
- 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
import android.content.Context
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/StudyOptionsViewModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/StudyOptionsViewModelTest.kt
index 1c697ee7facb..85f4377a6b8b 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/StudyOptionsViewModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/StudyOptionsViewModelTest.kt
@@ -6,7 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import anki.scheduler.CardAnswer.Rating
import app.cash.turbine.test
import com.ichi2.anki.CollectionManager.withCol
-import com.ichi2.testutils.ensureOpsExecuted
+import com.ichi2.anki.observability.ensureOpsExecuted
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import org.hamcrest.MatcherAssert.assertThat
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/TranslationTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/TranslationTest.kt
index acb605d8e13f..ce4787b0e91d 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/TranslationTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/TranslationTest.kt
@@ -368,6 +368,10 @@ class TranslationTest : RobolectricTest() {
private val IGNORED_BACKEND_TRANSLATIONS =
setOf(
"launcherOff", // "Off" - unrelated to R.string.full_screen_off
+ "launcherOn", // "On" - unrelated to R.string.reminder_troubleshooting_status_on
+ // "Advanced Settings" - this upstream string is Title Case; R.string.add_edit_reminder_advanced_label
+ // is both unrelated and sentence case (per AnkiDroid convention)
+ "deckConfigAdvancedSettings",
)
}
}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/WhiteboardDefaultForegroundColorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/WhiteboardDefaultForegroundColorTest.kt
index c3b41550bedc..b4d36035b222 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/WhiteboardDefaultForegroundColorTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/WhiteboardDefaultForegroundColorTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki
import android.content.Intent
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/BrowserColumnSelectionFragmentScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/BrowserColumnSelectionFragmentScreenshotTest.kt
new file mode 100644
index 000000000000..3d408df59655
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/BrowserColumnSelectionFragmentScreenshotTest.kt
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.browser
+
+import androidx.core.view.WindowInsetsCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.ichi2.anki.R
+import com.ichi2.anki.ScreenshotTest
+import com.ichi2.anki.model.CardsOrNotes
+import com.ichi2.anki.withCardBrowser
+import com.ichi2.testutils.scrollToEnd
+import com.ichi2.testutils.simulateSystemBars
+import com.ichi2.testutils.windowInsetsOf
+import com.ichi2.utils.dp
+import org.junit.Test
+import org.robolectric.RuntimeEnvironment
+
+/**
+ * Edge-to-edge regressions for the full-screen Manage columns dialog (Issue 22092).
+ *
+ * `./gradlew :AnkiDroid:verifyRoborazziPlayDebug -Pscreenshot --tests "com.ichi2.anki.browser.BrowserColumnSelectionFragmentScreenshotTest"`
+ */
+class BrowserColumnSelectionFragmentScreenshotTest : ScreenshotTest() {
+ @Test
+ fun portrait() =
+ captureColumns(
+ "portrait",
+ with(targetContext) { windowInsetsOf(navBarBottom = 48.dp) },
+ )
+
+ @Test
+ fun gestureNavigation() =
+ captureColumns(
+ "gesture_navigation",
+ with(targetContext) { windowInsetsOf(navBarBottom = 24.dp) },
+ )
+
+ @Test
+ fun portraitCutout() =
+ captureColumns(
+ "portrait_cutout",
+ with(targetContext) { windowInsetsOf(navBarBottom = 48.dp, cutoutTop = 40.dp) },
+ )
+
+ @Test
+ fun landscape() {
+ RuntimeEnvironment.setQualifiers("+land")
+ captureColumns(
+ "landscape",
+ with(targetContext) { windowInsetsOf(navBarRight = 48.dp, cutoutLeft = 32.dp) },
+ )
+ }
+
+ /** Captures the toolbar on opening, then the final column's clearance when fully scrolled. */
+ private fun captureColumns(
+ name: String,
+ insets: WindowInsetsCompat,
+ ) = withCardBrowser(noteCount = 1) { browser ->
+ // The backend uses the real clock; stabilize the created and modified column previews.
+ val timestamp = 1_594_105_200L
+ col.db.execute("update notes set id = ?, mod = ?", timestamp * 1000, timestamp)
+ col.db.execute("update cards set nid = ?, mod = ?", timestamp * 1000, timestamp)
+
+ val fragment = BrowserColumnSelectionFragment.createInstance(CardsOrNotes.CARDS)
+ fragment.showNow(browser.supportFragmentManager, "columns")
+ advanceRobolectricLooper()
+ try {
+ fragment.requireDialog().window!!.simulateSystemBars(insets)
+ captureScreen(name)
+
+ fragment.requireView().findViewById(R.id.recycler_view).scrollToEnd()
+ captureScreen("${name}_scrolled_to_bottom")
+ } finally {
+ fragment.dismissNow()
+ }
+ }
+}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt
index 1dab10f8727c..23fca6425617 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt
@@ -67,16 +67,17 @@ import com.ichi2.anki.model.SelectableDeck
import com.ichi2.anki.model.SortType
import com.ichi2.anki.model.cardBrowserNoSorting
import com.ichi2.anki.noteeditor.toIntent
+import com.ichi2.anki.observability.ensureNoOpsExecuted
+import com.ichi2.anki.observability.ensureOpWithHandler
+import com.ichi2.anki.observability.ensureOpsExecuted
import com.ichi2.anki.servicelayer.NoteService
import com.ichi2.anki.setFlagFilterSync
import com.ichi2.anki.settings.Prefs
+import com.ichi2.anki.utils.ext.defaultBrowserSearch
import com.ichi2.anki.utils.ext.ignoreAccentsInSearch
import com.ichi2.testutils.IntentAssert
import com.ichi2.testutils.JvmTest
import com.ichi2.testutils.createTransientDirectory
-import com.ichi2.testutils.ensureNoOpsExecuted
-import com.ichi2.testutils.ensureOpWithHandler
-import com.ichi2.testutils.ensureOpsExecuted
import com.ichi2.testutils.ext.reopenWithLanguage
import com.ichi2.testutils.mockIt
import kotlinx.coroutines.CancellationException
@@ -1900,6 +1901,47 @@ class CardBrowserViewModelTest : JvmTest() {
}
}
+ @Test
+ fun `default search text is applied on init`() {
+ col.config.defaultBrowserSearch = "deck:current"
+ try {
+ Prefs.devUsingCardBrowserSearchView = true
+ runViewModelTest {
+ assertThat(searchTerms, equalTo("deck:current"))
+ assertThat(defaultBrowserSearch, equalTo("deck:current"))
+ }
+ } finally {
+ Prefs.devUsingCardBrowserSearchView = false
+ }
+ }
+
+ @Test
+ fun `intent search wins over default search text`() {
+ col.config.defaultBrowserSearch = "deck:current"
+ try {
+ Prefs.devUsingCardBrowserSearchView = true
+ runViewModelTest(options = DeepLink("tag:foo")) {
+ assertThat(searchTerms, equalTo("tag:foo"))
+ }
+ } finally {
+ Prefs.devUsingCardBrowserSearchView = false
+ }
+ }
+
+ @Test
+ fun `setDefaultSearchText round-trips through collection config`() {
+ try {
+ Prefs.devUsingCardBrowserSearchView = true
+ runViewModelTest {
+ setDefaultSearchText("tag:foo").join()
+ assertThat(defaultBrowserSearch, equalTo("tag:foo"))
+ assertThat(col.config.defaultBrowserSearch, equalTo("tag:foo"))
+ }
+ } finally {
+ Prefs.devUsingCardBrowserSearchView = false
+ }
+ }
+
@Test
fun `updating sort type launches search`() =
runViewModelTest {
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardBrowserSearchViewModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardBrowserSearchViewModelTest.kt
index 7eaf15dd65a6..7c7af1b028b9 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardBrowserSearchViewModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardBrowserSearchViewModelTest.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.browser.search
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardStateTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardStateTest.kt
index 161584a8e09a..e38f0d6940d7 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardStateTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/CardStateTest.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.browser.search
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SearchRequestTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SearchRequestTest.kt
index f0e0ba3fd729..98defeca7ec6 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SearchRequestTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SearchRequestTest.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.browser.search
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SortOrderBottomSheetFragmentTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SortOrderBottomSheetFragmentTest.kt
index 9741e14cd7ea..d397e31b9c0b 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SortOrderBottomSheetFragmentTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/SortOrderBottomSheetFragmentTest.kt
@@ -2,7 +2,7 @@
package com.ichi2.anki.browser.search
-import androidx.core.os.bundleOf
+import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.RobolectricTest
@@ -48,9 +48,9 @@ class SortOrderBottomSheetFragmentTest : RobolectricTest() {
val viewModelFactory = SingleViewModelFactory.create(createCardBrowserViewModel())
val fragmentArgs =
- bundleOf(
- ARG_CURRENT_SORT_TYPE to currentSortType,
- )
+ Bundle().apply {
+ putParcelable(ARG_CURRENT_SORT_TYPE, currentSortType)
+ }
launchFragment(fragmentArgs) {
SortOrderBottomSheetFragment(viewModelFactory)
}.use { scenario ->
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/StandardSearchFragmentTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/StandardSearchFragmentTest.kt
index 54460771aeeb..f3ac0de929b0 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/StandardSearchFragmentTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/search/StandardSearchFragmentTest.kt
@@ -1,18 +1,4 @@
-/*
- * 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.browser.search
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/deckpicker/DeckPickerViewModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/deckpicker/DeckPickerViewModelTest.kt
index f218e15f292e..b61a14b6f3a8 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/deckpicker/DeckPickerViewModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/deckpicker/DeckPickerViewModelTest.kt
@@ -14,7 +14,7 @@ import com.ichi2.anki.libanki.Consts
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.Note
import com.ichi2.anki.libanki.emptyCids
-import com.ichi2.testutils.ensureOpsExecuted
+import com.ichi2.anki.observability.ensureOpsExecuted
import org.hamcrest.CoreMatchers.not
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/BrowserOptionsDialogScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/BrowserOptionsDialogScreenshotTest.kt
new file mode 100644
index 000000000000..c4288c791a2c
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/BrowserOptionsDialogScreenshotTest.kt
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.dialogs
+
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.ichi2.anki.ScreenshotTest
+import com.ichi2.anki.getBrowserWithNotes
+import com.ichi2.anki.settings.Prefs
+import com.ichi2.anki.utils.ext.defaultBrowserSearch
+import org.junit.Test
+
+class BrowserOptionsDialogScreenshotTest : ScreenshotTest() {
+ @Test
+ fun `default search text`(
+ @TestParameter hasDefaultSearch: Boolean,
+ ) = runTest {
+ col.config.defaultBrowserSearch = if (hasDefaultSearch) "deck:current" else ""
+ val previousSearchView = Prefs.devUsingCardBrowserSearchView
+ try {
+ Prefs.devUsingCardBrowserSearchView = true
+ val browser = getBrowserWithNotes(noteCount = 1)
+ browser.viewModel.searchJob?.join()
+ advanceRobolectricLooper()
+
+ browser.cardBrowserFragment.showOptionsDialog()
+ advanceRobolectricLooper()
+
+ captureScreen(if (hasDefaultSearch) "default_search_populated" else "default_search_empty")
+ } finally {
+ Prefs.devUsingCardBrowserSearchView = previousSearchView
+ }
+ }
+}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/multimedia/MultimediaImageFragmentTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/multimedia/MultimediaImageFragmentTest.kt
index e35b7c56e49c..fb9705dffe20 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/multimedia/MultimediaImageFragmentTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/multimedia/MultimediaImageFragmentTest.kt
@@ -6,9 +6,9 @@ package com.ichi2.anki.multimedia
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
+import android.os.Bundle
import android.webkit.WebView
import androidx.core.net.toUri
-import androidx.core.os.bundleOf
import androidx.fragment.app.commitNow
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.RobolectricTest
@@ -91,6 +91,6 @@ class MultimediaImageFragmentTest : RobolectricTest() {
private fun withImageFragment(block: MultimediaImageFragment.() -> Unit) =
launchFragmentInContainer(
- bundleOf(EXTRA_MEDIA_OPTIONS to MultimediaImageFragment.ImageOptions.GALLERY),
+ Bundle().apply { putSerializable(EXTRA_MEDIA_OPTIONS, MultimediaImageFragment.ImageOptions.GALLERY) },
).use { it.withFragment(block) }
}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/pages/AnkiServerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/pages/AnkiServerTest.kt
new file mode 100644
index 000000000000..44ee31d39b1b
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/pages/AnkiServerTest.kt
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.pages
+
+import fi.iki.elonen.NanoHTTPD.IHTTPSession
+import fi.iki.elonen.NanoHTTPD.Method
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verifyNoInteractions
+import java.io.ByteArrayInputStream
+import java.io.EOFException
+import java.io.InputStream
+import java.net.SocketTimeoutException
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
+
+class AnkiServerTest {
+ @Test
+ fun `a request body can arrive in several reads`() {
+ val bytes = ByteArray(100) { it.toByte() }
+ val stream =
+ object : ByteArrayInputStream(bytes) {
+ override fun read(
+ buffer: ByteArray,
+ offset: Int,
+ length: Int,
+ ): Int = super.read(buffer, offset, minOf(length, 3))
+ }
+ val session = postSession(stream, contentLength = bytes.size)
+
+ assertContentEquals(bytes, AnkiServer.getSessionBytes(session))
+ }
+
+ @Test
+ fun `a truncated request propagates to NanoHTTPD without dispatching`() {
+ val session = postSession(ByteArrayInputStream(byteArrayOf(1, 2)), contentLength = 10)
+ val handler = mock()
+ val server = AnkiServer(handler)
+
+ assertFailsWith { server.serve(session) }
+ verifyNoInteractions(handler)
+ }
+
+ @Test
+ fun `a body read timeout propagates to NanoHTTPD without dispatching`() {
+ val timeout = SocketTimeoutException("Read timed out")
+ val stream =
+ object : InputStream() {
+ override fun read(): Int = throw timeout
+ }
+ val session = postSession(stream, contentLength = 1)
+ val handler = mock()
+ val server = AnkiServer(handler)
+
+ assertSame(timeout, assertFailsWith { server.serve(session) })
+ verifyNoInteractions(handler)
+ }
+
+ @Test
+ fun `reading a body leaves bytes beyond content length unread`() {
+ val stream = ByteArrayInputStream(byteArrayOf(1, 2, 3, 4))
+ val session = postSession(stream, contentLength = 3)
+
+ assertContentEquals(byteArrayOf(1, 2, 3), AnkiServer.getSessionBytes(session))
+ assertEquals(4, stream.read())
+ }
+
+ @Test
+ fun `an empty protobuf request is accepted`() {
+ val session = postSession(ByteArrayInputStream(byteArrayOf()), contentLength = 0)
+
+ assertContentEquals(byteArrayOf(), AnkiServer.getSessionBytes(session))
+ }
+
+ private fun postSession(
+ stream: InputStream,
+ contentLength: Int,
+ ): IHTTPSession =
+ mock {
+ on { method } doReturn Method.POST
+ on { uri } doReturn "/_anki/test"
+ on { headers } doReturn mapOf("content-length" to contentLength.toString())
+ on { inputStream } doReturn stream
+ }
+}
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/pages/DeckOptionsTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/pages/DeckOptionsTest.kt
index 666120880bc4..4b30722f218f 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/pages/DeckOptionsTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/pages/DeckOptionsTest.kt
@@ -2,21 +2,31 @@
package com.ichi2.anki.pages
+import android.webkit.WebResourceRequest
import android.webkit.WebView
+import androidx.core.net.toUri
import androidx.core.view.children
import androidx.test.ext.junit.runners.AndroidJUnit4
+import anki.deck_config.UpdateDeckConfigsMode
+import anki.deck_config.UpdateDeckConfigsRequest
import com.ichi2.anki.R
import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.libanki.Consts
import com.ichi2.anki.settings.Prefs
import com.ichi2.testutils.ext.clear
+import com.ichi2.testutils.ext.defaultDeckNewCardsPerDay
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsString
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
import org.robolectric.Shadows.shadowOf
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
/** Test for [DeckOptions] */
@RunWith(AndroidJUnit4::class)
@@ -39,7 +49,95 @@ class DeckOptionsTest : RobolectricTest() {
}
}
- private fun withDeckOptions(block: DeckOptions.() -> Unit) {
+ @Test
+ fun `javascript can reload the current deck options page`() {
+ withDeckOptions {
+ val webView = webViewLayout.children.filterIsInstance().single()
+ val pageUrl =
+ webView
+ .url!!
+ .toUri()
+ .buildUpon()
+ .fragment(null)
+ .build()
+ val request = mock { on { url } doReturn pageUrl }
+
+ for (fragment in listOf(null, "night")) {
+ webView.loadUrl(
+ pageUrl
+ .buildUpon()
+ .fragment(fragment)
+ .build()
+ .toString(),
+ )
+ assertFalse(shadowOf(webView).webViewClient.shouldOverrideUrlLoading(webView, request), "fragment: $fragment")
+ }
+ }
+ }
+
+ @Test
+ fun `links to other local pages still open externally`() {
+ withDeckOptions {
+ val webView = webViewLayout.children.filterIsInstance().single()
+ val otherPage =
+ webView
+ .url!!
+ .toUri()
+ .buildUpon()
+ .path("/graphs")
+ .build()
+ val request = mock { on { url } doReturn otherPage }
+
+ assertTrue(shadowOf(webView).webViewClient.shouldOverrideUrlLoading(webView, request))
+ }
+ }
+
+ @Test
+ fun `optimizing all presets saves without closing options`() =
+ runTest {
+ withDeckOptions {
+ updateDeckConfigs(UpdateDeckConfigsMode.UPDATE_DECK_CONFIGS_MODE_COMPUTE_ALL_PARAMS, newPerDay = 42)
+
+ assertFalse(requireActivity().isFinishing)
+ assertEquals(42, col.defaultDeckNewCardsPerDay)
+ }
+ }
+
+ @Test
+ fun `normal save closes options`() =
+ runTest {
+ withDeckOptions {
+ updateDeckConfigs(UpdateDeckConfigsMode.UPDATE_DECK_CONFIGS_MODE_NORMAL, newPerDay = 42)
+
+ assertTrue(requireActivity().isFinishing)
+ assertEquals(42, col.defaultDeckNewCardsPerDay)
+ }
+ }
+
+ private suspend fun DeckOptions.updateDeckConfigs(
+ mode: UpdateDeckConfigsMode,
+ newPerDay: Int,
+ ) {
+ val data = col.backend.getDeckConfigsForUpdate(Consts.DEFAULT_DECK_ID)
+ val config =
+ data.allConfigList
+ .single()
+ .config
+ .toBuilder()
+ config.setConfig(config.config.toBuilder().setNewPerDay(newPerDay))
+ val request =
+ UpdateDeckConfigsRequest
+ .newBuilder()
+ .setTargetDeckId(Consts.DEFAULT_DECK_ID)
+ .setMode(mode)
+ .setFsrs(true)
+ .addConfigs(config)
+ .build()
+ .toByteArray()
+ requireActivity().updateDeckConfigsRaw(request)
+ }
+
+ private inline fun withDeckOptions(block: DeckOptions.() -> Unit) {
val activity =
startActivityNormallyOpenCollectionWithIntent(
SingleFragmentActivity::class.java,
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReminderTroubleshootingResolveActionTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReminderTroubleshootingResolveActionTest.kt
index cbeef781e797..aebd4eb4f114 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReminderTroubleshootingResolveActionTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReminderTroubleshootingResolveActionTest.kt
@@ -11,6 +11,7 @@ import androidx.core.net.toUri
import androidx.core.view.children
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.ichi2.anki.R
import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.databinding.ItemTroubleshootingCheckBinding
import com.ichi2.testutils.withRequestIgnoreBatteryOptimizationsInManifest
@@ -88,7 +89,9 @@ class ReminderTroubleshootingResolveActionTest : RobolectricTest() {
/** The displayed 'Battery optimization' check */
private val ReminderTroubleshootingFragment.batteryOptimizationCheck: ItemTroubleshootingCheckBinding
- get() =
- binding.checksList.children
+ get() {
+ val title = requireContext().getString(R.string.reminder_troubleshooting_check_battery_optimization)
+ return binding.checksList.children
.map { ItemTroubleshootingCheckBinding.bind(it) }
- .single { it.title.text.toString() == "Battery optimization" }
+ .single { it.title.text.toString() == title }
+ }
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt
index 4f5535d792dc..abadc97cc3cb 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt
@@ -3,6 +3,7 @@
package com.ichi2.anki.services
+import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
@@ -130,6 +131,59 @@ class NotificationServiceTest : RobolectricTest() {
reviewReminder.verifyLatestNotifTime(previousTime = creationTime, shouldHaveUpdated = true)
}
+ @Test
+ fun `notification description is singular when one card is due`() =
+ runTest {
+ val did = addDeck("Deck", setAsSelected = true).withNotes(count = 1)
+ val reviewReminder = createTestReminder(deckId = did)
+ ReviewRemindersDatabase.insertReminder(reviewReminder)
+
+ val notif = slot()
+
+ TimeManager.resetWith(today)
+ attemptNotif(reviewReminder)
+
+ verifyNotifSent(reviewReminder, slot = notif)
+ assertThat(notif.captured.extras.getString(Notification.EXTRA_TEXT), equalTo("1 card due"))
+ }
+
+ @Test
+ fun `notification description is plural when several cards are due`() =
+ runTest {
+ val did = addDeck("Deck", setAsSelected = true).withNotes(count = 2)
+ val reviewReminder = createTestReminder(deckId = did)
+ ReviewRemindersDatabase.insertReminder(reviewReminder)
+
+ val notif = slot()
+
+ TimeManager.resetWith(today)
+ attemptNotif(reviewReminder)
+
+ verifyNotifSent(reviewReminder, slot = notif)
+ assertThat(notif.captured.extras.getString(Notification.EXTRA_TEXT), equalTo("2 cards due"))
+ }
+
+ @Test
+ @SuppressLint("BidiSpoofing")
+ fun `notification title isolates bidi control characters in deck name`() =
+ runTest {
+ // an unclosed RTL embedding would otherwise move the closing quote: 'It's time to study "French", you silly person'
+ val did = addDeck("French, you silly person", setAsSelected = true).withNotes(count = 1)
+ val reviewReminder = createTestReminder(deckId = did)
+ ReviewRemindersDatabase.insertReminder(reviewReminder)
+
+ val notif = slot()
+
+ TimeManager.resetWith(today)
+ attemptNotif(reviewReminder)
+
+ verifyNotifSent(reviewReminder, slot = notif)
+ assertThat(
+ notif.captured.extras.getString(Notification.EXTRA_TITLE),
+ equalTo("It's time to study \"French, you silly person\""),
+ )
+ }
+
@Test
fun `triggering with non-existent deck should not fire notification but schedule next`() =
runTest {
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/tags/ManageTagsViewModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/tags/ManageTagsViewModelTest.kt
index 018ab1f713cd..b3f1b947ddb5 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/tags/ManageTagsViewModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/tags/ManageTagsViewModelTest.kt
@@ -6,7 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.dialogs.utils.AnKingTags
-import com.ichi2.testutils.ensureOpsExecuted
+import com.ichi2.anki.observability.ensureOpsExecuted
import kotlinx.coroutines.flow.first
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/workarounds/SafeWebViewLayoutTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/workarounds/SafeWebViewLayoutTest.kt
index 311326f40203..56b4ab5355a6 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/workarounds/SafeWebViewLayoutTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/workarounds/SafeWebViewLayoutTest.kt
@@ -2,10 +2,10 @@
package com.ichi2.anki.workarounds
+import android.os.Bundle
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.FrameLayout
-import androidx.core.os.bundleOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.EXTRA_MEDIA_OPTIONS
@@ -229,7 +229,7 @@ class SafeWebViewLayoutTest : RobolectricTest() {
private fun withMultimediaWebView(block: (SafeWebViewLayout, MultimediaImageFragment) -> Unit) {
launchFragmentInContainer(
- bundleOf(EXTRA_MEDIA_OPTIONS to MultimediaImageFragment.ImageOptions.GALLERY),
+ Bundle().apply { putSerializable(EXTRA_MEDIA_OPTIONS, MultimediaImageFragment.ImageOptions.GALLERY) },
).use { scenario ->
scenario.onFragment { fragment ->
block(fragment.binding.multimediaWebView, fragment)
diff --git a/AnkiDroid/src/test/java/com/ichi2/audio/AudioRecordingControllerTest.kt b/AnkiDroid/src/test/java/com/ichi2/audio/AudioRecordingControllerTest.kt
index a78386c92b36..32c8958dec14 100644
--- a/AnkiDroid/src/test/java/com/ichi2/audio/AudioRecordingControllerTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/audio/AudioRecordingControllerTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.audio
diff --git a/AnkiDroid/src/test/java/com/ichi2/preferences/PreferenceExtensionsTest.kt b/AnkiDroid/src/test/java/com/ichi2/preferences/PreferenceExtensionsTest.kt
index e640b19177e2..6c7dd520c567 100644
--- a/AnkiDroid/src/test/java/com/ichi2/preferences/PreferenceExtensionsTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/preferences/PreferenceExtensionsTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.preferences
import android.content.SharedPreferences
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiDroidAppWithFatalError.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiDroidAppWithFatalError.kt
index 9f7566841af8..b727f810f2de 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiDroidAppWithFatalError.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiDroidAppWithFatalError.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiFragmentScenario.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiFragmentScenario.kt
index 66aa9c8065cb..f9ae0bcc2f0f 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiFragmentScenario.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/AnkiFragmentScenario.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2024 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
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflict.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflict.kt
index ca736c67eac3..5645a6352ff2 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflict.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflict.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.testutils
import anki.backend.BackendError
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflictTest.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflictTest.kt
index 68c975750804..808090f8116a 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflictTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/BackendEmulatingOpenConflictTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.testutils
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/EmptyAnkiActivity.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/EmptyAnkiActivity.kt
index 500078ee167b..c97701698b59 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/EmptyAnkiActivity.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/EmptyAnkiActivity.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/Hamcrest.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/Hamcrest.kt
index e012af47faab..e2d1330b05fe 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/Hamcrest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/Hamcrest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2023 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/InsetsUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/InsetsUtils.kt
index cec6abc35a09..1381014b273f 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/InsetsUtils.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/InsetsUtils.kt
@@ -8,6 +8,7 @@ import android.content.Context
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
+import android.view.Window
import android.widget.FrameLayout
import androidx.core.graphics.Insets
import androidx.core.view.RoundedCornerCompat
@@ -17,6 +18,7 @@ import androidx.core.view.WindowInsetsCompat.Type.displayCutout
import androidx.core.view.WindowInsetsCompat.Type.ime
import androidx.core.view.WindowInsetsCompat.Type.navigationBars
import androidx.core.view.WindowInsetsCompat.Type.statusBars
+import androidx.core.view.WindowInsetsCompat.Type.systemBars
import com.ichi2.anki.RobolectricTest.Companion.advanceRobolectricLooper
import com.ichi2.utils.Dp
import com.ichi2.utils.dp
@@ -157,7 +159,6 @@ fun Activity.dispatchInsets(
* nothing marks the area on a real device either.
* @param bottomCornerRadius the radius of both bottom rounded display corners
*/
-@SuppressLint("RtlHardcoded") // insets and cutouts are physical: not layout-direction relative
fun Activity.simulateSystemBars(
cutoutLeft: Dp = 0.dp,
navBarBottom: Dp = 48.dp,
@@ -179,21 +180,33 @@ fun Activity.simulateSystemBars(
setRoundedCorner(position, RoundedCornerCompat(position, radius, radius, radius))
}
}.build()
- ViewCompat.dispatchApplyWindowInsets(findViewById(android.R.id.content), insets)
+ window.simulateSystemBars(insets, insetTarget = findViewById(android.R.id.content))
+}
- val decor = window.decorView as ViewGroup
+/**
+ * Injects [insets] into an activity or dialog window and marks the system bars and display cutout
+ * with translucent bands. Use [windowInsetsOf] to build the insets.
+ *
+ * @param insetTarget defaults to the decor; use the content view to bypass platform decor handling.
+ */
+@SuppressLint("RtlHardcoded") // insets and cutouts are physical: not layout-direction relative
+fun Window.simulateSystemBars(
+ insets: WindowInsetsCompat,
+ insetTarget: View = decorView,
+) {
+ ViewCompat.dispatchApplyWindowInsets(insetTarget, insets)
+
+ val decor = decorView as ViewGroup
+ val bars = insets.getInsets(systemBars() or displayCutout())
val bands =
- buildList {
- add(FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, statusBarHeight.toPx(context), Gravity.TOP))
- if (navBarBottom.dp > 0) {
- add(FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, navBarBottom.toPx(context), Gravity.BOTTOM))
- }
- if (cutoutLeft.dp > 0) {
- add(FrameLayout.LayoutParams(cutoutLeft.toPx(context), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.LEFT))
- }
- }
- bands.forEach { params ->
- decor.addView(View(this).apply { setBackgroundColor(0x80000000.toInt()) }, params)
+ listOf(
+ FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, bars.top, Gravity.TOP),
+ FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, bars.bottom, Gravity.BOTTOM),
+ FrameLayout.LayoutParams(bars.left, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.LEFT),
+ FrameLayout.LayoutParams(bars.right, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.RIGHT),
+ )
+ bands.filter { it.width != 0 && it.height != 0 }.forEach { params ->
+ decor.addView(View(context).apply { setBackgroundColor(0x80000000.toInt()) }, params)
}
advanceRobolectricLooper()
}
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/IntentAssert.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/IntentAssert.kt
index 73b8f881b090..a8086a325093 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/IntentAssert.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/IntentAssert.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.testutils
import android.os.Bundle
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/JsonUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/JsonUtils.kt
index 797bdfb6d826..91eca9c2be48 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/JsonUtils.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/JsonUtils.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/KeyEventUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/KeyEventUtils.kt
index 453a212ec24a..544968dc8411 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/KeyEventUtils.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/KeyEventUtils.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/MockFragmentClassLoader.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/MockFragmentClassLoader.kt
index 49164d0f70bb..9dcfbe67dfb6 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/MockFragmentClassLoader.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/MockFragmentClassLoader.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2022 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/MockUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/MockUtils.kt
index b049f86ffbe3..d8c6e73d2eae 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/MockUtils.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/MockUtils.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ProductionCollectionManager.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ProductionCollectionManager.kt
index 94405bf86cb3..3353307cc8f1 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ProductionCollectionManager.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ProductionCollectionManager.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ShadowStatFs.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ShadowStatFs.kt
index 639c6544c352..a22fe79e1aab 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ShadowStatFs.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ShadowStatFs.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2022 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/TestException.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/TestException.kt
index 794e71e5ecf2..29d255944330 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/TestException.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/TestException.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2022 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ViewUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ViewUtils.kt
index 2c13e7bcfe2a..e4ca22905643 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ViewUtils.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ViewUtils.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2022 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiActivity.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiActivity.kt
index cb9bc0dcf04c..54f5f24a3e6c 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiActivity.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiActivity.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils.ext
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiTest.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiTest.kt
index ab1c9e1e4f84..7d80917d22fc 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ext/AnkiTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils.ext
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ext/Collection.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ext/Collection.kt
index e96f9e39b7d6..1480b8b5cf9d 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ext/Collection.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ext/Collection.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils.ext
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/rules/MockitoCollectionRule.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/rules/MockitoCollectionRule.kt
index 3b109d8764e5..95dac2cdd915 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/rules/MockitoCollectionRule.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/rules/MockitoCollectionRule.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.testutils.rules
diff --git a/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt b/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt
index e12340fcc165..e02a4742ae6e 100644
--- a/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/ui/BindingPreferenceTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.ui
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/ContentResolverUtilTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/ContentResolverUtilTest.kt
index 8eaa24832cea..86e0cf80c530 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/ContentResolverUtilTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/ContentResolverUtilTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.utils
import android.content.ContentResolver
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt
index aa28614bda98..c51a633c9937 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.utils
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/ExceptionUtilTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/ExceptionUtilTest.kt
index c2164ae4a4d1..243adf63449d 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/ExceptionUtilTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/ExceptionUtilTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.utils
import com.ichi2.utils.ExceptionUtil.getExceptionMessage
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/JSONObjectTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/JSONObjectTest.kt
index bc531aaf9a4b..c51efa5f0e72 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/JSONObjectTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/JSONObjectTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.utils
import android.annotation.SuppressLint
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/LanguageUtilsTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/LanguageUtilsTest.kt
index a78478e325b5..469d48556795 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/LanguageUtilsTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/LanguageUtilsTest.kt
@@ -1,18 +1,5 @@
-/*
- Copyright (c) 2020 David Allison
+// SPDX-License-Identifier: GPL-3.0-or-later
- 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.utils
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/ResourceLoader.kt b/AnkiDroid/src/test/java/com/ichi2/utils/ResourceLoader.kt
index 2d87e0f157cf..bf1e3eb12ae0 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/ResourceLoader.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/ResourceLoader.kt
@@ -1,6 +1,5 @@
/*
* Copyright (c) 2014 Houssam Salem
- * Copyright (c) 2021 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
diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/StrictMock.kt b/AnkiDroid/src/test/java/com/ichi2/utils/StrictMock.kt
index f4764f5702f9..e658806bc80d 100644
--- a/AnkiDroid/src/test/java/com/ichi2/utils/StrictMock.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/utils/StrictMock.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.utils
diff --git a/AnkiDroid/src/test/java/com/ichi2/widget/WidgetUtilsIntegrationTest.kt b/AnkiDroid/src/test/java/com/ichi2/widget/WidgetUtilsIntegrationTest.kt
index 5acb13053fc5..afee0edfef6a 100644
--- a/AnkiDroid/src/test/java/com/ichi2/widget/WidgetUtilsIntegrationTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/widget/WidgetUtilsIntegrationTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.widget
diff --git a/anki-common/build.gradle.kts b/anki-common/build.gradle.kts
index 55f596f3cd0d..eaaeb1b72c4f 100644
--- a/anki-common/build.gradle.kts
+++ b/anki-common/build.gradle.kts
@@ -37,9 +37,20 @@ dependencies {
implementation(libs.jakewharton.timber)
implementation(libs.kotlinx.coroutines.core)
+ testImplementation(testFixtures(project(":anki-common")))
+ testImplementation(libs.hamcrest)
+ testImplementation(libs.junit.jupiter)
+ testImplementation(libs.junit.platform.launcher)
+ testImplementation(libs.junit.vintage.engine)
+ testImplementation(libs.kotlin.reflect)
+ testImplementation(libs.kotlinx.coroutines.test)
+
testFixturesImplementation(project(":common:android"))
+ testFixturesImplementation(project(":libanki"))
testFixturesImplementation(libs.androidx.core.ktx)
testFixturesImplementation(libs.androidx.test.core)
+ testFixturesImplementation(libs.jakewharton.timber)
+ testFixturesImplementation(libs.kotlin.test)
// The Kotlin Compose Compiler plugin attaches to every Kotlin compilation in the
// module including testFixtures, which has no @Composable code and refuses to
// run unless the Compose Runtime is on the classpath. compileOnly satisfies the
diff --git a/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt b/anki-common/src/main/kotlin/com/ichi2/anim/ActivityTransitionAnimation.kt
similarity index 98%
rename from AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt
rename to anki-common/src/main/kotlin/com/ichi2/anim/ActivityTransitionAnimation.kt
index b5dd1d7c8076..7cf1d03665a2 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anim/ActivityTransitionAnimation.kt
+++ b/anki-common/src/main/kotlin/com/ichi2/anim/ActivityTransitionAnimation.kt
@@ -6,7 +6,7 @@ import android.app.Activity
import android.content.Context
import android.util.LayoutDirection
import androidx.annotation.AnimRes
-import com.ichi2.anki.R
+import com.ichi2.anki.ankicommon.R
import com.ichi2.anki.common.ui.TransitionDirection
import com.ichi2.anki.compat.CompatHelper
diff --git a/anki-common/src/main/kotlin/com/ichi2/anki/LaunchCatching.kt b/anki-common/src/main/kotlin/com/ichi2/anki/LaunchCatching.kt
new file mode 100644
index 000000000000..1d67b86a0539
--- /dev/null
+++ b/anki-common/src/main/kotlin/com/ichi2/anki/LaunchCatching.kt
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: Copyright (c) 2023 Brayan Oliveira <69634269+brayandso@users.noreply.github.com>
+
+package com.ichi2.anki
+
+import com.ichi2.anki.libanki.exception.InvalidSearchException
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import net.ankiweb.rsdroid.BackendException
+import timber.log.Timber
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * Runs a suspend function that catches any uncaught errors and reports them to the user.
+ * Errors from the backend contain localized text that is often suitable to show to the user as-is.
+ * Other errors should ideally be handled in the block.
+ *
+ * @param context Coroutine context passed to [launch]
+ * @param errorMessageHandler Called after an exception is caught and logged, input is either
+ * `Exception.localizedMessage` or `Exception.toString()`
+ * @param block code to execute inside [launch]
+ */
+fun CoroutineScope.launchCatching(
+ context: CoroutineContext = EmptyCoroutineContext,
+ errorMessageHandler: suspend (String) -> Unit,
+ block: suspend CoroutineScope.() -> Unit,
+): Job =
+ launch(context) {
+ try {
+ block()
+ } catch (cancellationException: CancellationException) {
+ // CancellationException should be re-thrown to propagate it to the parent coroutine
+ throw cancellationException
+ } catch (exception: Exception) {
+ Timber.w(exception)
+ val message =
+ when (exception) {
+ is BackendException, is InvalidSearchException -> exception.localizedMessage
+ else -> null
+ } ?: exception.toString()
+ errorMessageHandler.invoke(message)
+ }
+ }
diff --git a/anki-common/src/main/kotlin/com/ichi2/anki/common/destinations/DeckOptionsDestination.kt b/anki-common/src/main/kotlin/com/ichi2/anki/common/destinations/DeckOptionsDestination.kt
index bc0327a8a858..8fad0b3873da 100644
--- a/anki-common/src/main/kotlin/com/ichi2/anki/common/destinations/DeckOptionsDestination.kt
+++ b/anki-common/src/main/kotlin/com/ichi2/anki/common/destinations/DeckOptionsDestination.kt
@@ -2,6 +2,8 @@
package com.ichi2.anki.common.destinations
+import androidx.annotation.CheckResult
+import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.libanki.DeckId
/**
@@ -14,7 +16,23 @@ data class DeckOptionsDestination(
val isFiltered: Boolean,
val options: List = emptyList(),
) : Destination() {
- companion object
+ companion object {
+ suspend fun fromDeckId(deckId: DeckId): DeckOptionsDestination =
+ DeckOptionsDestination(
+ deckId = deckId,
+ isFiltered = withCol { decks.isFiltered(deckId) },
+ )
+
+ @CheckResult
+ suspend fun fromCurrentDeck(): DeckOptionsDestination =
+ withCol {
+ val deckId = decks.getCurrentId()
+ DeckOptionsDestination(
+ deckId = deckId,
+ isFiltered = decks.isFiltered(deckId),
+ )
+ }
+ }
}
/**
diff --git a/AnkiDroid/src/main/res/anim/fade_in.xml b/anki-common/src/main/res/anim/fade_in.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/fade_in.xml
rename to anki-common/src/main/res/anim/fade_in.xml
diff --git a/AnkiDroid/src/main/res/anim/fade_out.xml b/anki-common/src/main/res/anim/fade_out.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/fade_out.xml
rename to anki-common/src/main/res/anim/fade_out.xml
diff --git a/AnkiDroid/src/main/res/anim/none.xml b/anki-common/src/main/res/anim/none.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/none.xml
rename to anki-common/src/main/res/anim/none.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_down_in.xml b/anki-common/src/main/res/anim/slide_down_in.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_down_in.xml
rename to anki-common/src/main/res/anim/slide_down_in.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_down_out.xml b/anki-common/src/main/res/anim/slide_down_out.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_down_out.xml
rename to anki-common/src/main/res/anim/slide_down_out.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_left_in.xml b/anki-common/src/main/res/anim/slide_left_in.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_left_in.xml
rename to anki-common/src/main/res/anim/slide_left_in.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_left_out.xml b/anki-common/src/main/res/anim/slide_left_out.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_left_out.xml
rename to anki-common/src/main/res/anim/slide_left_out.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_right_in.xml b/anki-common/src/main/res/anim/slide_right_in.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_right_in.xml
rename to anki-common/src/main/res/anim/slide_right_in.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_right_out.xml b/anki-common/src/main/res/anim/slide_right_out.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_right_out.xml
rename to anki-common/src/main/res/anim/slide_right_out.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_up_in.xml b/anki-common/src/main/res/anim/slide_up_in.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_up_in.xml
rename to anki-common/src/main/res/anim/slide_up_in.xml
diff --git a/AnkiDroid/src/main/res/anim/slide_up_out.xml b/anki-common/src/main/res/anim/slide_up_out.xml
similarity index 100%
rename from AnkiDroid/src/main/res/anim/slide_up_out.xml
rename to anki-common/src/main/res/anim/slide_up_out.xml
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/CoroutineHelpersTest.kt b/anki-common/src/test/kotlin/com/ichi2/anki/LaunchCatchingTest.kt
similarity index 97%
rename from AnkiDroid/src/test/java/com/ichi2/anki/CoroutineHelpersTest.kt
rename to anki-common/src/test/kotlin/com/ichi2/anki/LaunchCatchingTest.kt
index a80654105e2d..4a4c0a0dc0d5 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/CoroutineHelpersTest.kt
+++ b/anki-common/src/test/kotlin/com/ichi2/anki/LaunchCatchingTest.kt
@@ -9,7 +9,7 @@ import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Test
-class CoroutineHelpersTest {
+class LaunchCatchingTest {
@Test
fun `launchCatching does not include class names for InvalidSearchException`() =
runTest {
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/observability/ChangeManagerTest.kt b/anki-common/src/test/kotlin/com/ichi2/anki/observability/ChangeManagerTest.kt
similarity index 64%
rename from AnkiDroid/src/test/java/com/ichi2/anki/observability/ChangeManagerTest.kt
rename to anki-common/src/test/kotlin/com/ichi2/anki/observability/ChangeManagerTest.kt
index 5e402a50eb1e..9dc7499cd623 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/observability/ChangeManagerTest.kt
+++ b/anki-common/src/test/kotlin/com/ichi2/anki/observability/ChangeManagerTest.kt
@@ -2,30 +2,38 @@
package com.ichi2.anki.observability
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.app.Activity
+import android.content.Context
import anki.collection.OpChanges
-import com.ichi2.testutils.EmptyApplication
-import com.ichi2.testutils.JvmTest
-import com.ichi2.testutils.subscriberChangeCounter
+import com.ichi2.anki.common.crashreporting.CrashReportService
+import com.ichi2.anki.common.crashreporting.CrashReporter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.greaterThan
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-import org.robolectric.annotation.Config
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.javaType
import kotlin.reflect.jvm.isAccessible
-// this cannot yet use `EmptyApplication` - `CrashReportService` dependency
-@RunWith(AndroidJUnit4::class)
-class ChangeManagerExceptionHandlingTest : JvmTest() {
+class ChangeManagerExceptionHandlingTest {
+ @Before
+ fun setUp() {
+ ChangeManager.resetForTesting()
+ // an exception in a subscriber is sent to the crash reporting service
+ CrashReportService.setReporter(NoOpCrashReporter)
+ }
+
@Test
fun `all subscribers notified even if one throws exception`() {
var firstCalled = false
@@ -96,9 +104,17 @@ class ChangeManagerTest {
}
}
-@RunWith(AndroidJUnit4::class)
-@Config(application = EmptyApplication::class)
-class ChangeManagerPublishTest : JvmTest() {
+class ChangeManagerPublishTest {
+ @Before
+ fun setUp() {
+ ChangeManager.resetForTesting()
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
@Test
fun `publish notifies multiple subscribers asynchronously`() =
runTest {
@@ -118,3 +134,59 @@ class ChangeManagerPublishTest : JvmTest() {
assertThat("Second subscriber should be notified", counter2.hasChanges, equalTo(true))
}
}
+
+/**
+ * Produces a [ChangeCounter] which is subscribed to [ChangeManager].
+ *
+ * Query the result via [.hasChanges][ChangeCounter.hasChanges]
+ */
+private fun subscriberChangeCounter(): ChangeCounter {
+ val counter = ChangeCounter()
+ ChangeManager.subscribe(counter)
+ return counter
+}
+
+/** A [CrashReporter] which ignores all reports */
+private object NoOpCrashReporter : CrashReporter {
+ override fun sendExceptionReport(
+ message: String?,
+ origin: String?,
+ ) {
+ }
+
+ override fun sendExceptionReport(
+ e: Throwable,
+ origin: String?,
+ additionalInfo: String?,
+ onlyIfSilent: Boolean,
+ ) {
+ }
+
+ override fun sendExceptionReport(
+ e: Throwable,
+ origin: String?,
+ additionalInfo: String?,
+ onlyIfSilent: Boolean,
+ context: Context,
+ ) {
+ }
+
+ override fun onPreferenceChanged(
+ ctx: Context,
+ newValue: String,
+ ) {
+ }
+
+ override fun deleteLimiterData(context: Context) {
+ }
+
+ override fun setReportingMode(value: String) {
+ }
+
+ override fun isEnabled(
+ context: Context,
+ defaultValue: Boolean,
+ ): Boolean = false
+
+ override fun sendReport(activity: Activity): Boolean = false
+}
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/TestChangeSubscriber.kt b/anki-common/src/testFixtures/kotlin/com/ichi2/anki/observability/TestChangeSubscriber.kt
similarity index 80%
rename from AnkiDroid/src/test/java/com/ichi2/testutils/TestChangeSubscriber.kt
rename to anki-common/src/testFixtures/kotlin/com/ichi2/anki/observability/TestChangeSubscriber.kt
index 48b8cb0c1c44..ae1fa9b41020 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/TestChangeSubscriber.kt
+++ b/anki-common/src/testFixtures/kotlin/com/ichi2/anki/observability/TestChangeSubscriber.kt
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later
-package com.ichi2.testutils
+package com.ichi2.anki.observability
import anki.collection.OpChanges
-import com.ichi2.anki.observability.ChangeManager
import timber.log.Timber
import kotlin.test.fail
@@ -59,8 +58,12 @@ suspend fun ensureOpWithHandler(
fail("ChangeManager: expected handler to be $handler, but was ${handlerAccessor.handler}")
}
-// used to ensure a strong reference to the subscription is held
-internal class ChangeCounter : ChangeManager.Subscriber {
+/**
+ * Counts calls to [ChangeManager.notifySubscribers]
+ *
+ * Used to ensure a strong reference to the subscription is held
+ */
+class ChangeCounter : ChangeManager.Subscriber {
private var changes = 0
val changeCount get() = changes
val hasChanges get() = changes > 0
@@ -85,14 +88,3 @@ private class ExtractOpHandler : ChangeManager.Subscriber {
this.handler = handler
}
}
-
-/**
- * Produces a [ChangeCounter] which is subscribed to [ChangeManager].
- *
- * Query the result via [.changeCount][ChangeCounter.changeCount] or [.hasChanges][ChangeCounter.hasChanges]
- */
-internal fun subscriberChangeCounter(): ChangeCounter {
- val counter = ChangeCounter()
- ChangeManager.subscribe(counter)
- return counter
-}
diff --git a/anki-common/src/testFixtures/kotlin/com/ichi2/testutils/ext/DeckConfig.kt b/anki-common/src/testFixtures/kotlin/com/ichi2/testutils/ext/DeckConfig.kt
new file mode 100644
index 000000000000..a4ef5d62207d
--- /dev/null
+++ b/anki-common/src/testFixtures/kotlin/com/ichi2/testutils/ext/DeckConfig.kt
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.testutils.ext
+
+import com.ichi2.anki.libanki.Collection
+import com.ichi2.anki.libanki.Consts
+
+/** The preset's new-card limit for the default deck. */
+val Collection.defaultDeckNewCardsPerDay: Int
+ get() {
+ val data = backend.getDeckConfigsForUpdate(Consts.DEFAULT_DECK_ID)
+ return data.allConfigList
+ .single { it.config.id == data.currentDeck.configId }
+ .config.config.newPerDay
+ }
diff --git a/build.gradle.kts b/build.gradle.kts
index a9fdc9dc975f..e69928ced7d6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,6 +3,7 @@ import com.android.build.api.dsl.CommonExtension
import com.android.build.api.extension.impl.AndroidComponentsExtensionImpl
import com.ichi2.anki.gradle.GitHubActionsTestListener
import com.ichi2.anki.gradle.TestSummaryService
+import com.ichi2.anki.gradle.configureRobolectricJvm
import com.slack.keeper.optInToKeeper
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.buildconfiguration.tasks.UpdateDaemonJvm
@@ -80,6 +81,8 @@ subprojects {
it.maxHeapSize = "${unitTestForkMaxHeapGb}g"
it.minHeapSize = "1g"
+ it.configureRobolectricJvm()
+
it.useJUnitPlatform()
it.testLogging {
events("failed", "skipped")
@@ -138,7 +141,7 @@ subprojects {
compilerArgs += "-XXLanguage:+ExplicitBackingFields"
}
- if (project.path !in listOf(":anki-common", ":api", ":common", ":common:android")) {
+ if (project.path !in listOf(":api", ":common", ":common:android")) {
compilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
if (project.path != ":api") {
diff --git a/buildSrc/src/main/kotlin/com/ichi2/anki/gradle/Robolectric.kt b/buildSrc/src/main/kotlin/com/ichi2/anki/gradle/Robolectric.kt
new file mode 100644
index 000000000000..87efbe38b9b8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/ichi2/anki/gradle/Robolectric.kt
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.gradle
+
+import org.gradle.api.tasks.testing.Test
+
+/**
+ * Opens the JDK internals required by Robolectric on Java 17 and higher.
+ *
+ * See https://robolectric.org/getting-started/#running-with-java-17-and-higher
+ */
+fun Test.configureRobolectricJvm() {
+ jvmArgs(
+ "--add-opens=java.base/java.lang=ALL-UNNAMED",
+ "--add-opens=java.base/java.util=ALL-UNNAMED",
+ "--add-opens=java.base/java.io=ALL-UNNAMED",
+ "--add-opens=java.base/java.net=ALL-UNNAMED",
+ "--add-opens=java.base/java.security=ALL-UNNAMED",
+ "--add-opens=java.base/java.text=ALL-UNNAMED",
+ "--add-opens=java.base/jdk.internal.access=ALL-UNNAMED",
+ "--add-opens=java.desktop/java.awt.font=ALL-UNNAMED",
+ "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+ )
+}
diff --git a/common/android/src/main/java/com/ichi2/anki/common/permissions/Permissions.kt b/common/android/src/main/java/com/ichi2/anki/common/permissions/Permissions.kt
index cf3edbcb1889..a2addfacf36f 100644
--- a/common/android/src/main/java/com/ichi2/anki/common/permissions/Permissions.kt
+++ b/common/android/src/main/java/com/ichi2/anki/common/permissions/Permissions.kt
@@ -1,5 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
-// SPDX-FileCopyrightText: 2025 David Allison
// SPDX-FileCopyrightText: 2026 Ashish Yadav
package com.ichi2.anki.common.permissions
diff --git a/common/android/src/main/java/com/ichi2/anki/common/permissions/StoragePermissions.kt b/common/android/src/main/java/com/ichi2/anki/common/permissions/StoragePermissions.kt
index 6de8ad978894..bda6c4e658c8 100644
--- a/common/android/src/main/java/com/ichi2/anki/common/permissions/StoragePermissions.kt
+++ b/common/android/src/main/java/com/ichi2/anki/common/permissions/StoragePermissions.kt
@@ -1,5 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
-// SPDX-FileCopyrightText: 2025 David Allison
// SPDX-FileCopyrightText: 2026 Ashish Yadav
package com.ichi2.anki.common.permissions
diff --git a/common/android/src/main/java/com/ichi2/anki/common/ui/TransitionDirection.kt b/common/android/src/main/java/com/ichi2/anki/common/ui/TransitionDirection.kt
index 7c9eb3182524..6e1f8d053d00 100644
--- a/common/android/src/main/java/com/ichi2/anki/common/ui/TransitionDirection.kt
+++ b/common/android/src/main/java/com/ichi2/anki/common/ui/TransitionDirection.kt
@@ -8,7 +8,7 @@ import kotlinx.parcelize.Parcelize
/**
* Direction of an Activity transition animation.
*
- * Consumed by `ActivityTransitionAnimation` (AnkiDroid app module) to choose
+ * Consumed by `ActivityTransitionAnimation` (`:anki-common`) to choose
* the appropriate slide/fade animation. Lives here so navigation destinations
* in lower modules can pass a direction across the module boundary.
*/
diff --git a/compat/src/main/java/com/ichi2/anki/compat/PackageManagerCompat.kt b/compat/src/main/java/com/ichi2/anki/compat/PackageManagerCompat.kt
index eddc2948b74f..d53b50767f49 100644
--- a/compat/src/main/java/com/ichi2/anki/compat/PackageManagerCompat.kt
+++ b/compat/src/main/java/com/ichi2/anki/compat/PackageManagerCompat.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2023 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/BrowserConfig.kt b/libanki/src/main/java/com/ichi2/anki/libanki/BrowserConfig.kt
index 78a30c1b8be3..ef06011235da 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/BrowserConfig.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/BrowserConfig.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2024 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/CardTemplate.kt b/libanki/src/main/java/com/ichi2/anki/libanki/CardTemplate.kt
index 67a6ecc61f26..18f11792ec31 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/CardTemplate.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/CardTemplate.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt
index bc47011ecc66..45c78a5e25fe 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Collection.kt
@@ -76,6 +76,7 @@ import com.ichi2.anki.libanki.sched.DummyScheduler
import com.ichi2.anki.libanki.sched.Scheduler
import com.ichi2.anki.libanki.utils.LibAnkiAlias
import com.ichi2.anki.libanki.utils.NotInPyLib
+import com.ichi2.anki.libanki.utils.capitalizePy
import net.ankiweb.rsdroid.Backend
import net.ankiweb.rsdroid.BackendException.BackendFatalError
import net.ankiweb.rsdroid.BackendException.BackendSearchException
@@ -653,12 +654,17 @@ class Collection(
@CheckResult
@LibAnkiAlias("nextID")
- @RustCleanup("Python returns 'Any' - may fail for Double?")
- @Deprecated("not implemented", level = DeprecationLevel.HIDDEN)
fun nextId(
type: String,
inc: Boolean = true,
- ): Long = TODO()
+ ): Long {
+ val cType = "next" + type.capitalizePy()
+ val id = config.get(cType) ?: 1L
+ if (inc) {
+ config.set(cType, id + 1L)
+ }
+ return id
+ }
/*
* Notes
@@ -754,9 +760,17 @@ class Collection(
fun cardIdsOfNote(nid: NoteId): List = backend.cardsOfNote(nid = nid)
/**
- * Get starting deck and notetype for add screen.
- * An option in the preferences controls whether this will be based on the current deck
- * or current notetype.
+ * Returns the initial deck and note type for the Add screen.
+ *
+ * Use both values together. For note-type changes, prefer [defaultDeckForNoteType].
+ *
+ * Selection behavior based on [ConfigKey.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK]:
+ * - **Enabled**: Prioritizes the deck, then its last recorded note type (fallback: current or first available).
+ * - **Disabled**: Prioritizes the current note type (or first available), then its last recorded valid deck.
+ *
+ * Deck fallback chain: Selected non-filtered deck -> [currentReviewCard]'s deck -> [Default][Decks.getDefault].
+ *
+ * @see Upstream defaults_for_adding
*/
@CheckResult
@LibAnkiAlias("defaults_for_adding")
@@ -766,12 +780,18 @@ class Collection(
}
/**
- * If 'change deck depending on notetype' is enabled in the preferences,
- * return the last deck used with the provided notetype, if any..
+ * Returns the last requested deck recorded by [addNote] for [noteTypeId], if that deck still
+ * exists and is not filtered.
+ *
+ * Returns `null` in [current-deck mode][ConfigKey.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK] or when
+ * there is no usable recorded deck; the editor should then keep its current deck.
+ *
+ * The note type's existence is not checked.
+ *
+ * @see Upstream default_deck_for_notetype
*/
@CheckResult
@LibAnkiAlias("default_deck_for_notetype")
- @RustCleanup("check if the == 0L logic is necessary")
fun defaultDeckForNoteType(noteTypeId: NoteTypeId): DeckId? {
if (config.getBool(ConfigKey.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK)) {
return null
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/CollectionFiles.kt b/libanki/src/main/java/com/ichi2/anki/libanki/CollectionFiles.kt
index 9004c47b57e5..daf7be4e069f 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/CollectionFiles.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/CollectionFiles.kt
@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Arthur Milchior
- * Copyright (c) 2025 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/DeckConfig.kt b/libanki/src/main/java/com/ichi2/anki/libanki/DeckConfig.kt
index 8c5cd82a2a41..189e3bf4592b 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/DeckConfig.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/DeckConfig.kt
@@ -1,5 +1,4 @@
/*
- Copyright (c) 2020 David Allison
Copyright (c) 2020 Arthur Milchior
This program is free software; you can redistribute it and/or modify it under
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Decks.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Decks.kt
index eed6ae4c7c1b..ade1aaaa63e5 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Decks.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Decks.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2025 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Field.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Field.kt
index 14cd2b43dcff..ac49c4976fa6 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Field.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Field.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Fields.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Fields.kt
index 8607234cea06..cd57659f3777 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Fields.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Fields.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2024 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Lang.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Lang.kt
index eb48f0af7d84..ca3e81dd88eb 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Lang.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Lang.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/LibAnki.kt b/libanki/src/main/java/com/ichi2/anki/libanki/LibAnki.kt
index 06e2dfbf1d25..06698f5686cc 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/LibAnki.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/LibAnki.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt
index 7a08ec30eca7..1ed0204aaf85 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Notetypes.kt
@@ -3,7 +3,6 @@
* Copyright (c) 2010 Rick Gruber-Riemer *
* Copyright (c) 2011 Norbert Nagold *
* Copyright (c) 2011 Kostas Spyropoulos *
- * Copyright (c) 2021 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/PythonTypes.kt b/libanki/src/main/java/com/ichi2/anki/libanki/PythonTypes.kt
index 0898809ce7dc..955e0abb0fa5 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/PythonTypes.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/PythonTypes.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Sound.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Sound.kt
index 44b98a726afc..bd5752c9b0c5 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Sound.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Sound.kt
@@ -1,7 +1,6 @@
/*
* Copyright (c) 2009 Edu Zamora
* Copyright (c) 2014 Timothy rae
- * Copyright (c) 2021 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/TemplateManager.kt b/libanki/src/main/java/com/ichi2/anki/libanki/TemplateManager.kt
index 3302efc80ee5..4cacbae8e22b 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/TemplateManager.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/TemplateManager.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2021 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/Tts.kt b/libanki/src/main/java/com/ichi2/anki/libanki/Tts.kt
index 6d243db6f746..86aaf06edd86 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/Tts.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/Tts.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2023 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
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/backend/BackendUtils.kt b/libanki/src/main/java/com/ichi2/anki/libanki/backend/BackendUtils.kt
index a1e4b0aac5be..22663c71e66a 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/backend/BackendUtils.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/backend/BackendUtils.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
@file:Suppress("FunctionName")
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/backend/model/NoteUtil.kt b/libanki/src/main/java/com/ichi2/anki/libanki/backend/model/NoteUtil.kt
index 35a3575d8d3f..bc2e2d75af7b 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/backend/model/NoteUtil.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/backend/model/NoteUtil.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki.backend.model
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/exception/EmptyMediaException.kt b/libanki/src/main/java/com/ichi2/anki/libanki/exception/EmptyMediaException.kt
index c8beea090a50..b5073f17a5d9 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/exception/EmptyMediaException.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/exception/EmptyMediaException.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki.libanki.exception
/**
diff --git a/libanki/src/main/java/com/ichi2/anki/libanki/utils/PythonExtensions.kt b/libanki/src/main/java/com/ichi2/anki/libanki/utils/PythonExtensions.kt
index 99cfc60ba4a9..df7b0a93f9a0 100644
--- a/libanki/src/main/java/com/ichi2/anki/libanki/utils/PythonExtensions.kt
+++ b/libanki/src/main/java/com/ichi2/anki/libanki/utils/PythonExtensions.kt
@@ -136,3 +136,5 @@ fun len(templates: JSONContainer) = templates.jsonArra
// Changed signature from `copy.deepcopy(clone_from)`
fun DeckConfig.deepClone() = this.copy(jsonObject = this.jsonObject.deepClone())
+
+internal fun String.capitalizePy(): String = if (isEmpty()) this else first().titlecase() + drop(1).lowercase()
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/AbstractSchedTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/AbstractSchedTest.kt
index d2f0a17cec57..ac437b2f0356 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/AbstractSchedTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/AbstractSchedTest.kt
@@ -1,18 +1,5 @@
-/*
- Copyright (c) 2020 David Allison
+// SPDX-License-Identifier: GPL-3.0-or-later
- 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.libanki
import anki.scheduler.CardAnswer.Rating
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/CollectionOnDiskTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/CollectionOnDiskTest.kt
index da48e98e6b6d..d01d485bfe8a 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/CollectionOnDiskTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/CollectionOnDiskTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/CollectionTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/CollectionTest.kt
index d238a0fc2772..57a57d2a2838 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/CollectionTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/CollectionTest.kt
@@ -361,4 +361,20 @@ class CollectionTest : InMemoryAnkiTest() {
fun `get all columns`() {
assertThat(col.allBrowserColumns(), not(hasSize(0)))
}
+
+ @Test
+ fun `nextId new type`() {
+ assertEquals(1L, col.nextId(type = "someType"))
+ }
+
+ @Test
+ fun `nextId when inc is false`() {
+ assertEquals(1L, col.nextId(type = "someType", inc = false))
+ }
+
+ @Test
+ fun `nextId increments by default`() {
+ assertEquals(1L, col.nextId(type = "someType"))
+ assertEquals(2L, col.nextId(type = "someType"))
+ }
}
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/ConfigTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/ConfigTest.kt
index a867d4563796..f7504891cb9e 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/ConfigTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/ConfigTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/MetaTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/MetaTest.kt
index cfc939b35d3f..64b2fbb00fc5 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/MetaTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/MetaTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki.libanki
import com.ichi2.anki.libanki.testutils.InMemoryAnkiTest
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/NoteTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/NoteTest.kt
index 4c136aab1e8d..4ab7a6ba022f 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/NoteTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/NoteTest.kt
@@ -1,18 +1,5 @@
-/*
- * Copyright (c) 2020 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
+
package com.ichi2.anki.libanki
import com.ichi2.anki.libanki.Note.ClozeUtils
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/NoteWithColTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/NoteWithColTest.kt
index cb3fa88bf197..32dadcf4f69a 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/NoteWithColTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/NoteWithColTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/NotetypesTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/NotetypesTest.kt
index 0550b677777d..d384c3928a4a 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/NotetypesTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/NotetypesTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/PythonExtensionsTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/PythonExtensionsTest.kt
index c0b73f365d44..0ac8b7807b95 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/PythonExtensionsTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/PythonExtensionsTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/SchedulerTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/SchedulerTest.kt
index 6c0b25c3da71..76d81f226e8b 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/SchedulerTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/SchedulerTest.kt
@@ -1,18 +1,5 @@
-/*
- Copyright (c) 2020 David Allison
+// SPDX-License-Identifier: GPL-3.0-or-later
- 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.libanki
import android.annotation.SuppressLint
diff --git a/libanki/src/test/java/com/ichi2/anki/libanki/StorageRustTest.kt b/libanki/src/test/java/com/ichi2/anki/libanki/StorageRustTest.kt
index dea0eba06f97..8fc6062a4cff 100644
--- a/libanki/src/test/java/com/ichi2/anki/libanki/StorageRustTest.kt
+++ b/libanki/src/test/java/com/ichi2/anki/libanki/StorageRustTest.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2021 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki
diff --git a/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/HtmlUtils.kt b/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/HtmlUtils.kt
index 1a26ec0ee203..1cf19f06af85 100644
--- a/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/HtmlUtils.kt
+++ b/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/HtmlUtils.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki.testutils
diff --git a/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Card.kt b/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Card.kt
index a45424d0eb96..fa881b7ca7f1 100644
--- a/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Card.kt
+++ b/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Card.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki.testutils.ext
diff --git a/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Collection.kt b/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Collection.kt
index 981ebed115f1..8ff28510fa41 100644
--- a/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Collection.kt
+++ b/libanki/src/testFixtures/java/com/ichi2/anki/libanki/testutils/ext/Collection.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright (c) 2025 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 .
- */
+// SPDX-License-Identifier: GPL-3.0-or-later
package com.ichi2.anki.libanki.testutils.ext
diff --git a/lint-rules/src/main/java/com/ichi2/anki/lint/rules/DuplicateCrowdInStrings.kt b/lint-rules/src/main/java/com/ichi2/anki/lint/rules/DuplicateCrowdInStrings.kt
index b0f17fd4aa41..1c911a84bf34 100644
--- a/lint-rules/src/main/java/com/ichi2/anki/lint/rules/DuplicateCrowdInStrings.kt
+++ b/lint-rules/src/main/java/com/ichi2/anki/lint/rules/DuplicateCrowdInStrings.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2020 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
diff --git a/lint-rules/src/main/java/com/ichi2/anki/lint/utils/Aapt2Util.kt b/lint-rules/src/main/java/com/ichi2/anki/lint/utils/Aapt2Util.kt
index e710dd0e2b08..e1f24da3f0dd 100644
--- a/lint-rules/src/main/java/com/ichi2/anki/lint/utils/Aapt2Util.kt
+++ b/lint-rules/src/main/java/com/ichi2/anki/lint/utils/Aapt2Util.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2022 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
diff --git a/lint-rules/src/main/java/com/ichi2/anki/lint/utils/StringFormatDetector.kt b/lint-rules/src/main/java/com/ichi2/anki/lint/utils/StringFormatDetector.kt
index e691c6650cc6..2f2c3dc89d56 100644
--- a/lint-rules/src/main/java/com/ichi2/anki/lint/utils/StringFormatDetector.kt
+++ b/lint-rules/src/main/java/com/ichi2/anki/lint/utils/StringFormatDetector.kt
@@ -1,6 +1,4 @@
/*
- * Copyright (c) 2020 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