Skip to content

Merge upstream AnkiDroid - #62

Closed
float3 wants to merge 251 commits into
float3:ankiquestfrom
ankidroid:main
Closed

float3 wants to merge 251 commits into
float3:ankiquestfrom
ankidroid:main

Conversation

@float3

@float3 float3 commented Oct 5, 2026

Copy link
Copy Markdown
Owner

22 new commits from ankidroid/Anki-Android main.

Merge with a merge commit, never squash or rebase, so ankiquest keeps upstream's history.

If this conflicts, merge ankidroid/Anki-Android main into a branch from ankiquest, resolve there, and merge that branch instead; this pull request then shows as merged.

david-allison and others added 29 commits October 5, 2026 08:02
* Fix scrolling spacing so bottom rows are reachable
* Wrapped field/template don't overlap vertically
* Truncate note-type names
* Tabs handle large text

Fixes 21008

Assisted-by: GPT-6
The design of 'To' already indicates it's clickable
and we could do with the space to render the note type
name

From a review comment on 21008

Assisted-by: GPT-6
Prepare the Kotlin DSL conversion while preserving git blame.

Break the build to maintain git history across the migrated files

Issue 20910
Part of Issue 20910

Assisted-by: GPT-6
`SyncAuth` was not constructed correctly, dropping ioTimeoutSecs

We now handle this via `toProto().toByteArray()`

Fixes 22293

Assisted-by: GPT-6
* Tags
* Locale Selection

Prep for 7110

Assisted-by: GPT-6
The dialog should not be responsible for the parent. Code was unnecessary.

Part of 7110
Code added in 409455e (2014); ADJUST_RESIZE was set
 so controls were usable when the keyboard was open, but this also applied it to the
 calling window.

Assisted-by: GPT-6
and document it

Part of 7110

Assisted-by: GPT-6
Prep for 7110

Assisted-by: GPT-6
Replaces `SOFT_INPUT_ADJUST_RESIZE` (Android 11+ and later)

On resize, we now fit the IME insets.
On pan (compact screens), we remove the insets and apply `SOFT_INPUT_ADJUST_PAN`

Part of 7110

Assisted-by: GPT-6
List the CardIds and return the current row of the cursor, rather than
pre-fetching all rows.

Reduces memory usage

Part of 20253

Assisted-by: GPT-6
Part of 20253

Assisted-by: GPT-6
Only load when a column is read, so restoring the cursor position
does not fetch an unused row.

Part of 20253

Assisted-by: GPT-6
Defers rendering questions/answers/card names until they're requested.

Part of 20253

Assisted-by: GPT-6
Part of 20253

Assisted-by: GPT-6
When loading a row, check if the card is deleted.

If it is, all columns except the ID will return `null`

Part of 20253

Assisted-by: GPT-6
No longer exists.

Part of 21487

Assisted-by: GPT-6
This takes up a runner, 2.5GB of cache space, and has only
once picked up a runner-specific bug, which was a
fixed bug in the framework.

We're getting more runner contention these days, so only
test it weekly.

Part of 21487

Assisted-by: GPT-6
Frees up 2.5 GB of cache (1/4 of our total)

Part of 21487

Assisted-by: GPT-6
ProcessPhoenix will do the restart. It relaunches the app from a
separate process once this one has been killed, so the relaunch is
never handed to a process that is still dying. It is Apache 2.0 and has
no dependencies of its own.
PhoenixActivity runs in its own :phoenix process, and Android creates
the Application in every process of the app. AnkiDroidApp would start
the backend and the rest of the app there, in a process that only lives
long enough to kill the old one and launch the new one. It now returns
straight away there.

Assisted-by: Claude Opus 5 (multiple iterations over original code)
A profile switch restarts the process, so it has to wait for work that
outlives the collection queue, and keep new work from starting. A
backup keeps writing after the backend releases the collection, and
syncs run in workers.

Each now holds a lock while it runs, for the restart to take. A worker
that finds its lock taken drops its work rather than rescheduling it,
since the work carries the AnkiWeb key of the profile being left.

The backend cannot be asked to wait for a backup from elsewhere: it
hands its running backup to the first caller that asks, and
createBackup asks as soon as the backup starts.

Assisted-by: Claude Opus 5 (multiple iterations over original code)
A profile switch cannot be applied in place: the backend, WebView and
SharedPreferences all cache paths from the profile that was active when
the process started, so the process has to die and come back.

The restart takes the sync lock first, waiting for a sync that is
already running, since that sync queues a media sync before it lets go
of its lock. Sync work is then cancelled, since it carries this
profile's AnkiWeb key, which also stops a running media sync rather
than waiting it out. The media sync and backup locks come next. All
three are kept until ProcessPhoenix kills the process, so nothing can
start in the meantime, and released if the handover fails.

The collection is then closed: the backend holds it open, and killing
the process mid-write risks corrupting it. ensureClosed runs on the
collection queue, so it also waits for an operation that is already
running.

Assisted-by: Claude Opus 5 (multiple iterations over original code)
A switch is followed by a process restart, and SharedPreferences.apply()
writes to disk asynchronously. Android does not flush pending apply()
writes when the process is killed, so the new profile id could be lost
and the app would come back on the old profile.

Commit the switch write. Startup keeps the asynchronous write, since
nothing kills the process after it.
A few Card Browser, Previewer and Custom Study tests only passed
because a leftover test dispatcher made background loads finish
instantly. They now wait for the load, or run under runTest like the
tests around them.

Assisted-by: Claude Opus 5.5
runTest swapped ioDispatcher for a test dispatcher and never put it
back, so later tests in the same fork ran background work on the main
thread. That hid races like 22271 until a test happened to run first.
Reset it in tearDown, next to resetMain().

Assisted-by: Claude Opus 5.5
criticalAY and others added 27 commits October 13, 2026 22:52
The recording screen now goes through the shared recorder and player
instead of AudioRecordingController. The layout is the same, but leaving
the app or recreating the screen no longer loses the recording, Done
works after the reviewer's recorder was used (#19438), and a failed
recording shows an error and can't be added. Discarded recordings are
deleted, the screen only stays on while recording or playing, the long
press buzz isn't recorded, playback takes audio focus, and landscape
hides the waveform.

Assisted-by: Claude Opus 5.5
Alpha and beta builds kept an old changelog, because it was only
rendered for public releases. Render it for every release, and add a
note to test builds saying their changelog comes with the stable
release. The docs repo is now cloned for test releases too.

Fixes 18707

Assisted-by: Claude Opus 5.5
Every observer dismissed the one shared dialog on `Idle`, so with two
observers in an activity, one could close the other's dialog.

Assisted-by: Claude Opus 5 (implementation iteration and tests)
The loading dialog only shows after 600 ms, and until then the screen
still took taps. On the deck list a tap could change the selected deck
while an operation was using it. Block touches straight away, like the
legacy withProgress does.

The legacy withProgress now uses the same count, so neither one
unblocks touches while the other is still running.

Assisted-by: Claude Opus 5.5
The application keeps the manager to route storage, but only privately,
so the switch profiles screen had no way to read the profile registry.
The list was a hardcoded empty flow. It now maps
ProfileManager.getAllProfiles() into the UI model, so the screen shows
the real profiles.

The registry is backed by SharedPreferences, which has no defined
iteration order, so profiles are sorted by name, ignoring case, to keep
the list stable across launches.

If the profile environment failed to load at startup, the screen shows
an error and goes back instead of crashing.

Assisted-by: Claude Opus 5.5
Fixes 22497

Assisted-by: GPT-6-Astra default
Assisted-by: GPT-6-Astra default
`"false"` is truthy in JS

Fixes 22483
Introduced in 44a1977
Related 15576
Related 15569

Assisted-by: GPT-6-Astra default
Ensure we select the correct dialog via a fragment lookup

```
com.ichi2.anki.ForegroundMediaNetworkTest.conflict resolution preserves one-time metered confirmation
java.lang.ClassCastException: class android.app.ProgressDialog cannot be cast to class androidx.appcompat.app.AlertDialog
```

Fixes 22489

Assisted-by: GPT-6-Astra default
The screenshot-diff artifact held every file Roborazzi left in each
diffs folder. Reviewers only use the _compare images, so stage just
those. The comment workflow already reads only them.

Fixes 22297

Assisted-by: Claude Opus 5.5
On a fresh install where Android can't provide a storage location,
the storage permission check throws. Treat that as no access, as the
widget provider and BootService already do.

Assisted-by: Claude Opus 5.5
A widget added before storage permission was granted kept a dead
button, because the broadcast meant to refresh it named a component
that does not exist. Send it to WidgetPermissionReceiver directly.

Assisted-by: Claude Opus 5.5
On a phone in landscape, the "Choose display order" sheet sat in the
middle of the screen like a dialog, because bottom sheets stop at
640dp wide. It now uses the full width on phones, as in portrait.
Tablets keep the narrower sheet.

Fixes 22080

Assisted-by: Claude Opus 5.5
Opening the study screen in a dark theme briefly showed a white card
area, because the WebView had no background until the card loaded.
It now starts with the theme's background, which the card uses too.
The previewers share this code and get the same fix.

Fixes 19657

Assisted-by: Claude Opus 5.5
Use `getAppWidgetManager`

Fixes 22515
Related to 16581

Assisted-by: GPT-6-Astra default
`AppWidgetManager.getInstance` returns null on a Supernote A5 X

Part of 22515
Related to 16581

Assisted-by: GPT-6-Astra default
Deleting unused media, backups or the collection now reports
progress from ManageSpaceViewModel instead of the fragment.

Assisted-by: Claude Opus 5.5
Android clears an app's cache when storage runs low, which could delete
a recording while it was still being made. Recordings now go to
noBackupFilesDir, which isn't cleared or backed up.

Assisted-by: Claude Opus 5.5
The system no longer clears recordings left behind when the app is
killed, so recordings older than 7 days are deleted at startup, like
the multimedia argument files.

Assisted-by: Claude Opus 5.5
@float3

float3 commented Oct 15, 2026

Copy link
Copy Markdown
Owner Author

Merged into ankiquest as 1d19554 (fast-forwarded to 2a9f3aa). GitHub does not close this automatically because the head is ankidroid/Anki-Android:main, a branch in another repository that keeps moving; the commits are in, including 328b42e.

Twenty-nine files conflicted. Eighteen were the ankiquest string files, which upstream's move of every translation into common/android had split across two modules; they all stay where they were. The rest were the Add Note receiver leaving the app manifest for the new :widgets module, androidx folding core-ktx into core, onDecksReloaded, flowOfDeckList gaining a fifth flow, and four test files.

Four things then failed in CI that no conflict could show, because upstream changed APIs this fork calls on lines it never touched: four R.string references to strings that moved to common/android (now through CommonString), the new DirectAppWidgetManagerInstanceUsage lint rule (three call sites in AnkiquestWidget), one more moved string in a test, and the new duplicate-subscription guard in ChangeManager — Ankiquest is an object subscribing without a lifecycle owner, so every Robolectric test subscribed it again and 8052 assertions died on that one bug.

Code quality, emulator tests, the conflict scan and the regression tests are green. Unit tests are red, as they were on ankiquest before this merge and for the same reason: UnsatisfiedLinkError: NativeMethods.openBackend, the rsdroid backend not loading under Robolectric. 243 tests failed before the merge and 216 after, no ankiquest test newly fails, and the 16 that newly fail are upstream's own tests hitting the same backend.

@float3 float3 closed this Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.