Skip to content

Merge upstream AnkiDroid main (2026-11-04) - #109

Merged
float3 merged 64 commits into
mainfrom
upstream-2026-11-04
Nov 4, 2026
Merged

float3 merged 64 commits into
mainfrom
upstream-2026-11-04

Conversation

@float3

@float3 float3 commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Merges the 62 commits on upstream AnkiDroid main since the last sync (up to b9e492ddbf "feat: remove the 'localStorage in Study Screen' setting"). Supersedes #104.

Conflicts

  • gradle/libs.versions.toml: both sides added a library entry on the same line after androidx-core. The fork added androidx-core-remoteviews, upstream added androidx-arch-core-testing. Kept both. Each one's version key (androidxCoreRemoteViews, androidxArchCore) merged cleanly.

No other conflicts. Files changed on both sides that merged cleanly: AnkiDroid/build.gradle.kts, AnkiDroid/src/main/AndroidManifest.xml, AnkiDroidApp.kt, DeckPickerTest.kt. Upstream deleted pages/AnkiServer.kt and NavigationDrawerActivityTest.kt, and nothing in the fork references either one.

Checks run locally

  • :AnkiDroid:compilePlayDebugKotlin, :AnkiDroid:ktlintCheck
  • :AnkiDroid:testPlayDebugUnitTest --tests 'com.ichi2.anki.ankiquest.*' --tests 'com.ichi2.anki.DeckPickerTest': 506 tests, 0 failures

criticalAY and others added 30 commits November 1, 2026 21:15
The rule dropped what `ime enable` and `ime set` printed, so a failed
switch only showed the two keyboard IDs. The restore and disable checks
already include their command output.

Android prints refusals to stderr, which `UiDevice.executeShellCommand`
does not read, so an empty output means the switch was refused.

Assisted-by: Claude Opus 5.5
When a command timed out, the test only saw "Test IME did not respond":
an exception from the keyboard's process keeps only its message. The
message now includes the stack of the keyboard's main thread.

Assisted-by: Claude Opus 5.5
The cloze tabs only had the clozes in the fields, so a card whose
cloze was removed wasn't listed, although it exists until 'Empty
Cards' is used. In the template editor, which previews the first
card, a 'Card 2' tab was then shown over 'No cloze 1 found'.

The note's existing cards are now listed too, and a card without its
cloze in the fields is marked as '(empty)'.

Part of 20919

Assisted-by: Claude Opus 5.5
The template editor previews the first card of cloze note types, so
'No cloze 1 found' was shown under a 'Card 2' tab if 'Empty Cards'
had deleted card 1. The note editor also requests card 1 if no cloze
is left in the fields.

If the requested card isn't one of the tabs, the first tab is shown.

Fixes 20919

Assisted-by: Claude Opus 5.5
WebView storage was pointed at the profile before its folders were
created. If loading then failed, AnkiDroid fell back to Default's data
but kept the failed profile's WebView storage, and on older Android it
also cleared the cookies.

A failed load now sets the WebView up for Default instead, so Default
never opens with another profile's cookies.

Assisted-by: Claude Opus 5.5
Part of 18393

Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
Assisted-by: GPT-6.1-Sol xhigh
Part of 18393

Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
Assisted-by: GPT-6.1-Sol xhigh
Part of 18393

Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
Assisted-by: GPT-6.1-Sol xhigh
The deck list and card analysis widgets refresh every minute. If the
collection could not be opened, for example while another AnkiDroid
install held it locked, the refresh threw and crashed the app. The error
is now logged and the widget keeps what it showed before.

Part of 22838

Assisted-by: Claude Opus 5.5
Every widget update listed all of the device's storage folders just to
decide whether the collection was usable, which can stall on Android's
storage service (Issue 22825). Widgets now only wait until a storage
location is chosen, and update straight away after that.

Part of 22838

Assisted-by: Claude Opus 5.5
The Add Note widget only opens the note editor, which checks storage
itself, so it no longer waits for the storage check. Its button works
as soon as the widget is placed.

Part of 22838

Assisted-by: Claude Opus 5.5
Every screen with the side drawer told the Add Note widget to refresh,
in case storage access had just been granted, and ran the slow storage
check on the main thread to decide. The widget no longer waits for
storage, so the refresh has nothing left to do.

Fixes 22838

Assisted-by: Claude Opus 5.5
This fix was based on principle of 'Treat tool changes as a third gesture
ending path' And added setActiveTool() method in the viewModel to handle
pending erase gestures before switching tools. Approach was like if the new
tool cannot continue the active stroke erase gesture, finalize the pending
erase before activating it. i.e If the new tool is also a stroke eraser
allow the gesture to continue. All six existing activeTool.value sites have
been routed through setActiveTool() and added the multiple tests as well

