From 5e168cbfffcab6bcc2ac4d344e7acf0c0328ffbc Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 19 Jun 2026 22:11:26 +0000 Subject: [PATCH 1/7] Support Android API level 27 Lower minSdk from 29 (Android 10) to 27 (Android 8.1) so the SDK installs on a wider device base. Two framework calls require API 28 and now route through lossless pre-28 fallbacks on older devices: - InstallationInfoHelper: read the deprecated int versionCode when PackageInfo.longVersionCode is unavailable (< API 28). - DeviceIDsCollector: release MediaDrm via release() instead of close() (the AutoCloseable close() was added in API 28). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 +++++- README.md | 2 +- .../com/maxmind/device/collector/DeviceIDsCollector.kt | 8 +++++++- .../device/collector/helper/InstallationInfoHelper.kt | 7 ++++++- gradle/libs.versions.toml | 2 +- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0a4ff..839a79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # Changelog -## 0.2.1 (TBD) +## 0.3.0 (TBD) +- Lowered the minimum supported Android API level from 29 (Android 10) to 27 + (Android 8.1). Device data collection on API 27 and 28 falls back to + pre-API-28 methods for the app version code and MediaDRM cleanup; no collected + signals are lost. - Fixed `enableLogging` not being forwarded from `SdkConfig` to `DeviceDataCollector`, which caused collector-level error logs to be silently suppressed even when logging was explicitly enabled. diff --git a/README.md b/README.md index 31039d3..5c65a2c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Android SDK for collecting and reporting device data to MaxMind. ## Requirements -- Android API 29+ (Android 10+) +- Android API 27+ (Android 8.1+) - Kotlin 1.9.22+ - AndroidX libraries diff --git a/device-sdk/src/main/java/com/maxmind/device/collector/DeviceIDsCollector.kt b/device-sdk/src/main/java/com/maxmind/device/collector/DeviceIDsCollector.kt index eba655a..e51a30f 100644 --- a/device-sdk/src/main/java/com/maxmind/device/collector/DeviceIDsCollector.kt +++ b/device-sdk/src/main/java/com/maxmind/device/collector/DeviceIDsCollector.kt @@ -3,6 +3,7 @@ package com.maxmind.device.collector import android.annotation.SuppressLint import android.content.Context import android.media.MediaDrm +import android.os.Build import android.provider.Settings import android.util.Base64 import android.util.Log @@ -46,7 +47,12 @@ internal class DeviceIDsCollector( val deviceId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID) Base64.encodeToString(deviceId, Base64.NO_WRAP) } finally { - mediaDrm.close() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + mediaDrm.close() + } else { + @Suppress("DEPRECATION") + mediaDrm.release() + } } } catch ( @Suppress("TooGenericExceptionCaught", "SwallowedException") diff --git a/device-sdk/src/main/java/com/maxmind/device/collector/helper/InstallationInfoHelper.kt b/device-sdk/src/main/java/com/maxmind/device/collector/helper/InstallationInfoHelper.kt index a8d8e8b..2edcfbd 100644 --- a/device-sdk/src/main/java/com/maxmind/device/collector/helper/InstallationInfoHelper.kt +++ b/device-sdk/src/main/java/com/maxmind/device/collector/helper/InstallationInfoHelper.kt @@ -22,7 +22,12 @@ internal class InstallationInfoHelper( public fun collect(): InstallationInfo { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - val versionCode = packageInfo.longVersionCode + val versionCode = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode + } else { + packageInfo.versionCode.toLong() + } val installerPackage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd1abe2..11459a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # SDK Versions -minSdk = "29" +minSdk = "27" targetSdk = "36" compileSdk = "36" From 432d75f8559468dbc315cab38d9f6cd74f3ae80f Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 19 Jun 2026 22:11:33 +0000 Subject: [PATCH 2/7] Add API-27 Robolectric tests for legacy fallback paths Pin new Robolectric test classes to @Config(sdk = [27]) to exercise the pre-API-28 branches added for API 27 support: the legacy int versionCode read in InstallationInfoHelper, and the MediaDrm release()/collect path in DeviceIDsCollector. The JUnit 5 Robolectric extension only allows @Config at the class level, so these live in separate classes from the sdk-29/30 tests that cover the modern branches. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../DeviceIDsCollectorApi27RobolectricTest.kt | 37 +++++++++++++ ...tallationInfoHelperApi27RobolectricTest.kt | 55 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 device-sdk/src/test/java/com/maxmind/device/collector/DeviceIDsCollectorApi27RobolectricTest.kt create mode 100644 device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt diff --git a/device-sdk/src/test/java/com/maxmind/device/collector/DeviceIDsCollectorApi27RobolectricTest.kt b/device-sdk/src/test/java/com/maxmind/device/collector/DeviceIDsCollectorApi27RobolectricTest.kt new file mode 100644 index 0000000..6b04418 --- /dev/null +++ b/device-sdk/src/test/java/com/maxmind/device/collector/DeviceIDsCollectorApi27RobolectricTest.kt @@ -0,0 +1,37 @@ +package com.maxmind.device.collector + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.robolectric.annotation.Config +import tech.apter.junit.jupiter.robolectric.RobolectricExtension + +/** + * Best-effort Robolectric smoke test for [DeviceIDsCollector] on API 27 (Android 8.1). + * + * On API < 28 the collector releases [android.media.MediaDrm] via the deprecated + * `release()` lifecycle call instead of `close()` (which only exists from API 28). + * This test pins the SDK to 27 and asserts that [DeviceIDsCollector.collect] returns a + * [com.maxmind.device.model.DeviceIDs] without throwing. + * + * Known limitation: Robolectric's MediaDrm shadow does not exercise the real + * `release()`/`close()` split, so this test cannot fully validate the cleanup branch — + * consistent with how [DeviceIDsCollectorTest] treats MediaDRM as null in unit tests. + * The real branch is validated by lint's NewApi check and (optionally) an instrumented + * test on an API-27 device. + */ +@ExtendWith(RobolectricExtension::class) +@Config(sdk = [27]) +internal class DeviceIDsCollectorApi27RobolectricTest { + @Test + internal fun `collect returns DeviceIDs without throwing on API 27`() { + val context = ApplicationProvider.getApplicationContext() + val collector = DeviceIDsCollector(context) + + val result = collector.collect() + + assertNotNull(result) + } +} diff --git a/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt b/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt new file mode 100644 index 0000000..4e7ee8b --- /dev/null +++ b/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt @@ -0,0 +1,55 @@ +package com.maxmind.device.collector.helper + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import tech.apter.junit.jupiter.robolectric.RobolectricExtension + +/** + * Robolectric-based tests for [InstallationInfoHelper] on API 27 (Android 8.1). + * + * On API < 28 the helper falls back to the deprecated int [android.content.pm.PackageInfo.versionCode] + * field (read as `.versionCode.toLong()`) and to [android.content.pm.PackageManager.getInstallerPackageName] + * for the installer. JUnit 5's Robolectric extension can only set `@Config(sdk)` at the class + * level, so this separate class pins the SDK to 27 to exercise those legacy branches; the + * API-28+ branches stay covered by [InstallationInfoHelperRobolectricTest] at sdk 29. + */ +@ExtendWith(RobolectricExtension::class) +@Config(sdk = [27]) +internal class InstallationInfoHelperApi27RobolectricTest { + @Test + internal fun `collect reads versionCode from legacy int field on API 27`() { + val context = ApplicationProvider.getApplicationContext() + val shadowPm = Shadows.shadowOf(context.packageManager) + + // On API 27 the helper reads the deprecated int versionCode field, not longVersionCode. + val packageInfo = shadowPm.getInternalMutablePackageInfo(context.packageName) + @Suppress("DEPRECATION") + packageInfo.versionCode = 456 + packageInfo.versionName = "9.8.7" + + val helper = InstallationInfoHelper(context) + val result = helper.collect() + + assertEquals(456L, result.versionCode, "versionCode should come from the legacy int field") + assertEquals("9.8.7", result.versionName, "versionName should match stubbed value") + } + + @Test + internal fun `collect resolves installer via legacy branch without throwing on API 27`() { + // On API < 30 the helper takes the getInstallerPackageName branch. The installer is + // typically null in the Robolectric environment; we only verify the legacy branch is + // exercised without throwing (mirroring the sdk-29 InstallationInfoHelperRobolectricTest). + val context = ApplicationProvider.getApplicationContext() + val helper = InstallationInfoHelper(context) + + val result = helper.collect() + + assertNotNull(result) + } +} From 430160994987d2424f896057e243d3cde5702851 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Sat, 20 Jun 2026 01:38:59 +0000 Subject: [PATCH 3/7] Correct Kotlin version in README requirements The README listed Kotlin 1.9.22+, but the project builds with Kotlin 2.2.21 (gradle/libs.versions.toml). Update the requirement to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c65a2c..ba6e543 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Android SDK for collecting and reporting device data to MaxMind. ## Requirements - Android API 27+ (Android 8.1+) -- Kotlin 1.9.22+ +- Kotlin 2.2.21+ - AndroidX libraries ## Installation From 18a75f456a52c59d342cedb2d8811bf16bde3ea8 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 23 Jun 2026 16:58:26 +0000 Subject: [PATCH 4/7] Strengthen API-27 versionCode test to discriminate the branch The test set packageInfo.versionCode = 456 and asserted 456, which a reviewer noted could pass regardless of which branch production took. The suggested fix (pack a non-zero versionCodeMajor into longVersionCode) is not possible here: at @Config(sdk = [27]) Robolectric loads the API-27 runtime, where PackageInfo.getLongVersionCode/setLongVersionCode (API 28) do not exist. That same fact is what makes the test discriminate: if the production guard regressed to read longVersionCode at API 27, collect() would throw NoSuchMethodError (it is not caught) and the assertion would never pass. Verified by temporarily flipping the guard. Documented the mechanism in the test instead of relying on value packing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../helper/InstallationInfoHelperApi27RobolectricTest.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt b/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt index 4e7ee8b..c045292 100644 --- a/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt +++ b/device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt @@ -27,8 +27,12 @@ internal class InstallationInfoHelperApi27RobolectricTest { val context = ApplicationProvider.getApplicationContext() val shadowPm = Shadows.shadowOf(context.packageManager) - // On API 27 the helper reads the deprecated int versionCode field, not longVersionCode. val packageInfo = shadowPm.getInternalMutablePackageInfo(context.packageName) + // Set only the legacy int versionCode field. The API-28 longVersionCode accessor does + // not exist in the API-27 runtime Robolectric loads here, so we cannot (and need not) + // pack a major version. This also makes the test discriminate the branch: if the + // production guard regressed to read packageInfo.longVersionCode at API 27, collect() + // would throw NoSuchMethodError (it is not caught) and this assertion would never run. @Suppress("DEPRECATION") packageInfo.versionCode = 456 packageInfo.versionName = "9.8.7" @@ -36,7 +40,7 @@ internal class InstallationInfoHelperApi27RobolectricTest { val helper = InstallationInfoHelper(context) val result = helper.collect() - assertEquals(456L, result.versionCode, "versionCode should come from the legacy int field") + assertEquals(456L, result.versionCode, "versionCode should come from the legacy int field on API 27") assertEquals("9.8.7", result.versionName, "versionName should match stubbed value") } From 69d5653110987fbf4c3a2ffcc34fcbd63ccda606 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 23 Jun 2026 10:05:26 -0700 Subject: [PATCH 5/7] Update mise lockfile --- mise.lock | 134 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 15 deletions(-) diff --git a/mise.lock b/mise.lock index 156e912..5b2b8e0 100644 --- a/mise.lock +++ b/mise.lock @@ -1,29 +1,133 @@ # @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html +[[tools.android-sdk]] +version = "1.0" +backend = "vfox:mise-plugins/vfox-android-sdk" + +[tools.android-sdk."platforms.linux-arm64"] +url = "https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip" + +[tools.android-sdk."platforms.linux-arm64-musl"] +url = "https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip" + +[tools.android-sdk."platforms.linux-x64"] +url = "https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip" + +[tools.android-sdk."platforms.linux-x64-musl"] +url = "https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip" + +[tools.android-sdk."platforms.macos-arm64"] +url = "https://dl.google.com/android/repository/commandlinetools-mac-6200805_latest.zip" + +[tools.android-sdk."platforms.macos-x64"] +url = "https://dl.google.com/android/repository/commandlinetools-mac-6200805_latest.zip" + +[tools.android-sdk."platforms.windows-x64"] +url = "https://dl.google.com/android/repository/commandlinetools-win-6200805_latest.zip" + +[[tools."github:houseabsolute/precious"]] +version = "0.11.0" +backend = "github:houseabsolute/precious" + +[tools."github:houseabsolute/precious"."platforms.linux-arm64"] +checksum = "sha256:44d98091a988671786a99d8025b169c76bb08718ffa8f793a80ae1aa97ca5472" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-Linux-musl-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434639525" + +[tools."github:houseabsolute/precious"."platforms.linux-arm64-musl"] +checksum = "sha256:44d98091a988671786a99d8025b169c76bb08718ffa8f793a80ae1aa97ca5472" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-Linux-musl-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434639525" + +[tools."github:houseabsolute/precious"."platforms.linux-x64"] +checksum = "sha256:6a267a3e309ebd39ef36352e283b2ad70066ccb61e4b04785afbd85df3fa76b4" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-Linux-musl-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434639494" + +[tools."github:houseabsolute/precious"."platforms.linux-x64-musl"] +checksum = "sha256:6a267a3e309ebd39ef36352e283b2ad70066ccb61e4b04785afbd85df3fa76b4" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-Linux-musl-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434639494" + +[tools."github:houseabsolute/precious"."platforms.macos-arm64"] +checksum = "sha256:e6cbdba5261c15e3eb9208af928facff50c883bb11c0dd0c10766379285c75b9" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-macOS-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434639276" + +[tools."github:houseabsolute/precious"."platforms.macos-x64"] +checksum = "sha256:5500304c379686acd0cb852ad9f32e20baaab939f7fbc1f75f1ec0583f4bd676" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-macOS-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434639281" + +[tools."github:houseabsolute/precious"."platforms.windows-x64"] +checksum = "sha256:bc5dfb482675ef79798fa578dc5f8edea1cd9f2b5d7bdc5bb02b4a9590f54467" +url = "https://github.com/houseabsolute/precious/releases/download/v0.11.0/precious-Windows-msvc-x86_64.zip" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/434640219" + +[[tools.java]] +version = "temurin-21.0.11+10.0.LTS" +backend = "core:java" + +[tools.java."platforms.linux-x64"] +checksum = "sha256:4b2220e232a97997b436ca6ab15cbf70171ecff52958a46159dfa5a8c44ca4de" +url = "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.11%2B10/OpenJDK21U-jdk_x64_linux_hotspot_21.0.11_10.tar.gz" + [[tools.lychee]] -version = "0.23.0" +version = "0.24.2" backend = "aqua:lycheeverse/lychee" [tools.lychee."platforms.linux-arm64"] -checksum = "sha256:97eb93b02a7d78a752fc33e5b0983439ccaadbf3db952b68a0a4401acd92e6e0" -url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-aarch64-unknown-linux-gnu.tar.gz" +checksum = "sha256:91a7bd65685da41b90ccb9bc867a3d649a7818042dae04ff405e55a25bddee4c" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-aarch64-unknown-linux-gnu.tar.gz" [tools.lychee."platforms.linux-arm64-musl"] -checksum = "sha256:97eb93b02a7d78a752fc33e5b0983439ccaadbf3db952b68a0a4401acd92e6e0" -url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-aarch64-unknown-linux-gnu.tar.gz" +checksum = "sha256:91a7bd65685da41b90ccb9bc867a3d649a7818042dae04ff405e55a25bddee4c" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-aarch64-unknown-linux-gnu.tar.gz" [tools.lychee."platforms.linux-x64"] -checksum = "sha256:5538440d2c69a45a0a09983271e5dee0c2fe7137d8035d25b2632e10a66a090a" -url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-unknown-linux-musl.tar.gz" +checksum = "sha256:73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-unknown-linux-musl.tar.gz" [tools.lychee."platforms.linux-x64-musl"] -checksum = "sha256:5538440d2c69a45a0a09983271e5dee0c2fe7137d8035d25b2632e10a66a090a" -url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-unknown-linux-musl.tar.gz" +checksum = "sha256:73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5" +url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-unknown-linux-musl.tar.gz" + +[[tools.prettier]] +version = "3.8.4" +backend = "npm:prettier" + +[[tools.yamllint]] +version = "1.38.0" +backend = "pipx:yamllint" + +[[tools.yq]] +version = "4.53.3" +backend = "aqua:mikefarah/yq" + +[tools.yq."platforms.linux-arm64"] +checksum = "sha256:578648e463a11c1b6db6010cbf41eafed6bee79466fcffa1bb446672cf7945ea" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_arm64" + +[tools.yq."platforms.linux-arm64-musl"] +checksum = "sha256:578648e463a11c1b6db6010cbf41eafed6bee79466fcffa1bb446672cf7945ea" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_arm64" + +[tools.yq."platforms.linux-x64"] +checksum = "sha256:fa52a4e758c63d38299163fbdd1edfb4c4963247918bf9c1c5d31d84789eded4" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_amd64" + +[tools.yq."platforms.linux-x64-musl"] +checksum = "sha256:fa52a4e758c63d38299163fbdd1edfb4c4963247918bf9c1c5d31d84789eded4" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_amd64" + +[tools.yq."platforms.macos-arm64"] +checksum = "sha256:877de31753a4dd2401aa048937aa9a7fc4d5f6ce858cf31508c5802954297213" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_darwin_arm64" -[tools.lychee."platforms.macos-arm64"] -checksum = "sha256:4c8034900e11083b68ac6f6582c377ff1f704e268991999e09d717973e493e7f" -url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-arm64-macos.dmg" +[tools.yq."platforms.macos-x64"] +checksum = "sha256:b4ba1ecce3c47f00803f4f964de38394326c7a32eb6540616e04fb2935a0f08d" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_darwin_amd64" -[tools.lychee."platforms.windows-x64"] -checksum = "sha256:0fda7ff0a60c0250939fc25361c2d4e6e7853c31c996733fdd5a1dd760bcb824" -url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.23.0/lychee-x86_64-windows.exe" +[tools.yq."platforms.windows-x64"] +checksum = "sha256:e279bc506a452eeafcdf364f91a025455e402a8001169083caf01f4b64a544e2" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_windows_amd64.exe" From dab8fa7f16b2d24b4e6ce6e7a6dd00618a173190 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 23 Jun 2026 17:12:53 +0000 Subject: [PATCH 6/7] Fix lychee config for lychee 0.24+ (include_fragments enum) lychee 0.24 changed include_fragments from a boolean to a string enum (none | anchor-only | text-only | full). After the mise lockfile bumped lychee to 0.24.2, the old `include_fragments = true` no longer parses ("wanted string or table"), so the Links workflow failed before checking any links. Use "full" to check both anchor and text fragments. Co-Authored-By: Claude Opus 4.8 (1M context) --- lychee.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lychee.toml b/lychee.toml index 25b099c..16fa09a 100644 --- a/lychee.toml +++ b/lychee.toml @@ -4,8 +4,10 @@ # Run locally with: # lychee './**/*.md' './**/*.kt' './**/*.java' -# Include URL fragments in checks -include_fragments = true +# Check fragments in links. Since lychee 0.24 this is a string enum +# (none | anchor-only | text-only | full), not a boolean. "full" checks both +# anchor fragments (#section) and text fragments (#:~:text=...). +include_fragments = "full" # Don't allow any redirects, so links that have moved are surfaced and can be # updated to their canonical destination. From fdeec5049eed78464e5459ab6260ed901ed92678 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 23 Jun 2026 10:16:36 -0700 Subject: [PATCH 7/7] Do not list dependencies in CLAUDE.md They are out of date and it would be better for it to check the canoncial source. --- CLAUDE.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9bd72e7..89cf17d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -212,15 +212,6 @@ When adding new public APIs: All dependencies are centralized in `gradle/libs.versions.toml`: -**Key Dependencies:** - -- Ktor 2.3.7 (HTTP client with Android engine) -- kotlinx.serialization 1.6.2 (JSON serialization) -- kotlinx.coroutines 1.7.3 (async operations) -- Detekt 1.23.5 (Kotlin linting) -- ktlint 12.1.0 (code formatting) -- Dokka 1.9.10 (API documentation) - **To update a dependency:** 1. Edit version in `gradle/libs.versions.toml`