Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3f7a13d
fix(recorder): keep recordings out of the cache
criticalAY Oct 12, 2026
2ff269e
fix(recorder): clean up old recordings at startup
criticalAY Oct 12, 2026
675ee6d
refactor(card-browser): deduplicate card state icons
david-allison Oct 15, 2026
93078f3
test: support disabling backend loading
david-allison Oct 14, 2026
4b0be9c
test: Windows support for FileProvider host paths
david-allison Oct 12, 2026
e363704
refactor(note-editor): extract `launchEditor`
david-allison Oct 13, 2026
90ca322
fix(note-editor): stop flicker on cancelling 'Gallery'
david-allison Oct 13, 2026
77052f0
test(reviewer): wait for autofocus before changing fields
david-allison Oct 13, 2026
4f0c574
feat(study-screen): support HTML `nosuggest`
david-allison Oct 2, 2026
99b6f9b
fix(editor): use backend filenames when pasting media
david-allison Oct 15, 2026
84f4ad1
perf(deck-picker): release the background image when stopped
criticalAY Oct 14, 2026
2d10b79
refactor: use REQUEST_ for Fragment Result API request keys
david-allison Oct 14, 2026
3241819
lint: enforce REQUEST_ for Fragment Result API request keys
david-allison Oct 14, 2026
4338790
build(ci): store screenshot baselines for dependency-updates
criticalAY Oct 14, 2026
e1adc09
build(release): add the release to the publish run summary
criticalAY Oct 15, 2026
e59c39e
test(profiles): fix flaky SwitchProfilesFragmentTest
criticalAY Oct 15, 2026
57eb96f
fix(mediacheck): handle onRenderProcessGone with SafeWebViewLayout
Ratnesh5 Oct 15, 2026
4448506
fix(card-viewer): allow HTTP/HTTPS card links
david-allison Oct 13, 2026
78fa213
fix(card-viewer): allow deep links if a browser isn't installed
david-allison Oct 13, 2026
7548b99
fix(card-viewer): suppress failed card link launches
david-allison Oct 13, 2026
b399981
fix(card-viewer): avoid catching `Error` during link launches
david-allison Oct 13, 2026
8590180
fix(reviewer): handle rejected external link launches
david-allison Oct 15, 2026
0a117f7
build(buildSrc): add Compose convention plugin
david-allison Oct 15, 2026
2e764b0
build: add `:note-editor` module
david-allison Oct 14, 2026
8c1e4a0
refactor(note-editor): extract toolbar icons to modules
david-allison Oct 15, 2026
233134f
refactor(note-editor): extract ToolbarPreferences
david-allison Oct 15, 2026
f151fb9
feat(compose): support previews for all app themes
david-allison Oct 15, 2026
1b25591
refactor: use `RESULT_` for fragment result key names
david-allison Oct 15, 2026
e07cf6f
lint: enforce `RESULT_` for fragment result key names
david-allison Oct 15, 2026
d64360c
refactor(test): convert java to kotlin
david-allison Oct 15, 2026
507e758
refactor(sync): move fullUploadOrDownload
david-allison Oct 15, 2026
a60a9c9
refactor(sync): document `FullSync:MediaSync`
david-allison Oct 15, 2026
7b2baa8
fix(import): do not close import dialog on startup [tablet]
david-allison Oct 15, 2026
25cf98f
fix(card-template-editor): count the cards a new card type creates
criticalAY Oct 13, 2026
8860d8a
test(reminders): stabilize code coverage
david-allison Oct 15, 2026
205f71c
test(reviewer): stabilize code coverage
david-allison Oct 15, 2026
3784723
fix(one-way-sync): apply metered connection restrictions for media
david-allison Oct 15, 2026
5d18a0b
fix(ci): mark a skipped weekly publish as 'cancelled'
david-allison Oct 12, 2026
5c078fa
fix(release): push the version bump before uploading to Play
criticalAY Oct 15, 2026
7c1f39b
Bumped version to 2.26.0alpha5
mikehardy Oct 15, 2026
b3d74d3
refactor: prefix intent extra keys with `EXTRA_`
criticalAY Oct 15, 2026
7e29d24
lint: enforce `EXTRA_` for intent extra keys
criticalAY Oct 15, 2026
6f03d60
fix(settings): reword web links note
uditpandey727 Oct 16, 2026
ea1a3db
fix(release): stop if the release tag is missing
criticalAY Oct 16, 2026
b742597
ci(emulator): pin the build time for the whole job
criticalAY Oct 15, 2026
7b51627
ci(emulator): run the release smoke test in its own steps
criticalAY Oct 15, 2026
cd90578
ci(emulator): add a tablet to scheduled runs
criticalAY Oct 15, 2026
e331d34
test(study-options): fix flaky buried count test
criticalAY Oct 16, 2026
311111b
fix(ci): retry GitHub API calls in the label workflow
criticalAY Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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.");
}
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
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
1 change: 1 addition & 0 deletions .github/workflows/screenshot_compare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ jobs:
name: screenshot
workflow: screenshot_store.yml
branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.event.repository.default_branch }}
event: push

- name: Compare screenshots
id: compare-screenshot-test
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/screenshot_store.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ on:
push:
branches:
- main
- dependency-updates
# On PRs, only run this when structural changes occur, to be sure 'main' can still store baselines
pull_request:
paths:
Expand All @@ -41,7 +42,7 @@ permissions: {}
jobs:
store-screenshot-test:
# On PRs, this verifies the workflow runs on `main`, it's not a store
name: ${{ github.ref == 'refs/heads/main' && 'store' || 'test' }}
name: ${{ github.event_name == 'push' && 'store' || 'test' }}
runs-on: ubuntu-latest
timeout-minutes: 20

Expand Down
64 changes: 51 additions & 13 deletions .github/workflows/tests_emulator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,23 @@ jobs:
// try different architectures, targets, delays etc
let arch = ['x86_64'];
let target = ['google_apis', 'google_apis_ps16k'];
// Keep API 37 and the 16 KB emulator off the per-change CI path.
// Scheduled and manual runs exercise both API levels and targets.
let include = [];
// Keep API 37, the 16 KB emulator and the tablet off the per-change CI path.
// Scheduled and manual runs exercise both API levels and targets, plus a tablet.
if (context.eventName !== 'schedule' && context.eventName !== 'workflow_dispatch') {
target = ['google_apis'];
} else {
// Keep the minor version: SDK 37 packages use android-37.0.
apiLevel.push('37.0');
include = iterationArray.map((iteration) => ({
"iteration": iteration,
"api-level": 36,
"arch": "x86_64",
"target": "google_apis",
"first-boot-delay": 600,
"device": "tablet",
"profile": "pixel_tablet"
}));
}
let firstBootDelay = [600];

Expand All @@ -83,15 +93,17 @@ jobs:
"api-level": apiLevel,
"arch": arch,
"target": target,
"first-boot-delay": firstBootDelay
"first-boot-delay": firstBootDelay,
"device": ["phone"],
"include": include
}
- name: Debug Output
run: echo "${{ steps.build-matrix.outputs.result }}"

# This uses the matrix generated from the matrix-prep stage
# it will run unit tests on whatever OS combinations are desired
emulator_test:
name: Android Emulator Test (${{ matrix.api-level }}, ${{ matrix.arch }}, ${{ matrix.target }}, ${{ matrix.first-boot-delay }}, ${{ matrix.iteration }})
name: Android Emulator Test (${{ matrix.api-level }}, ${{ matrix.arch }}, ${{ matrix.target }}, ${{ matrix.first-boot-delay }}, ${{ matrix.iteration }}${{ matrix.device == 'tablet' && ', tablet' || '' }})
runs-on: ubuntu-latest
needs: matrix_prep
timeout-minutes: 75
Expand Down Expand Up @@ -152,7 +164,7 @@ jobs:

# Cache only the per-change emulator: the 16 KB snapshot costs about 2.5 GiB.
- name: AVD cache
if: matrix.target == 'google_apis'
if: matrix.target == 'google_apis' && matrix.device == 'phone'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: avd-cache
with:
Expand All @@ -163,6 +175,9 @@ jobs:
restore-keys: |
avd-${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-v2

- name: Pin Build Time
run: echo "ORG_GRADLE_PROJECT_buildTime=$(date +%s000)" >> "$GITHUB_ENV"

- name: Warm Gradle Cache
# This makes sure we fetch gradle network resources with a retry
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
Expand Down Expand Up @@ -202,15 +217,16 @@ jobs:
"${ANDROID_HOME:?}/system-images/android-${SDK_API_LEVEL:?}/${SDK_TARGET:?}/${SDK_ARCH:?}/.installer"

- name: AVD Boot and Snapshot Creation
# Warm up the uncached 16 KB emulator on every run; otherwise only on main cache misses.
# Warm up the uncached 16 KB and tablet emulators on every run; otherwise only on main cache misses.
# Comment the if out to generate snapshots on branch for performance testing
if: "${{ matrix.target == 'google_apis_ps16k' || (steps.avd-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main') }}"
if: "${{ matrix.target == 'google_apis_ps16k' || matrix.device == 'tablet' || (steps.avd-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main') }}"
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
profile: ${{ matrix.profile }}
emulator-options: ${{ env.EMULATOR_OPTIONS }}
ram-size: ${{ env.EMULATOR_RAM_SIZE }}
pre-emulator-launch-script: ${{ env.EMULATOR_PRE_LAUNCH_SCRIPT }}
Expand All @@ -225,10 +241,10 @@ jobs:

# This step is separate so pure install time may be calculated as a step
- name: Emulator Snapshot After Firstboot Warmup
# Always warm up the 16 KB emulator. The default emulator only needs it on main cache misses.
# Always warm up the 16 KB and tablet emulators. The default emulator only needs it on main cache misses.
# Switch the if statements via comment if generating snapshots for performance testing
# if: matrix.first-boot-delay != '0'
if: "${{ matrix.target == 'google_apis_ps16k' || (steps.avd-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main') }}"
if: "${{ matrix.target == 'google_apis_ps16k' || matrix.device == 'tablet' || (steps.avd-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main') }}"
env:
FIRST_BOOT_DELAY: ${{ matrix.first-boot-delay }}
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
Expand All @@ -237,6 +253,7 @@ jobs:
force-avd-creation: false
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
profile: ${{ matrix.profile }}
emulator-options: ${{ env.EMULATOR_OPTIONS }}
ram-size: ${{ env.EMULATOR_RAM_SIZE }}
pre-emulator-launch-script: ${{ env.EMULATOR_PRE_LAUNCH_SCRIPT }}
Expand All @@ -260,6 +277,7 @@ jobs:
force-avd-creation: false
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
profile: ${{ matrix.profile }}
emulator-options: -no-snapshot-save ${{ env.EMULATOR_OPTIONS }}
ram-size: ${{ env.EMULATOR_RAM_SIZE }}
pre-emulator-launch-script: ${{ env.EMULATOR_PRE_LAUNCH_SCRIPT }}
Expand All @@ -274,12 +292,32 @@ jobs:
bash .github/adb_retry.sh emu screenrecord start --time-limit 1800 video.webm
sleep 5
bash .github/with_emulator_log.sh adb-log.txt python3 .github/run_emulator_tests.py
TEST_RELEASE_BUILD=false ./gradlew :baselineprofile:connectedBenchmarkBenchmarkAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.ichi2.anki.baselineprofile.ReleaseSmokeTest -Pandroid.testInstrumentationRunnerArguments.releaseSmokeTest=true --daemon

- name: Report Emulator Test Results
if: always() && hashFiles('AnkiDroid/build/outputs/androidTest-results/connected/**/TEST-*.xml') != ''
run: python3 .github/report_test_failures.py AnkiDroid/build/outputs/androidTest-results/connected adb-log.txt --title "Emulator test failures"

- name: Build Release Smoke Test
run: TEST_RELEASE_BUILD=false ./gradlew packagePlayRelease :baselineprofile:packageBenchmarkBenchmark --daemon

- name: Run Release Smoke Test
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
timeout-minutes: 10
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
emulator-options: -no-snapshot-save ${{ env.EMULATOR_OPTIONS }}
ram-size: ${{ env.EMULATOR_RAM_SIZE }}
pre-emulator-launch-script: ${{ env.EMULATOR_PRE_LAUNCH_SCRIPT }}
sdcard-path-or-size: 100M
disable-animations: true
script: |
bash .github/disable_art_metrics.sh
bash .github/adb_retry.sh shell settings put global hide_error_dialogs 1
TEST_RELEASE_BUILD=false ./gradlew :baselineprofile:connectedBenchmarkBenchmarkAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.ichi2.anki.baselineprofile.ReleaseSmokeTest -Pandroid.testInstrumentationRunnerArguments.releaseSmokeTest=true --daemon

- name: Configure NDK tools PATH for alignment checking
run: |
echo "Using pre-installed NDK tools for alignment checking"
Expand Down Expand Up @@ -332,21 +370,21 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-jacocoAndroidTestReport
name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-${{ matrix.device }}-jacocoAndroidTestReport
path: ~/work/Anki-Android/Anki-Android/AnkiDroid/build/reports/jacoco/

- name: Upload Emulator Log
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-adb_logs
name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-${{ matrix.device }}-adb_logs
path: adb-log.txt

- name: Upload Emulator Screen Record
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-adb_video
name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-${{ matrix.device }}-adb_video
path: video.webm

- uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
Expand Down
Loading
Loading