Fixes ankidroid#22307

Assisted-by: Claude Opus 5
AGP 9.5 Compose Preview tasks were realized, and failed for
variants without unit tests.

Now: resole test tasks by name.

Prep for AGP 9.5
PR 22859

Assisted-by: GPT-6.1-Sol xhigh
Use `anki-backend-core` (JVM) for local backend builds and test fixtures

https://redirect.github.com/ankidroid/Anki-Android-Backend/pull/770

Assisted-by: GPT-6.1-Sol xhigh
Remove Android runtime dependencies from libanki and use SLF4J logging.
Keep Android cursor adapters in anki-common and restrict FrameworkDatabase
to the helpers used by its callers.

Document moving shared cursor paging and conversion logic into
anki-backend-core.

Assisted-by: GPT-6.1-Sol xhigh
`JvmTest.runTest` now uses the coroutine test scheduler, allowing
ViewModel/collection tests to move away from Robolectric.

Same code as already used by Robolectric

Assisted-by: GPT-6.1-Sol xhigh
Assisted-by: GPT-6.1-Sol xhigh
InstantTaskExecutorRule (AndroidX) is used so we don't need an Android
looper.

Assisted-by: GPT-6.1-Sol xhigh
OnDiskJvmTest allows for these to execute using a temp directory for
these tests.

Assisted-by: GPT-6.1-Sol xhigh
Keeping the integration checks under the Android runner
(WorkManager-related)

Assisted-by: GPT-6.1-Sol xhigh
Run settings checks on the JVM and retain a Robolectric integration test
that verifies the factory loads the saved action and display durations.

Assisted-by: GPT-6.1-Sol xhigh
criticalAY and others added 25 commits November 3, 2026 06:58
Typing a search before the deck list had loaded, or expanding or
collapsing a deck in the results, brought back every deck while the
search text was still there. The list now keeps the search applied.

Assisted-by: Claude Opus 5.5
Adding or cloning a note type ran its collection work inside the dialog.
It now runs in ManageNoteTypesViewModel, which adds the note type and
reloads the list under one progress dialog, even if the screen is
recreated meanwhile. Errors still go through launchCatchingTask, also on
a recreated screen.

Assisted-by: Claude Opus 5.5
Loading the column picker and the notes for Find and Replace now report
progress from CardBrowserViewModel, so the browser's existing observer
shows the dialog and blocks input while they load.

Assisted-by: Claude Opus 5.5
Previously, `AnkiDroidApp.appResources` was required, even if all
required preferences were overridden.

Now, an in-memory SharedPreferences instance (SPMock) can be used

Assisted-by: GPT-6.1-Sol xhigh
SharedPreferencesLastDeckIdRepository - use SharedPreferencesProvider

Assisted-by: GPT-6.1-Sol xhigh
We previously leaked `BackendFactory.defaultLanguages` changes, which
could apply to other tests

Assisted-by: GPT-6.1-Sol xhigh
Moves 146 tests to the JVM

Assisted-by: GPT-6.1-Sol xhigh
This avoids deck taps becoming long presses on a slow emulator.

Fixes 22912

Assisted-by: GPT-6.1-Sol xhigh
So an exception caught outside an activity coroutine, for example in a
ViewModel, can be shown the same way, with database corruption still
opening the recovery dialog.

Assisted-by: Claude Opus 5.5
The release smoke test can fail when ADB loses the emulator mid-run,
even though the app is fine.

Run it once more, but only if ADB reconnected and AGP's test results
show the test never finished. A test that reported a failure is never
retried, and a run that passes without any test result now fails.

Assisted-by: Claude Opus 5.5
Assisted-by: GPT-6.1-Sol xhigh
Assisted-by: GPT-6.1-Sol xhigh
The deletion test clicked the "Other" deck as soon as the deck tree had
loaded, but the rows are drawn a moment later, so on a slow runner the
click could come before the row existed. It now waits until both decks
are shown.

Assisted-by: Claude Opus 5.5
PostRequestUri parses them, while AnkiServer only receives the requests,
and is about to be replaced.

Assisted-by: Claude Opus 5.5
Adds addWebMessageListener and addDocumentStartJavaScript, for
PostRequestBridge. Callers must check that the WebView supports
WEB_MESSAGE_LISTENER and DOCUMENT_START_SCRIPT.

Assisted-by: Claude Opus 5.5
Android's WebView can intercept requests, but can't read their bodies,
so AnkiServer listens on a localhost port to receive POST requests. Any
app on the device can connect to that port.

PostRequestBridge receives them without a server:
`post-request-bridge.js` replaces fetch() before the page scripts run,
and sends same-origin POST requests as binary web messages, which the
bridge passes to a PostRequestHandler. Only frames from
https://appassets.androidplatform.net get the script and the message
object. That's the host WebViewAssetLoader uses: Google reserves it, so
it doesn't resolve and nobody else can get a certificate for it. Unlike
127.0.0.1, where any app can listen on port 80 since Android 13 with
kernel 5.15.

Requests made with XMLHttpRequest, forms or from web workers aren't
sent: the Anki pages and AnkiDroid's scripts only use fetch().

Claude Opus 5.5 implemented the bridge, the fetch() shim and their tests

Assisted-by: Claude Opus 5.5
PostRequestBridge, which is replacing AnkiServer, needs
WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER: stable since WebView 116.

DeckPicker and the importers show the outdated WebView dialog below it,
or if the WebView lacks the features used by PostRequestBridge, as
non-Google WebViews may.

Assisted-by: Claude Opus 5.5
The importers enabled cancelling an import with Back from
onLoadResource(), which WebView doesn't call for messages of
PostRequestBridge. They now track the import progress from the POST
requests they handle, which works with both AnkiServer and the bridge.

The test is from David Allison's review.

Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
Assisted-by: GPT-6
Assisted-by: Claude Opus 5.5
The Anki pages and the card viewers (the reviewer, the new study screen
and the previewers) send their POST requests through PostRequestBridge,
and AnkiServer is removed: AnkiDroid no longer listens on a localhost
port.

They're loaded from https://appassets.androidplatform.net/, whose
assets the WebViewClients serve.

Card templates may use http:// resources, like scripts, which an https
page blocks by default, so the card viewers use
MIXED_CONTENT_ALWAYS_ALLOW, as AnkiServer's http:// origin allowed them.

The origin no longer changes, so localStorage persists for everyone, and
the 'localStorage in Study Screen' setting has no effect. Data that
templates stored under its fixed port isn't available from the new
origin.

The pages can't send requests without the bridge, so all of them show
the outdated WebView dialog below version 116, instead of only the
importers. The card viewers still load, but their requests fail.

Unlike AnkiServer, a failed request without an error message returns
status 500 with the exception name, instead of 200 with an empty body.

Claude Opus 5.5 implemented this change and verified the Anki pages, the
reviewer and the new study screen on an emulator

Assisted-by: Claude Opus 5.5
AnkiServer was its only user in the app. ReviewerMediaErrorTest returns
its failing responses from its WebViewClient, instead of a NanoHTTPD
server. It fails its API request with an XMLHttpRequest, as same-origin
fetch() POST requests go through PostRequestBridge.

Assisted-by: Claude Opus 5.5
It made the reviewers' server use a fixed port, so templates kept their
localStorage. Without the server, the origin is always
https://appassets.androidplatform.net/ and localStorage persists for
everyone.

A preference upgrade removes its stored values.

Assisted-by: Claude Opus 5.5
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Snapshot diff report vs main. Open screenshot-diff for diffs.

  • CustomStudyScreenshotTest: 2 changes
  • DeckPickerScreenshotTest: 6 changes
  • DeckPickerTabletScreenshotTest: 1 change
  • ExportDialogFragmentScreenshotTest: 1 change
  • NavigationDrawerScreenshotTest: 1 change
  • PreferencesScreenshotTest: 1 change
  • ReviewRemindersScreenshotTest: 5 changes
All 17 changed screenshots

CustomStudyScreenshotTest

  • tablet_unavailable_deck_compare.png
  • unavailable_deck_compare.png

DeckPickerScreenshotTest

  • bottom_navigation_keyboard_open_scrolled_to_bottom_compare.png
  • deck_list_after_dismissing_create_deck_dialog_compare.png
  • hierarchy_lines_collapsed_compare.png
  • hierarchy_lines_compare.png
  • hierarchy_lines_many_siblings_compare.png
  • keyboard_open_scrolled_to_bottom_compare.png

DeckPickerTabletScreenshotTest

  • keyboard_open_compare.png

ExportDialogFragmentScreenshotTest

  • deck_compare.png

NavigationDrawerScreenshotTest

  • navigationDrawer_compare.png

PreferencesScreenshotTest

  • AdvancedSettingsFragment_compare.png

ReviewRemindersScreenshotTest

  • studyOptionsFragmentHost_scheduleReminders_compare.png
  • studyOptionsFragmentHost_systemBars_compare.png
  • studyOptionsFragmentHost_systemBars_scrolledToEnd_compare.png
  • studyOptionsFragmentHost_troubleshooting_compare.png
  • studyOptionsFragmentHost_troubleshooting_systemBars_compare.png

@float3
float3 merged commit 0c41ae1 into main Nov 4, 2026
40 checks passed
@float3
float3 deleted the upstream-2026-11-04 branch November 4, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants