Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
361 commits
Select commit Hold shift + click to select a range
dc7a5b1
lint: enforce `STATE_` for saved instance state keys
criticalAY Oct 18, 2026
a0a17c0
refactor(config): delegate Config.browserSortCardsColumn
david-allison Sep 20, 2026
e9a3602
refactor(config): delegate Config.browserSortCardsBackwards
david-allison Sep 20, 2026
03d36af
fix(deck-picker): retain scheduler upgrade prompt
n1snt Oct 20, 2026
24f527d
fix(ci): use an on-disk /tmp
david-allison Oct 21, 2026
337e96c
feat(tags): handle `TagName` name equality
david-allison Oct 20, 2026
9bb1281
fix(tags): workaround backend tag duplication bug
david-allison Oct 16, 2026
3ecfd30
feat(tags): move tag subtrees
david-allison Oct 16, 2026
c9919c6
feat(tags): expose total tag count
david-allison Oct 16, 2026
17a2c14
feat(tags): efficient 'tagExists' operation
david-allison Oct 16, 2026
348753a
test(deck-picker): wait before checking FAB
david-allison Oct 21, 2026
dc5b9df
test(deck-picker): fix flake: 30_decks_tablet_compare.png
david-allison Oct 21, 2026
b918d1a
refactor(widgets): move `DayRolloverAlarm` to `:widgets`
criticalAY Oct 20, 2026
1c15726
refactor(widgets): move `AnkiDroidWidgetSmall` to `:widgets`
criticalAY Oct 20, 2026
85d21d3
refactor(widgets): open widget config screens via a destination
criticalAY Oct 21, 2026
5b381ef
refactor(widgets): move `CardAnalysisWidget` to `:widgets`
criticalAY Oct 21, 2026
ed68310
refactor(widgets): move `DeckPickerWidget` to `:widgets`
criticalAY Oct 21, 2026
c861a03
fix(card-template-editor): save card type changes in one update
criticalAY Oct 19, 2026
ea5cb5d
refactor(card-template-editor): remove `adjustedTemplateChanges`
criticalAY Oct 19, 2026
28f8f26
refactor: prefix fragment argument keys with `ARG_`
criticalAY Oct 18, 2026
28b44ce
lint: enforce `ARG_` for fragment argument keys
criticalAY Oct 18, 2026
c21b8c0
fix(insert-field): handle lifecycle bugs
david-allison Oct 21, 2026
5371107
build(deps): update backend for multiple classloaders
david-allison Oct 20, 2026
978ac20
refactor(tests): remove classloader workarounds
david-allison Oct 20, 2026
e2ed43b
perf(tests): remove unnecessary Android/collection fixtures
david-allison Oct 20, 2026
d937eeb
test: add `@NeedsRobolectric` annotation
david-allison Oct 20, 2026
e232858
fix(tests): `DeckStudyStateTest` - handle day rollover
david-allison Oct 21, 2026
b617424
NF: replace `UnknownBinding` with `Binding?`
Arthur-Milchior May 11, 2025
4c232ff
NF: remove `Binding.keyCode`
Arthur-Milchior May 12, 2025
3aacf26
fix(note-editor): allow toolbar button cursor at extremities
david-allison Oct 21, 2026
ffe1bbd
Restored padding so the FAB no longer hides on scroll or overlaps item
Phoenix1808 Oct 21, 2026
481de67
docs: millisecondsSinceLastSyncAttempt/lastSyncTime
david-allison Oct 3, 2026
d3df04a
fix(sync): rate limits for background sync
david-allison Oct 3, 2026
e3deca0
fix: skip `DO_SYNC` when metered connections are disallowed
david-allison Oct 3, 2026
6987017
fix: continue `DO_SYNC` intent in background
david-allison Oct 3, 2026
28f45e9
fix(sync): ignore duplicate sync attempts
david-allison Oct 3, 2026
9294bf4
refactor(sync): handle sync + response in same `withCol`
david-allison Oct 21, 2026
2257f9f
fix(sync): have worker perform full uploadd
david-allison Oct 21, 2026
6d99e9d
fix(sync): have worker complete automatic downloads
david-allison Oct 21, 2026
ea9bf0f
fix(crash-reporting): reject circular exception causes
david-allison Oct 21, 2026
f6c8681
fix(crash-reporting): detect PII in suppressed exceptions
david-allison Oct 21, 2026
559ab49
fix(crash-reporting): report fatal errors (stripping PII)
david-allison Oct 21, 2026
1349d37
fix(crash-reporting): clear logcat for sanitized reports
david-allison Oct 21, 2026
efe9fb9
test: accessibility scans via `-PaccessibilityScan`
david-allison Oct 19, 2026
9118e44
test(shared-decks): add screenshots for download progress
david-allison Oct 19, 2026
94dddd2
style(shared-decks): outline cards in e-ink mode
david-allison Oct 19, 2026
7d5d694
style(shared-decks): e-ink download progress
david-allison Oct 19, 2026
943da95
feat(theme): define more material colors
david-allison Oct 19, 2026
3c393fa
fix(widgets): link resources without the app
criticalAY Oct 22, 2026
4587027
fix(profiles): keep new profile collections in app storage
criticalAY Oct 8, 2026
f3e0bd3
fix(tablet): make the pane divider easier to grab
criticalAY Oct 16, 2026
d9eed1e
refactor(widgets): move `WidgetRegistry` to `:widgets`
criticalAY Oct 22, 2026
0f8e70b
fix(tags): workaround upstream 'ffi' bug
david-allison Oct 21, 2026
0679e6a
test(deck-picker): wait for the deck list after recreation
criticalAY Oct 22, 2026
b09a344
feat(bottom navigation): Hide the bottom nav bar when scrolling
ShaanNarendran Oct 4, 2026
44a4476
Bumped version to 2.26.0alpha7
mikehardy Oct 23, 2026
8f3d788
refactor(deck-picker): select decks from any FragmentActivity
criticalAY Oct 22, 2026
9fd9558
fix(card-template-editor): correctly map deletion to DB ordinals
david-allison Oct 21, 2026
df3d1ec
fix(card-template-editor): correct card count deletion dialog
david-allison Oct 21, 2026
cfdaaaf
fix(card-template-editor): use correct metadata when previewing delet…
david-allison Oct 21, 2026
bd634a8
fix(card-template-editor): retain state if save fails
david-allison Oct 21, 2026
483c63c
fix(study-options): keep empty bury counts hidden
criticalAY Oct 22, 2026
0ac7f51
fix(ci): use an on-disk /tmp - screenshots
david-allison Oct 23, 2026
746c0ae
fix(tests): await the deck list in `deckPicker {`
david-allison Oct 20, 2026
da8fc0c
fix(tests): de-flake DeckPicker - use current searchView
david-allison Oct 20, 2026
f8b1430
refactor(errors): handle `CharSequence` errors
david-allison Oct 22, 2026
f0fc203
feat(browser): show monospace in search errors
david-allison Oct 22, 2026
794017a
refactor: prefix preference key constants with `PREF_`
criticalAY Oct 18, 2026
8cece57
lint: enforce `PREF_` for preference key constants
criticalAY Oct 18, 2026
16e896c
refactor(gradle): extract JaCoCo implementations
david-allison Oct 22, 2026
b74119e
refactor(gradle): deduplicate JaCoCo report outputs
david-allison Oct 22, 2026
25b0a81
fix(coroutines): skip tasks when fragment is destroyed
david-allison Oct 23, 2026
19a24b5
fix(coverage): include `src/main/kotlin`
david-allison Oct 20, 2026
140a37d
fix(coverage): dynamically discover modules
david-allison Oct 20, 2026
450991c
test(coverage): coverage for module/test discovery
david-allison Oct 20, 2026
3e963da
test(coverage): ensure untested libraries are uncovered
david-allison Oct 20, 2026
2191acf
build(ci): increase unit test timeout to 60 minutes
david-allison Oct 23, 2026
ce39696
fix(tests): reset AdaptionUtil web browser cache in Robolectric
mikehardy Oct 23, 2026
b53e575
fix(tests): add SharedPreferencesTestHook for isolated default prefs
mikehardy Oct 23, 2026
8450fc5
fix(tests): isolate gesture prefs in ReviewerNoParamTest drawer tests
mikehardy Oct 23, 2026
7590255
fix(media-check): collect using the view lifecycle
david-allison Oct 23, 2026
807dc4b
docs: identify 75 more Material icons
david-allison Oct 23, 2026
678187f
docs: attribute modified troubleshooting icons
david-allison Oct 23, 2026
421be9c
build(widgets): set up unit test dependencies
david-allison Oct 23, 2026
d82f82a
test: move WidgetAlarmTest to widgets
david-allison Oct 23, 2026
58b84a6
test: use AndroidTest for `AnalyticalWidgetProviderTest`
david-allison Oct 23, 2026
1f2a688
test: move `AnalyticalWidgetProviderTest` to widgets
david-allison Oct 23, 2026
4fe5153
refactor(ui): add dialog preview helper
david-allison Oct 18, 2026
5617926
fix(dev): apply app theme to `TextInputDialogPreview`
david-allison Oct 18, 2026
2f9eea4
fix(dev): give dialog previews transparent backgrounds
david-allison Oct 18, 2026
bd5fe2e
refactor(note-editor): remove obsolete code
david-allison Sep 20, 2026
6b86725
fix(note-editor): preserve restored deck
david-allison Sep 20, 2026
10832ba
refactor: move `dp` to `:common:android`
criticalAY Oct 22, 2026
af61613
refactor: move `dp` to `com.ichi2.anki.common.utils`
criticalAY Oct 23, 2026
49b922c
refactor: move snackbar helpers to `:common:android`
criticalAY Oct 22, 2026
8de080a
fix(controls): preserve '+' bindings
david-allison Oct 23, 2026
a6b4654
refactor: separate binding lookup and warning ui
Arthur-Milchior May 12, 2025
ab7d214
perf(lint): disable `UElementAsPsi` for non-lint modules
david-allison Oct 12, 2026
8563f29
refactor(coverage): capture/expose library variants
david-allison Oct 23, 2026
a2b809f
refactor(gradle): coverage implementation
david-allison Oct 22, 2026
5a951b5
fix(gradle): coverage runs on debug variants
david-allison Oct 22, 2026
9e67946
fix(gradle): use JVM source sets for coverage
david-allison Oct 22, 2026
a91158b
fix(gradle): use Android artifacts for coverage
david-allison Oct 22, 2026
8f058df
fix(gradle): restrict JaCoCo data to test task outputs
david-allison Oct 22, 2026
fa7aee2
feat(backend): expose active locales
david-allison Oct 13, 2026
a00ec1f
feat(debug-info): Add locales to 'copy debug info'
Tanay3183 Oct 10, 2026
1fa324c
fix(study-screen): allow multi-finger gestures on clickable card content
Ratnesh5 Oct 14, 2026
fafd484
fix(custom-study): keep chosen tags after the screen is recreated
criticalAY Oct 20, 2026
5a8ff91
fix(congrats): stop Custom Study crashing after the screen is recreated
criticalAY Oct 20, 2026
af39491
fix(custom-study): keep sub-dialogs after process death
criticalAY Oct 22, 2026
9d0f913
test(congrats): shorten the custom study process death test
criticalAY Oct 23, 2026
4b3fec2
fix(card-browser): save its own editor pane width
criticalAY Oct 17, 2026
ae024d4
feat(webview): add Activity support to SafeWebViewLayout
Ratnesh5 Oct 9, 2026
7aa9e51
fix(webview): migrate Info to use SafeWebViewLayout
Ratnesh5 Oct 9, 2026
624e01d
refactor: move dialog helper utilities to `:common:android`
criticalAY Oct 24, 2026
b1a2b44
refactor: move AlertDialog helpers to `:common:android`
criticalAY Oct 24, 2026
212cf6a
refactor: move `DiscardChangesDialog` to `:anki-common`
criticalAY Oct 24, 2026
3a8ca05
fix(lint): check string-array in InvalidStringFormat
SunilKumarPradhan Oct 20, 2026
2339fc4
fix(studyoptions): extend decorative shadow edge-to-edge
Ratnesh5 Oct 23, 2026
17e2772
fix(studyoptions): mirror decorative shadow gradient in RTL
Ratnesh5 Oct 23, 2026
d278b68
fix(deckpicker): handle insets for split view in RTL
Ratnesh5 Oct 23, 2026
e492a44
test(deckpicker): add RTL tablet screenshot test
Ratnesh5 Oct 23, 2026
1722322
fix(custom-study): keep the typed value when another dialog closes
criticalAY Oct 23, 2026
d50598a
refactor(startup): stop depending on IntentHandler in StartupGuards
criticalAY Oct 24, 2026
dfcc53c
refactor(startup): move StartupGuards to `:anki-common`
criticalAY Oct 24, 2026
09f9fa9
test(api): cover parallel provider URIs & constants
david-allison Sep 28, 2026
f1ea354
feat(api): add helpers for parallel build URIs
iamllama Oct 28, 2024
a19654a
fix(api): supports parallel builds
iamllama Oct 26, 2024
8967a95
feat(api): add `Card.getContentUri(authority)`
david-allison Sep 28, 2026
13d929a
refactor: prefix intent actions with `ACTION_`
david-allison Oct 23, 2026
42d251b
lint: enforce `ACTION_` for intent actions
david-allison Oct 23, 2026
236f2af
refactor(settings): extract theme prefs/contract to `:common:android`
david-allison Oct 23, 2026
c2478fa
refactor(theme): move `theme_attrs.xml` to `:common:android`
david-allison Oct 23, 2026
2f8ad4e
refactor(theme): move deck/browser theme attrs to `:common:android`
david-allison Oct 23, 2026
d141060
refactor(theme): move note editor theme attrs to `:common:android`
david-allison Oct 23, 2026
00ec5c9
refactor(theme): move common UI attrs to `:common:android`
david-allison Oct 23, 2026
0942319
fix(browser): handle search feedback when stopped
david-allison Oct 16, 2026
8709f8c
fix(browser): display counts after search actions
david-allison Oct 16, 2026
c6e96d8
fix(browser): display counts after cards/notes mode changes
david-allison Oct 16, 2026
5ae40ba
fix(browser): display counts after note/card edits
david-allison Oct 16, 2026
b39026f
fix(browser): display counts after note editor changes
david-allison Oct 16, 2026
72bc071
Updated strings from Crowdin
Oct 24, 2026
0d48828
refactor(note-editor): add noteTypeId param to `setNote`
david-allison Oct 23, 2026
1e518c3
fix(note-editor): when copying, copy NoteTypeId
Arthur-Milchior Sep 18, 2026
2b76dbe
refactor(note-editor): change source of draft field values
david-allison Oct 23, 2026
a0fabe5
fix(note-editor): retain draft fields when changing note type
david-allison Oct 23, 2026
e0be06a
fix(note-editor): restore hidden draft fields after editor recreation
david-allison Oct 23, 2026
5401608
feat: show dialog if no TTS engine is installed
david-allison Oct 2, 2026
5a9ca32
test(compose): add themed screenshot test base
david-allison Oct 18, 2026
eb82f8f
fix(lint): ignore translated strings in `:common:android`
david-allison Oct 18, 2026
cbf8b00
fix(dev): make dialog preview backgrounds transparent
david-allison Oct 18, 2026
222d357
feat(note-editor): add toolbar layout chooser
david-allison Oct 16, 2026
504bd36
feat(note-editor): use new toolbar layout chooser
david-allison Oct 16, 2026
be8f427
fix(note-editor): toolbar layout - show focus in e-ink mode
david-allison Oct 22, 2026
de688fb
feat(profiles): add a profile from the switch profile screen
criticalAY Oct 14, 2026
bc02ca2
feat(profiles): rename a profile
criticalAY Oct 14, 2026
b338399
fix(profiles): don't log a new profile's name
criticalAY Oct 16, 2026
7cc342b
docs(profiles): note the name check is stricter than Desktop
criticalAY Oct 16, 2026
684ba05
build(deps): bump undici from 6.29.0 to 6.29.1 in /tools/localization
dependabot[bot] Oct 24, 2026
77c148d
fix(shared-decks): offer log in and sign up when AnkiWeb asks for a l…
criticalAY Oct 5, 2026
693b477
test(shared-decks): screenshot the AnkiWeb login dialog
criticalAY Oct 14, 2026
74ff3c5
fix(reviewer): fix delayed answer feedback animation
david-allison Oct 24, 2026
2865630
refactor(import): extract `openCsvImporter`
david-allison Oct 20, 2026
b8dd0af
fix(import): expose text file copy failures
david-allison Oct 20, 2026
4c74cba
refactor(import): return source & path on success
david-allison Oct 20, 2026
3dd0dad
refactor(import): extract cacheImport
david-allison Oct 20, 2026
3fd8881
refactor(import): expose `prepareImport`
david-allison Oct 20, 2026
9801213
refactor(import): UI methods require `Activity`
david-allison Oct 21, 2026
6b42561
refactor(import): extract openImport
david-allison Oct 21, 2026
a428146
refactor(import): only resolve file once
david-allison Oct 21, 2026
5e9ca41
feat(preferences): add Compose timebox settings subscreen with Materi…
Ratnesh5 Oct 12, 2026
005ed67
feat(preferences): use text field for timebox limit
Ratnesh5 Oct 23, 2026
7ca100e
feat(preferences): inline unit in timebox stepper
Ratnesh5 Oct 23, 2026
4aacb63
fix(preferences): stepper delta, align, and insets
Ratnesh5 Oct 23, 2026
622282f
fix(preferences): loading state for timebox screen
Ratnesh5 Oct 23, 2026
ede1dfe
fix(preferences): reorder modifier in screen
Ratnesh5 Oct 24, 2026
7f55aa5
fix(preferences): address timebox screen review feedback
Ratnesh5 Oct 24, 2026
c1fe1ec
fix(preferences): use plurals for timebox suffix and enlarge stepper …
Ratnesh5 Oct 24, 2026
c628a27
fix(preferences): keep uniform card container color and avoid nav bar…
Ratnesh5 Oct 24, 2026
73ce387
fix(browser): stop UI work when view is destroyed
david-allison Oct 24, 2026
afd924a
fix(filtered-decks): use the view lifecycle to collect flows
david-allison Oct 24, 2026
f2686e8
fix(filtered-decks): bind input listeners after view restoration
david-allison Oct 24, 2026
4884abb
fix(reminders): stop UI work the view is destroyed
david-allison Oct 24, 2026
32e7fe0
fix(reminders): stop work when fragment is removed
david-allison Oct 24, 2026
90431bc
refactor(theme): move theme palette colors to `:common:android`
david-allison Oct 24, 2026
e83a292
refactor(theme): move text color selectors to `:common:android`
david-allison Oct 24, 2026
947691d
refactor(theme): move reviewer footer drawables to `:common:android`
david-allison Oct 24, 2026
95f76b0
refactor(theme): move navigation theme assets to `:common:android`
david-allison Oct 24, 2026
6dc4c02
refactor(theme): move ui control styles to `:common:android`
david-allison Oct 24, 2026
42f656f
refactor(collection): add `withClosedCollection` utility
david-allison Oct 24, 2026
ffe2f81
refactor(permissions): extract `configurePermissions`
david-allison Oct 24, 2026
6e03892
refactor(permissions): extract `showPermissionsFragment`
david-allison Oct 24, 2026
ae312f5
test(permissions): grant permissions for 'Continue' case
david-allison Oct 24, 2026
b0e89c8
test(permissions): extract `PermissionsActivityScreenshotTest`
david-allison Oct 24, 2026
98c50ad
refactor(custom-study): inflate menu using XML
david-allison Oct 22, 2026
85f0635
fix(custom-study): add loading state
david-allison Oct 22, 2026
219dfaf
fix(custom-study): asynchronously load menu
david-allison Oct 22, 2026
0f0b274
fix(custom-study): handle empty and hidden decks
david-allison Oct 22, 2026
9c96397
fix(custom-study): handle stale menu selections
david-allison Oct 22, 2026
bb30a38
fix(custom-study): cancel initialization on dialog close
david-allison Oct 23, 2026
5fe4950
fix(custom-study): ignore concurrent menu selections
david-allison Oct 23, 2026
5672808
feat(multimedia): external image editing helpers
david-allison Oct 11, 2026
fe2acfc
feat(multimedia): connect external editors to image actions
david-allison Oct 11, 2026
96fed4a
fix(multimedia): add MediaStore.EXTRA_OUTPUT for Google Photos
david-allison Oct 11, 2026
b89938f
test: coverage for RepositionCardTemplateDialog
david-allison Oct 23, 2026
9693d19
refactor: leaner card template ordinal tracking
david-allison Oct 23, 2026
a7121c1
feat(card-templates): reposition card types
david-allison Oct 23, 2026
d152169
pref(test): use preview translations instead of mocks
david-allison Oct 25, 2026
650d143
Improve extend daily limits dialogs
Roniscend Sep 11, 2026
4e438a2
fix(lint): ignore translated strings in `:common:android`
david-allison Oct 18, 2026
7aa1f85
refactor: use 'Deck' accessors
Arthur-Milchior Jan 8, 2025
6931ba2
refactor: make 'Deck' wrap its JSONObject
Arthur-Milchior Jan 8, 2025
6112081
refactor: delegate & document Deck properties
Arthur-Milchior Jan 8, 2025
5fc65ab
refactor: strongly type fields of 'Deck'
Arthur-Milchior Jan 8, 2025
a15eba6
refactor: extract JSONObject.getLongOrNull to `:common`
david-allison Oct 6, 2026
67491d9
refactor: add filtered deck search terms accessor
Arthur-Milchior Jan 8, 2025
e677ff8
docs: link AI_POLICY in welcome bot comments
david-allison Oct 24, 2026
46f29bd
docs: relax GitHub AI disclosure formatting
david-allison Oct 24, 2026
e1180ba
docs: allow AI-assisted code review comments
david-allison Oct 24, 2026
4ef05f5
docs: simplify AI tool disclosure in commit messages
david-allison Oct 24, 2026
52eae6c
docs: allow AI-produced issues at maintainer discretion
david-allison Oct 24, 2026
3256094
Improve screenshot test output folder format
Phoenix1808 Oct 23, 2026
86e24bd
Updated strings from Crowdin
Oct 25, 2026
fe65770
Bumped version to 2.26.0alpha8
mikehardy Oct 25, 2026
0efacb2
refactor(errors): open help destinations through the navigator
criticalAY Oct 25, 2026
f41b7b8
design(preferences): reduce 'blue' in timebox settings
david-allison Oct 25, 2026
50d6999
design(preferences): improve timebox contrast on e-ink
david-allison Oct 25, 2026
44505a9
fix(theme): give Black theme bottom sheets the dialog colour
criticalAY Oct 25, 2026
c22d356
refactor: move deck selection helpers out of :AnkiDroid
criticalAY Oct 25, 2026
2b8f6af
refactor(startup): prep for making `MANAGE_EXTERNAL_STORAGE` skippable
david-allison Oct 25, 2026
0fb9f43
refactor(startup): rename 'ensureCollectionPathSet'
david-allison Oct 25, 2026
a90aa4c
feat(startup): defer the storage decision if public storage is inacce…
david-allison Aug 16, 2026
50cc545
docs: source `ic_sort_pill_arrow_down_label`
david-allison Oct 24, 2026
75e3354
docs: source `ic_bar_chart_black`
david-allison Oct 24, 2026
8357a6c
docs: source `ic_flashcard_black`
david-allison Oct 24, 2026
802b124
docs: source `ic_view_list_white_24dp`
david-allison Oct 24, 2026
705220d
docs: source `ic_add_equation_black_24dp`
david-allison Oct 24, 2026
6b2dfdc
fix(ci): add swap to Linux unit test runners
criticalAY Oct 26, 2026
841586c
refactor: explicit backing fields in AddEditReminderDialogViewModel
ericli3690 Oct 2, 2026
6efd8a6
fix(reminders): migrate should strictly point to next schema
ericli3690 Oct 3, 2026
e025876
fix(reminders): keep id upon migration
ericli3690 Oct 3, 2026
2fc33b4
fix(reminders): do not persist cachedDeckName
ericli3690 Oct 4, 2026
dc81333
feat(reminders): threshold filters schema migration
ericli3690 Oct 4, 2026
22b6779
feat(reminders): threshold filters UI
ericli3690 Oct 5, 2026
0860097
feat(reminders): apply threshold filters at notification time
ericli3690 Oct 5, 2026
d00f4f2
docs(reminders): add threshold filter usage examples
ericli3690 Oct 6, 2026
0879c5d
docs: source `ic_horizontal_rule_black_24dp`
david-allison Oct 26, 2026
e121467
docs: source `calendar_date_range`
david-allison Oct 26, 2026
25bb233
docs: source `calendar_single_day`
david-allison Oct 26, 2026
3b6d671
docs: source `ic_restore_variant`
david-allison Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
32 changes: 21 additions & 11 deletions .github/check_alpha_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
const fs = require("node:fs");

module.exports = async function checkAlphaRelease({ github, context, core }) {
async function skipRelease(reason) {
core.info(`Skipping scheduled release: ${reason}`);
await github.rest.actions.cancelWorkflowRun({
...context.repo,
run_id: context.runId,
});
// Cancellation is asynchronous. Keep this step alive so cancelWorkflowRun can kill it.
await keepProcessAlive();
}

const metadata = JSON.parse(
fs.readFileSync("AnkiDroid/build/outputs/release-metadata.json", "utf8"),
);
if (metadata.releaseType !== "alpha") {
core.info("Skipping scheduled release: main is not on an alpha version.");
return false;
return await skipRelease("main is not on an alpha version.");
}

const releases = await github.paginate(github.rest.repos.listReleases, {
Expand All @@ -22,33 +31,34 @@ module.exports = async function checkAlphaRelease({ github, context, core }) {
release => !release.draft && today - Math.floor(Date.parse(release.published_at) / day) < 7,
);
if (recentRelease) {
core.info(
`Skipping scheduled release: ${recentRelease.tag_name} was published fewer than seven UTC calendar days ago.`,
return await skipRelease(
`${recentRelease.tag_name} was published fewer than seven UTC calendar days ago.`,
);
return false;
}

const latestAlpha = releases
.filter(release => !release.draft && /^v\d+\.\d+\.\d+alpha\d+$/.test(release.tag_name))
.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
if (!latestAlpha) {
core.info("Skipping scheduled release: no published alpha to compare against.");
return false;
return await skipRelease("no published alpha to compare against.");
}

const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
...context.repo,
basehead: `${latestAlpha.tag_name}...${context.sha}`,
});
if (comparison.status !== "ahead" || comparison.ahead_by === 0) {
core.info(
`Skipping scheduled release: no new commits descending from ${latestAlpha.tag_name}.`,
);
return false;
return await skipRelease(`no new commits descending from ${latestAlpha.tag_name}.`);
}

core.info(
`Publishing an alpha: ${comparison.ahead_by} new commits since ${latestAlpha.tag_name}.`,
);
return true;
};

async function keepProcessAlive() {
// Fail after five minutes if cancellation never arrives.
await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
throw new Error("Timed out waiting for workflow cancellation.");
}
2 changes: 2 additions & 0 deletions .github/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Hello! 👋 Thanks for logging this issue. Please remember we are all volunteers here, so some patience may be required before we can get to the issue. Also remember that the fastest way to get resolution on an issue is to propose a change directly, https://github.com/ankidroid/Anki-Android/wiki/Contributing
Please read our [AI Policy](https://github.com/ankidroid/Anki-Android/blob/main/AI_POLICY.md) before contributing.

# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome

# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
First PR! 🚀 We sincerely appreciate that you have taken the time to propose a change to AnkiDroid! Please have patience with us as we are all volunteers - we will get to this as soon as possible.
Please ensure your contribution meets our [AI Policy](https://github.com/ankidroid/Anki-Android/blob/main/AI_POLICY.md) before contributing.

# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge

Expand Down
97 changes: 89 additions & 8 deletions .github/report_test_failures.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later

"""Report failures and skipped tests from JUnit XML using the standard library."""
"""Report failures, skipped tests, and overview summary from JUnit XML using the standard library."""

import argparse
import html
Expand All @@ -12,6 +12,19 @@
import xml.etree.ElementTree as ET


TABLE_HEADER = (
"## Test Results\n\n"
"| Suite | Result | Duration | Tests | Passed | Failed | Skipped |\n"
"| ----- | ------ | -------- | ----- | ------ | ------ | ------- |\n"
)


def format_duration(seconds):
minutes = int(seconds // 60)
rem_seconds = seconds % 60
return f"{minutes:02d}m {rem_seconds:06.3f}s"


def read_ignored_tests(log_file):
ignored = set()
failed = set()
Expand All @@ -26,22 +39,61 @@ def read_ignored_tests(log_file):
return ignored - failed


def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"):
def get_report_duration(root):
suites = root.findall("testsuite")
if suites:
total = 0.0
for s in suites:
try:
total += float(s.get("time", 0) or 0)
except (ValueError, TypeError):
pass
if total > 0.0:
return total
try:
root_time = float(root.get("time", 0) or 0)
if root_time > 0.0:
return root_time
except (ValueError, TypeError):
pass
total = 0.0
for tc in root.iter("testcase"):
try:
total += float(tc.get("time", 0) or 0)
except (ValueError, TypeError):
pass
return total


def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures", suite=None):
failures = []
skipped = []
ignored = read_ignored_tests(log_file)
total_duration = 0.0
passed_count = 0
failed_count = 0
skipped_count = 0

for report in sorted(results_dir.rglob(pattern)):
try:
root = ET.parse(report).getroot()
except (ET.ParseError, OSError) as error:
failures.append(f"Could not read {report}: {error}")
if os.environ.get("GITHUB_ACTIONS") == "true":
print(f"::warning::Could not read {report}: {error}")
else:
print(f"Warning: Could not read {report}: {error}", file=sys.stderr)
continue

total_duration += get_report_duration(root)

for test in root.iter("testcase"):
name = f"{test.get('classname', '')}.{test.get('name', '')}"
has_failure = False
has_skipped = False

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.
Expand All @@ -56,10 +108,30 @@ def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"
"org.junit.AssumptionViolatedException",
"org.junit.internal.AssumptionViolatedException",
):
has_skipped = True
skipped.append(f"{name}\n{details or result.get('type', 'Test skipped')}")
else:
has_failure = True
failures.append(f"{name}\n{details or result.get('type', 'Test failed')}")

if has_failure:
failed_count += 1
elif has_skipped:
skipped_count += 1
else:
passed_count += 1

total_tests = passed_count + failed_count + skipped_count
is_successful = failed_count == 0 and total_tests > 0
result_text = "SUCCESS" if is_successful else "FAILURE"
elapsed = format_duration(total_duration)

if suite:
print(
f"{suite}: {result_text} ({elapsed}) - "
f"{total_tests} tests: {passed_count} passed, {failed_count} failed, {skipped_count} skipped"
)

for failure in failures:
if os.environ.get("GITHUB_ACTIONS") == "true":
# GitHub renders the escaped message as a multiline error in the log.
Expand All @@ -79,19 +151,27 @@ def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"
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 os.environ.get("GITHUB_STEP_SUMMARY"):
with open(os.environ["GITHUB_STEP_SUMMARY"], "a+", encoding="utf-8") as summary:
summary.seek(0)
existing_content = summary.read()
if suite:
if "## Test Results" not in existing_content:
summary.write(TABLE_HEADER)
summary.write(f"| {suite} | **{result_text}** | {elapsed} | {total_tests} | {passed_count} | {failed_count} | {skipped_count} |\n\n")

if failures:
summary.write(f"### {title}\n\n")
for failure in failures:
summary.write(f"<pre>{html.escape(failure)}</pre>\n\n")
if skipped:
skipped_title = f"Skipped tests ({len(skipped)})"
summary.write(f"<details><summary>{skipped_title}</summary>\n\n")
for skip in skipped:
summary.write(f"<pre>{html.escape(skip)}</pre>\n\n")
summary.write("</details>\n\n")

return 1 if failures else 0
return 1 if (failures or (suite and not is_successful)) else 0


if __name__ == "__main__":
Expand All @@ -100,5 +180,6 @@ def main(results_dir, log_file=None, pattern="TEST-*.xml", title="Test failures"
parser.add_argument("log_file", type=Path, nargs="?")
parser.add_argument("--pattern", default="TEST-*.xml", help="Recursive report glob relative to results_dir")
parser.add_argument("--title", default="Test failures", help="Failure heading in the job summary")
parser.add_argument("--suite", help="Suite name for markdown summary table")
args = parser.parse_args()
sys.exit(main(args.results_dir, args.log_file, args.pattern, args.title))
sys.exit(main(args.results_dir, args.log_file, args.pattern, args.title, args.suite))
27 changes: 27 additions & 0 deletions .github/with_emulator_cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-or-later

set -euo pipefail

# Only for dedicated Linux CI runners with one emulator and -no-snapshot-save.
# Forced shutdown must not interrupt saving a snapshot for subsequent jobs.
stop_emulator() {
# #22613 `emu kill` can report success while QEMU keeps running and
# holds the emulator action's output pipes open. Bound both ADB and QEMU.
timeout -s KILL 5s "$ANDROID_HOME/platform-tools/adb" -s "emulator-$EMULATOR_PORT" emu kill || true

local emulator_pattern="^$ANDROID_HOME/emulator/qemu/"
local deadline=$((SECONDS + 20))
while pgrep -f "$emulator_pattern" > /dev/null; do
if ((SECONDS >= deadline)); then
echo "Emulator did not stop within 20 seconds; killing QEMU." >&2
pkill -KILL -f "$emulator_pattern" || true
break
fi
sleep 1
done
}

# Preserve the exit status
trap stop_emulator EXIT
"$@"
6 changes: 6 additions & 0 deletions .github/workflows/label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
let removeLabelsList = [
"Pending Merge",
Expand Down Expand Up @@ -42,6 +43,7 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
request: { retries: 0 },
body: `Maintainers: Please [Sync Translations](https://github.com/ankidroid/Anki-Android/actions/workflows/sync_translations.yml) to produce a commit with only the automated changes from this PR.

Read more about updating strings on the wiki,
Expand Down Expand Up @@ -80,6 +82,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
const I18N_FILES = [
"01-core",
Expand Down Expand Up @@ -133,6 +136,7 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
request: { retries: 0 },
body: `> [!IMPORTANT]
> **Maintainers**: This PR contains https://github.com/ankidroid/Anki-Android/labels/Strings changes

Expand Down Expand Up @@ -207,6 +211,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
const creator = context.payload.sender.login
const opts = github.rest.issues.listForRepo.endpoint.merge({
Expand Down Expand Up @@ -240,6 +245,7 @@ jobs:
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
retries: 3
script: |
await github.rest.issues.addLabels({
issue_number: context.issue.number,
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/merge_requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ on:

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Don't cancel runs: this is a fast check which shows as a failing check when canceled.

jobs:
squashMerge:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ concurrency:
jobs:
check_release:
name: 'Check whether a release is due'
permissions:
contents: read
actions: write # Cancel this run when a scheduled publish is skipped.
if: github.event_name == 'schedule' && github.repository == 'ankidroid/Anki-Android' && github.ref == 'refs/heads/main'
timeout-minutes: 20
runs-on: ubuntu-latest
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/screenshot_comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
name: Check if there are valid files
shell: bash
run: |
# Find Roborazzi diff PNGs in the downloaded artifact (in <TestClass>/).
# Find Roborazzi diff PNGs in the downloaded artifact (in <module>/<TestClass>/).
mapfile -t files_to_add < <(find . -name "*_compare.png" -type f)
if [ ${#files_to_add[@]} -gt 0 ]; then
exist_valid_files="true"
Expand All @@ -82,13 +82,14 @@ jobs:
BASE_REF: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.pull_requests[0].base.ref || github.event.repository.default_branch }}
shell: bash
run: |
# Find Roborazzi diff PNGs to mention in the comment (in <TestClass>/).
# Find Roborazzi diff PNGs to mention in the comment (in <module>/<TestClass>/).
mapfile -t files < <(find . -name "*_compare.png" -type f | sort)
total=${#files[@]}
# Cap the full list so we remain within GitHub's comment limit
max_details=100

# Group by test class - the file's immediate parent directory.
# Group by test class, module prefix is dropped for readability.
# Reconsider it once we are heavily mutlimodule.
declare -A class_counts class_files
for file in "${files[@]}"; do
class_dir="${file%/*}"
Expand Down
Loading
Loading