Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8416306
ci: Add GitHub Actions workflows and Dependabot config
oschwald Dec 8, 2025
7040914
feat: Initial SDK with restructured data model for device fingerprinting
oschwald Dec 3, 2025
fd248b6
feat: Add StoredIDs collection (MediaDRM ID + Android ID)
oschwald Dec 3, 2025
a1c2da5
test: Add DeviceDataCollector tests for build, display, hardware info
oschwald Dec 3, 2025
dc04110
feat: Add GPU info collection via OpenGL ES
oschwald Dec 3, 2025
9b4ed96
feat: Add audio hardware profile collection
oschwald Dec 3, 2025
432d290
feat: Add sensor enumeration for device fingerprinting
oschwald Dec 3, 2025
b5c6cc0
feat: Add camera hardware capabilities collection
oschwald Dec 3, 2025
01f9476
feat: Add codec enumeration and system features collection
oschwald Dec 3, 2025
1ce48fb
feat: Add network context collection with WiFi details
oschwald Dec 3, 2025
2c7d76c
feat: Add system settings and behavioral signals collection
oschwald Dec 3, 2025
9616fc4
feat: Add WebView user agent collection
oschwald Dec 3, 2025
5cf3ca5
fix: Address detekt warnings in collectors and core SDK
oschwald Dec 3, 2025
25da76a
feat: Add server-stored ID support and rename device identifiers
oschwald Dec 3, 2025
06b6f93
feat: Add telephony and font profile collection
oschwald Dec 3, 2025
ce09dd1
test: Add MockEngine tests for DeviceApiClient HTTP behavior
oschwald Dec 8, 2025
283c870
feat: Implement IPv6/IPv4 dual-request flow for IP address capture
oschwald Dec 8, 2025
c5c9b9a
feat(sample): Add collapsible sections for device data display
oschwald Dec 8, 2025
15fee3e
feat(sample): Add device IDs to summary section
oschwald Dec 8, 2025
4d77465
feat: Add collectSafe helper for fault-tolerant data collection
oschwald Dec 9, 2025
1895dd5
refactor: Extract helper classes for testability
oschwald Dec 9, 2025
d587d54
test: Add instrumented tests for GPU, Camera, and Font collectors
oschwald Dec 9, 2025
655b7a3
test: Add DeviceTrackerTest for singleton behavior validation
oschwald Dec 9, 2025
8667afb
docs: Update README with correct API usage and comprehensive data list
oschwald Dec 9, 2025
519f937
feat: Add local development server support for sample app
oschwald Dec 11, 2025
6138538
docs: Add precious tidy instructions to CLAUDE.md
oschwald Dec 11, 2025
6f0ff6a
feat: Require account ID configuration for sample app
oschwald Dec 11, 2025
7c3e050
feat: Add request_duration measurement for proxy detection
oschwald Dec 11, 2025
004d1c0
docs: Move developer-focused content from README to SETUP.md
oschwald Jan 5, 2026
033d51f
feat: Add conditional logging to collectors for debugging
oschwald Jan 5, 2026
239346f
refactor: Standardize CodecCollector exception handling
oschwald Jan 5, 2026
51b52f0
refactor: Remove sorting from SystemFeaturesCollector for consistency
oschwald Jan 5, 2026
ba89767
fix: Generate network security config at build time
oschwald Jan 5, 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
11 changes: 3 additions & 8 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ name: CodeQL
on:
push:
branches: [main]
branches-ignore: [dependabot/**]
pull_request:
branches: [main]
branches: ['**']
schedule:
- cron: '0 14 * * 6'

Expand All @@ -26,16 +27,10 @@ jobs:
distribution: temurin
java-version: '17'

- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-disabled: true

- uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
languages: java-kotlin
build-mode: manual

- name: Build with Gradle
run: ./gradlew clean compileDebugKotlin assembleDebug --no-daemon --no-build-cache --no-configuration-cache
- uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7

- uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,7 @@ inputs.csv
summary.md
plan.md
build-log.txt

# Generated files for local development (configured via local.properties)
sample/src/main/res/raw/debug_ca.crt
sample/src/main/res/xml/network_security_config.xml
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 0.1.0 (unreleased)

- Initial release
325 changes: 325 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository.

## Project Overview

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

## Naming Conventions

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 Commands

### Core Development

```bash
# Build SDK library only (fastest for SDK development)
./gradlew :device-sdk:assemble

# Build debug variants (skips minification issues)
./gradlew assembleDebug

# Build SDK library with all variants
./gradlew :device-sdk:build

# Install sample app to connected device
./gradlew :sample:installDebug
```

### Testing

```bash
# Run unit tests for SDK
./gradlew :device-sdk:test

# Run specific test class
./gradlew :device-sdk:test --tests "com.maxmind.device.DeviceTrackerTest"

# Run tests with coverage (JaCoCo)
./gradlew :device-sdk:testDebugUnitTest jacocoTestReport
```

### Code Quality

```bash
# Run all quality checks
./gradlew detekt ktlintCheck

# Auto-fix formatting issues
./gradlew ktlintFormat

# Generate API documentation
./gradlew :device-sdk:dokkaHtml
# Output: device-sdk/build/dokka/
```

### Pre-commit Formatting

This project uses [precious](https://github.com/houseabsolute/precious) for
pre-commit hooks. Before committing, run:

```bash
# Tidy all staged files (fixes formatting issues)
precious tidy -g

# Then stage the tidied files and commit
git add -u && git commit
```

If a commit fails due to formatting, run `precious tidy -g` and retry.

### Publishing

```bash
# Publish to Maven Central (requires credentials in local.properties)
./gradlew :device-sdk:publishReleasePublicationToMavenCentralRepository
```

## Architecture

### SDK Entry Point Pattern

The SDK uses a **singleton pattern with initialization guard**:

1. **DeviceTracker** - Main singleton entry point
- Private constructor prevents direct instantiation
- `initialize(Context, SdkConfig)` must be called first
- `getInstance()` returns the initialized instance or throws
- `isInitialized()` checks initialization state

2. **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`

### Component Architecture

**Four-layer architecture:**

1. **Public API Layer** (`DeviceTracker.kt`)
- Singleton facade pattern
- Both suspend functions and callback-based methods
- Example: `collectAndSend()` (suspend) and `collectAndSend(callback)`
(callbacks)

2. **Configuration Layer** (`config/SdkConfig.kt`)
- Immutable configuration with builder pattern
- `SdkConfig.Builder` validates inputs in `build()`
- Default servers: `d-ipv6.mmapiws.com` and `d-ipv4.mmapiws.com`
(dual-request flow)

3. **Data Collection Layer** (`collector/DeviceDataCollector.kt`)
- Collects device information via Android APIs
- Uses `WindowManager` for display metrics
- Returns `DeviceData` serializable model

4. **Network Layer** (`network/DeviceApiClient.kt`)
- Ktor HTTP client with Android engine
- kotlinx.serialization for JSON
- Optional logging based on `enableLogging` config
- Returns `Result<ServerResponse>` for error handling

**Dual-Request Flow (IPv6/IPv4):** To capture both IP addresses for a device,
the SDK uses a dual-request flow:
1. First request sent to `d-ipv6.mmapiws.com/device/android`
2. If response contains `ip_version: 6`, a second request is sent to
`d-ipv4.mmapiws.com/device/android`
3. The IPv4 request is fire-and-forget (failures don't affect the result)
4. 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.

### Data Model

**DeviceData** (`model/DeviceData.kt`):

- Marked with `@Serializable` for kotlinx.serialization
- All fields are public for Java compatibility
- Immutable data class
- Optional `deviceId` field (can be null)

## Java Compatibility Strategy

When adding new public APIs:

1. **Use @JvmStatic for static/companion methods**

```kotlin
companion object {
@JvmStatic
fun initialize(context: Context, config: SdkConfig): DeviceTracker
}
```

2. **Use @JvmOverloads for optional parameters**

```kotlin
@JvmOverloads
public fun collectAndSend(callback: ((Result<Unit>) -> Unit)? = null)
```

3. **Provide callback-based alternatives to suspend functions**

```kotlin
// Suspend function for Kotlin
suspend fun collectAndSend(): Result<Unit>

// Callback version for Java
fun collectAndSend(callback: (Result<Unit>) -> Unit)
```

4. **Use explicit visibility modifiers**
- All public APIs must have `public` keyword (enforced by
`-Xexplicit-api=strict`)

## Dependency Management

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`
2. Sync Gradle
3. Run `./gradlew :device-sdk:build` to verify

## ProGuard/R8 Configuration

The SDK includes consumer ProGuard rules in `consumer-rules.pro`:

- Keeps public SDK API
- Keeps kotlinx.serialization classes
- Keeps Ktor classes
- Apps using this SDK automatically inherit these rules

## Environment Setup

**Required:**

1. Java 21 (Android Studio JDK) configured in `gradle.properties`:

```
org.gradle.java.home=/home/greg/.local/share/android-studio/jbr
```

2. Android SDK with API 34 at `~/Android/Sdk`

3. `local.properties` file (gitignored):
```properties
sdk.dir=/home/greg/Android/Sdk
```

**Java Version Issues:**

- The project requires Java 17+ (set to use Java 21 from Android Studio)
- If you see "25" error, Java 25 is being used instead
- Fix by setting `org.gradle.java.home` in `gradle.properties`

## Common Issues

### Build Failures

**"SDK licenses not accepted"**

```bash
~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager --licenses
```

**"Resource mipmap/ic_launcher not found"**

- Sample app has no launcher icons (intentional for simplicity)
- Build works for `assembleDebug`, may fail on `assemble` (release variant)

**Detekt/ktlint failures**

- Skip with: `./gradlew build -x detekt -x ktlintCheck`
- Auto-fix formatting: `./gradlew ktlintFormat`

### Release Build MinifyEnabled

The sample app has `isMinifyEnabled = true` for release builds, which may cause
R8 issues. For development:

```bash
# Build debug variant instead
./gradlew :sample:assembleDebug
```

## Testing Strategy

- **Unit tests** in `device-sdk/src/test/` use JUnit 5, MockK, and Robolectric
- **Android instrumented tests** in `device-sdk/src/androidTest/`
- Test coverage with JaCoCo
- Turbine for testing Flows/coroutines

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

## Version Catalog Structure

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.kotlin.android`

## Maven Publishing Configuration

Located in `device-sdk/build.gradle.kts`:

- POM metadata from `gradle.properties` (POM_NAME, POM_URL, etc.)
- Signing configuration placeholders in `gradle.properties`
- Actual credentials should be in `local.properties` (gitignored)

**Required for publishing:**

```properties
signing.keyId=...
signing.password=...
signing.secretKeyRingFile=...
mavenCentralUsername=...
mavenCentralPassword=...
```

## Module Structure

- `device-sdk/` - Android library module (the SDK)
- `sample/` - Android application module (demo app)
- `config/detekt/` - Shared Detekt configuration
- `gradle/` - Gradle wrapper and version catalog

Both modules are independent but `sample` depends on `device-sdk` via
`implementation(project(":device-sdk"))`.
Loading
Loading