Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
9 changes: 0 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Android SDK for collecting and reporting device data to MaxMind.

## Requirements

- 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.

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.

- AndroidX libraries

## Installation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Context>()
val collector = DeviceIDsCollector(context)

val result = collector.collect()

assertNotNull(result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 {

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.

@Test
internal fun `collect reads versionCode from legacy int field on API 27`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val shadowPm = Shadows.shadowOf(context.packageManager)

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"

val helper = InstallationInfoHelper(context)
val result = helper.collect()

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")
}

@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<Context>()
val helper = InstallationInfoHelper(context)

val result = helper.collect()

assertNotNull(result)
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
# SDK Versions
minSdk = "29"
minSdk = "27"
targetSdk = "36"
compileSdk = "36"

Expand Down
6 changes: 4 additions & 2 deletions lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
134 changes: 119 additions & 15 deletions mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading