Skip to content

Support Android API level 27 (STF-772)#42

Merged
horgh merged 7 commits into
mainfrom
greg/stf-772-support-android-sdk-api-level-27-for-device-tracking
Jun 23, 2026
Merged

Support Android API level 27 (STF-772)#42
horgh merged 7 commits into
mainfrom
greg/stf-772-support-android-sdk-api-level-27-for-device-tracking

Conversation

@oschwald

@oschwald oschwald commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Lowers the SDK's minSdk from 29 (Android 10) to 27 (Android 8.1) so the library installs on a wider device base. Requested in STF-772 — a payment-processor prospect blocked their evaluation on the lack of API-27 support (~7% of their customers on Android 8.1/9).

An audit of every Android framework call in device-sdk/src/main found only two call sites that use an API above 27, both API 28, and both have lossless fallbacks — no collected signal is lost on older devices, so nothing needed to be disabled:

  • InstallationInfoHelper — reads the deprecated int PackageInfo.versionCode when longVersionCode (API 28) is unavailable. On API 27 there is no versionCodeMajor, so the value is identical.
  • DeviceIDsCollector — releases MediaDrm via release() instead of close() (the AutoCloseable close() was added in API 28). The DRM ID is read before the finally, so only cleanup differs.

All other framework calls are ≤ API 24 or already guarded (refreshRate@30, hdrCapabilities@24, getInstallSourceInfo@30).

Changes

  • gradle/libs.versions.toml: minSdk 29 → 27 (single source of truth; moves both modules).
  • Two Build.VERSION.SDK_INT >= P guards with pre-28 fallbacks.
  • New API-27 Robolectric test classes exercising the legacy branches (pinned to @Config(sdk = [27]) since the JUnit 5 Robolectric extension only allows class-level @Config).
  • Docs: README requirement updated to "Android API 27+ (Android 8.1+)"; CHANGELOG entry under 0.3.0. Also corrected a stale Kotlin version in the README (1.9.22+2.2.21+), in its own commit.

Verification

  • ./gradlew :device-sdk:lintDebug — zero NewApi errors at minSdk 27 (the authoritative gate that nothing above 27 remains unguarded).
  • ./gradlew :device-sdk:test — all pass, including the new API-27 tests.
  • ./gradlew detekt ktlintCheck — clean.
  • ./gradlew :device-sdk:assemble — release AAR builds; merged manifest declares minSdkVersion="27" (no transitive dependency raises the floor).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Expanded Android compatibility by lowering the minimum supported API level from 29 to 27 (with continued behavior parity), and updated the minimum Kotlin version to 2.2.21
    • Improved installation metadata collection on older Android versions for more reliable version code handling
  • Bug Fixes
    • Fixed logging configuration not being forwarded correctly to the data collector when logging is enabled
    • Improved MediaDrm cleanup behavior across Android versions
  • Tests
    • Added Robolectric smoke tests covering Android API 27 paths
  • Documentation
    • Updated README and changelog requirements to match the new support levels

oschwald and others added 3 commits June 20, 2026 13:44
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@oschwald, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 40 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ceb8f686-8d10-442a-8254-be63ac33efb3

📥 Commits

Reviewing files that changed from the base of the PR and between 20c6535 and fdeec50.

⛔ Files ignored due to path filters (1)
  • mise.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CLAUDE.md
  • lychee.toml
📝 Walkthrough

Walkthrough

The minimum supported Android API level is lowered from 29 to 27. DeviceIDsCollector and InstallationInfoHelper gain SDK-version-conditional branches to use deprecated pre-API-28 APIs on older devices. The Gradle minSdk config, README, and CHANGELOG are updated accordingly, and Robolectric tests pinned to SDK 27 cover the new legacy code paths.

Changes

Android API 27 compatibility

