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 @@ + + + - + android:title="@string/schedule_reminders_troubleshoot_menu" + app:showAsAction="always" /> + android:title="@string/schedule_reminders_notification_settings_menu" + app:showAsAction="always" /> 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