This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an Android SDK library for collecting device data and sending it to MaxMind servers. The project uses Kotlin with Java compatibility (@JvmStatic, @JvmOverloads) and is designed to be published to Maven Central.
Key Design Principles:
- Kotlin-first with explicit API mode (
-Xexplicit-api=strict) - Java compatibility for broader adoption
- Coroutine-based async operations with callback alternatives
- Singleton pattern with initialization guard
- Builder pattern for configuration
Follow Kotlin conventions (kotlinlang.org/docs/coding-conventions.html):
- 2-letter acronyms: ALL CAPS (
ID,OS) - 3+ letter acronyms: First letter only (
Gpu,Drm,Api,Sdk,Cpu,Dpi)
# Build SDK library only (fastest for SDK development)
./gradlew :device-sdk:assemble
# Build debug variants (no minification, so faster)
./gradlew assembleDebug
# Build SDK library with all variants
./gradlew :device-sdk:build
# Install sample app to connected device
./gradlew :sample:installDebug# Run unit tests for SDK. Under AGP 9 this runs the suite once, against the
# debug variant: AGP 9 no longer registers testReleaseUnitTest.
./gradlew :device-sdk:test
# Run specific test class
./gradlew :device-sdk:test --tests "com.maxmind.device.DeviceTrackerTest"# Run all quality checks
./gradlew detekt ktlintCheck
# Auto-fix formatting issues
./gradlew ktlintFormat
# Generate API documentation
./gradlew :device-sdk:dokkaGenerate
# Output: device-sdk/build/dokka/This project uses precious for pre-commit hooks. Before committing, run:
# Tidy all staged files (fixes formatting issues)
precious tidy -g
# Then stage the tidied files and commit
git add -u && git commitIf a commit fails due to formatting, run precious tidy -g and retry.
See README.dev.md for the full release process. For manual publishing:
# Publish to Maven Central via Central Portal (requires credentials in local.properties)
./gradlew :device-sdk:publishAndReleaseToMavenCentralThe SDK uses a singleton pattern with initialization guard:
-
DeviceTracker - Main singleton entry point
- Private constructor prevents direct instantiation
initialize(Context, SdkConfig)must be called firstgetInstance()returns the initialized instance or throwsisInitialized()checks initialization state
-
Lifecycle Management
- Stores
applicationContext(not activity context) - Creates coroutine scope with
SupervisorJob + Dispatchers.IO shutdown()cancels scope and closes HTTP client- Automatic collection runs in background if
collectionIntervalMs > 0
- Stores
Four-layer architecture:
-
Public API Layer (
DeviceTracker.kt)- Singleton facade pattern
- Both suspend functions and callback-based methods
- Returns
Result<TrackingResult>containing tracking token - Example:
collectAndSend()(suspend) andcollectAndSend(callback)(callbacks)
-
Configuration Layer (
config/SdkConfig.kt)- Immutable configuration with builder pattern
SdkConfig.Buildervalidates inputs inbuild()- Default servers:
d-ipv6.mmapiws.comandd-ipv4.mmapiws.com(dual-request flow)
-
Data Collection Layer (
collector/DeviceDataCollector.kt)- Collects device information via Android APIs
- Uses
WindowManagerfor display metrics - Returns
DeviceDataserializable model
-
Network Layer (
network/DeviceApiClient.kt)- Ktor HTTP client with Android engine
- kotlinx.serialization for JSON
- Optional logging based on
enableLoggingconfig - Returns
Result<ServerResponse>internally for error handling
Dual-Request Flow (IPv6/IPv4): To capture both IP addresses for a device, the SDK uses a dual-request flow:
- First request sent to
d-ipv6.mmapiws.com/device/android - If response contains
ip_version: 6, a second request is sent tod-ipv4.mmapiws.com/device/android - The IPv4 request is fire-and-forget (failures don't affect the result)
- The stored ID from the IPv6 response is returned and persisted
If a custom server URL is configured via
SdkConfig.Builder.serverUrl(), the dual-request flow is disabled and only a single request is sent.
These are two names for related but distinct concepts:
- Stored ID (
stored_id): The server-generated identifier returned in the API response and persisted locally viaStoredIDStorage. Used internally throughout the SDK (model classes, storage, network layer, comments, logs). - Tracking token (
trackingToken): The value exposed to SDK consumers viaTrackingResult.trackingToken, intended for passing to the minFraud API's/device/tracking_tokenfield.
Today they happen to be the same value, but the abstraction exists so the public-facing token format can change independently of the internal stored ID. Use "stored ID" in internal code and "tracking token" only in public API surfaces.
DeviceData (model/DeviceData.kt):
- Internal data class marked with
@Serializablefor kotlinx.serialization - Immutable with default values for optional fields
When adding new public APIs:
-
Use @JvmStatic for static/companion methods
companion object { @JvmStatic fun initialize(context: Context, config: SdkConfig): DeviceTracker }
-
Use @JvmOverloads for optional parameters
@JvmOverloads public fun collectAndSend(callback: ((Result<TrackingResult>) -> Unit)? = null)
-
Provide callback-based alternatives to suspend functions
// Suspend function for Kotlin suspend fun collectAndSend(): Result<TrackingResult> // Callback version for Java fun collectAndSend(callback: (Result<TrackingResult>) -> Unit)
-
Use explicit visibility modifiers
- All public APIs must have
publickeyword (enforced by-Xexplicit-api=strict)
- All public APIs must have
All dependencies are centralized in gradle/libs.versions.toml:
To update a dependency:
- Edit version in
gradle/libs.versions.toml - Sync Gradle
- Run
./gradlew :device-sdk:buildto verify
The SDK includes consumer ProGuard rules in consumer-rules.pro:
- Keeps public SDK API classes (
DeviceTracker,SdkConfig,TrackingResult) - Keeps kotlinx.serialization classes
- Apps using this SDK automatically inherit these rules
Quick setup with mise (recommended for headless environments):
mise install # Installs Java 21, Android SDK cmdline-tools, etc.
mise run setup # Accepts licenses, installs platform packages, creates local.propertiesManual setup:
-
Java 21. The version is pinned by
mise.toml, not by the build scripts — there is noorg.gradle.java.homeingradle.properties. Without mise, put a Java 21 JDK onJAVA_HOME. -
Android SDK with the platform and build-tools matching
compileSdkingradle/libs.versions.toml -
local.propertiesfile (gitignored):sdk.dir=/path/to/your/Android/Sdk
Java Version Issues:
- The project requires Java 17+;
mise.tomlpins Java 21 - If Gradle picks up a different JDK, check
mise currentandJAVA_HOME
"SDK licenses not accepted"
~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager --licensesDetekt/ktlint failures
- Skip with:
./gradlew build -x detekt -x ktlintCheck - Auto-fix formatting:
./gradlew ktlintFormat
The sample app has isMinifyEnabled = true for release builds, so
:sample:assembleRelease is the only thing that runs R8 over the SDK's classes.
CI runs it on every pull request and on pushes to main, so it is expected to
pass; if it starts failing, treat that as a real problem rather than an expected
quirk of the sample.
Note what a green run does and does not prove. It shows R8 completed, not that
consumer-rules.pro still keeps everything kotlinx.serialization needs at
runtime — that would fail as a SerializationException inside a consumer app,
not as a build failure. AGP also wires lintVitalRelease into this task, so a
failure is not necessarily R8's.
- Unit tests in
device-sdk/src/test/use JUnit 5, MockK, and Robolectric - Android instrumented tests in
device-sdk/src/androidTest/
When adding features, write unit tests that:
- Mock the Android Context with MockK or use Robolectric
- Test both success and failure paths
- Test Java compatibility if API is public
Uses Gradle version catalog in gradle/libs.versions.toml:
[versions]- Version numbers[libraries]- Individual dependencies[plugins]- Gradle plugins[bundles]- Grouped dependencies (e.g.,ktor,testing)
Access in build files: libs.ktor.client.core, libs.plugins.android.library
Publishing uses the
Vanniktech Maven Publish
plugin configured in device-sdk/build.gradle.kts. The plugin publishes to
Maven Central via Central Portal with automatic release.
Credentials are read from ~/.m2/settings.xml (server id central) to share
credentials with other MaxMind Maven projects. GPG signing uses the system gpg
command, so existing ~/.gnupg configuration is used automatically.
See README.dev.md for the full release process and credential setup.
device-sdk/- Android library module (the SDK)sample/- Android application module (demo app)config/detekt/- Shared Detekt configurationgradle/- Gradle wrapper and version catalog
Both modules are independent but sample depends on device-sdk via
implementation(project(":device-sdk")).