Layer / File(s) Summary
minSdk config and API 27 implementation branches
gradle/libs.versions.toml, device-sdk/src/main/java/com/maxmind/device/collector/DeviceIDsCollector.kt, device-sdk/src/main/java/com/maxmind/device/collector/helper/InstallationInfoHelper.kt, README.md, CHANGELOG.md
minSdk is lowered to 27; DeviceIDsCollector.collectMediaDrmID() calls close() on API 28+ and release() on older SDKs; InstallationInfoHelper.collect() reads longVersionCode on API 28+ and versionCode.toLong() below; README and CHANGELOG document the new minimum requirement and the enableLogging forwarding fix.
Robolectric tests for API 27 legacy branches
device-sdk/src/test/java/com/maxmind/device/collector/DeviceIDsCollectorApi27RobolectricTest.kt, device-sdk/src/test/java/com/maxmind/device/collector/helper/InstallationInfoHelperApi27RobolectricTest.kt
DeviceIDsCollectorApi27RobolectricTest asserts collect() returns a non-null result at SDK 27; InstallationInfoHelperApi27RobolectricTest stubs the deprecated versionCode int field to verify the legacy long conversion and confirms the legacy installer-resolution branch completes without throwing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop hop, we've gone back in time,
From API 29 to 27 — sublime!
release() or close(), we check the SDK,
versionCode int? We handle that too, okay.
No signal is lost on this backwards leap,
The rabbit's been digging just a little more deep! 🕳️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically summarizes the main change: lowering the minimum Android API level from 29 to 27, which is the core objective of the entire changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch greg/stf-772-support-android-sdk-api-level-27-for-device-tracking

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request lowers the minimum supported Android API level from 29 to 27, introducing fallback mechanisms for retrieving the app version code and cleaning up MediaDrm on older API levels. Corresponding Robolectric tests have been added to verify these changes on API 27. The reviewer recommends reverting the Kotlin version update in the README as it is out of scope for this pull request.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread README.md
- Android API 29+ (Android 10+)
- Kotlin 1.9.22+
- Android API 27+ (Android 8.1+)
- Kotlin 2.2.21+

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This pull request includes an update to the Kotlin version in the README (Kotlin 1.9.22+ to Kotlin 2.2.21+). According to our general rules, we should avoid making out-of-scope edits to pre-existing content that is not the primary focus of the pull request. Please revert this change and address it in a separate PR or commit if necessary.

Suggested change
- Kotlin 2.2.21+
- Kotlin 1.9.22+
References
  1. Avoid making out-of-scope edits (such as wording or accessibility improvements) to pre-existing content that is not the primary focus of the pull request.

Comment thread README.md

- Android API 27+ (Android 8.1+)
- Kotlin 1.9.22+
- Kotlin 2.2.21+

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude noticed that CLAUDE.md is out of date regarding this.

*/
@ExtendWith(RobolectricExtension::class)
@Config(sdk = [27])
internal class InstallationInfoHelperApi27RobolectricTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, but from Claude:

  🟠 MAJOR — the legacy-versionCode test proves nothing (432d75f)

  InstallationInfoHelperApi27RobolectricTest.kt:33-39 — the test sets packageInfo.versionCode = 456 and asserts assertEquals(456L, result.versionCode). But in PackageInfo the
  deprecated int versionCode and longVersionCode share backing storage: getLongVersionCode() returns (versionCodeMajor << 32) | (versionCode & 0xFFFFFFFF), and versionCodeMajor
  defaults to 0 — so longVersionCode also returns 456.

  The assertion therefore passes identically whether production takes the >= P (longVersionCode) branch or the else (legacy int) branch. @Config(sdk = [27]) does force the else
  branch to execute (good for coverage), but if a regression flipped the if (SDK_INT >= P) condition or deleted the else branch, this test would still go green. That's exactly
  the false-confidence case the test was written to prevent. The production code is correct today; the test just can't verify it.

  Fix: make the int and long views differ so only the legacy path yields 456 — e.g. packageInfo.setLongVersionCode((7L << 32) or 456L) (or set versionCodeMajor = 7), so
  longVersionCode would read (7L<<32)|456 while the legacy int path reads 456L; then assert 456L. Alternatively, downgrade the KDoc's claim to "exercises the else branch
  crash-free" rather than "verifies value provenance."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is possible. Claude updated the comments though.

oschwald and others added 4 commits June 23, 2026 16:58
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
They are out of date and it would be better for it to check the
canoncial source.
@oschwald oschwald force-pushed the greg/stf-772-support-android-sdk-api-level-27-for-device-tracking branch from be9647e to fdeec50 Compare June 23, 2026 17:17
@horgh horgh merged commit 32a2ac0 into main Jun 23, 2026
11 checks passed
@horgh horgh deleted the greg/stf-772-support-android-sdk-api-level-27-for-device-tracking branch June 23, 2026 17:26
@coderabbitai coderabbitai Bot mentioned this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants