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: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.2.1 (TBD)

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

## 0.2.0 (2026-02-27)

- **Breaking:** `collectAndSend()` now returns `Result<TrackingResult>` instead
Expand Down
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,22 @@ The SDK includes consumer ProGuard rules in `consumer-rules.pro`:

## Environment Setup

**Required:**
**Quick setup with mise (recommended for headless environments):**

```bash
mise install # Installs Java 21, Android SDK cmdline-tools, etc.
mise run setup # Accepts licenses, installs platform packages, creates local.properties
```

**Manual setup:**

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`
2. Android SDK with API 36 at `~/Android/Sdk`

3. `local.properties` file (gitignored):
```properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class DeviceTracker private constructor(
) {
private val applicationContext: Context = context.applicationContext
private val storedIDStorage = StoredIDStorage(applicationContext)
private val deviceDataCollector = DeviceDataCollector(applicationContext, storedIDStorage)
private val deviceDataCollector = DeviceDataCollector(applicationContext, storedIDStorage, config.enableLogging)
private val apiClient = DeviceApiClient(config)

private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
Expand Down
28 changes: 27 additions & 1 deletion device-sdk/src/test/java/com/maxmind/device/DeviceTrackerTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.maxmind.device

import android.content.Context
import android.util.Log
import com.maxmind.device.collector.DeviceDataCollector
import com.maxmind.device.config.SdkConfig
import com.maxmind.device.model.ServerResponse
Expand All @@ -10,6 +11,8 @@ import com.maxmind.device.storage.StoredIDStorage
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
Expand Down Expand Up @@ -261,7 +264,30 @@ internal class DeviceTrackerTest {

@Test
@Order(15)
internal fun `15 collectAndSend wraps collectDeviceData exception in Result failure`() =
internal fun `15 enableLogging is forwarded to DeviceDataCollector`() {
resetSingleton()
mockkStatic(Log::class)
every { Log.d(any(), any()) } returns 0
try {
val loggingConfig =
SdkConfig
.Builder(12345)
.enableLogging(true)
.build()
val tracker = DeviceTracker.initialize(mockContext, loggingConfig)

val collector = getField<DeviceDataCollector>(tracker, "deviceDataCollector")
val enableLogging = getField<Boolean>(collector, "enableLogging")
Comment on lines +279 to +280
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

Using reflection to access private fields in tests is generally discouraged as it makes tests fragile to internal refactoring. Since DeviceDataCollector is an internal class and this test is part of the same module, consider making the enableLogging property internal or providing a package-private getter if you want to avoid reflection while still keeping it hidden from public API consumers.


assertTrue(enableLogging, "enableLogging should be forwarded from SdkConfig to DeviceDataCollector")
} finally {
unmockkStatic(Log::class)
}
}

@Test
@Order(16)
internal fun `16 collectAndSend wraps collectDeviceData exception in Result failure`() =
runTest {
val tracker = createTrackerWithMocks()
val mockCollector = mockk<DeviceDataCollector>()
Expand Down
8 changes: 4 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ serialization = "1.10.0"

# Android
androidGradlePlugin = "8.13.1"
androidxCore = "1.17.0"
androidxCore = "1.18.0"
androidxAppCompat = "1.7.1"
androidxLifecycle = "2.10.0"

# Networking
ktor = "3.4.0"
ktor = "3.4.2"

# Code quality
detekt = "1.23.8"
ktlint = "14.0.1"
ktlint = "14.2.0"

# Documentation
dokka = "2.1.0"
dokka = "2.2.0"

# Testing
junit = "4.13.2"
Expand Down
26 changes: 26 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,29 @@ java = "temurin-21"
# yq is used by release.sh to parse ~/.m2/settings.xml for Maven Central credentials
yq = "latest"
"github:houseabsolute/precious" = "latest"
android-sdk = "latest"

[tasks.setup]
description = "Install Android SDK platform packages and configure local.properties"
run = """
#!/usr/bin/env bash
set -euo pipefail

echo "Accepting Android SDK licenses..."
yes 2>/dev/null | sdkmanager --licenses > /dev/null 2>&1 || true

echo "Installing required SDK packages..."
sdkmanager "platforms;android-36" "build-tools;36.0.0"

if [ ! -f local.properties ]; then
echo "sdk.dir=$ANDROID_HOME" > local.properties
echo "Created local.properties with sdk.dir=$ANDROID_HOME"
elif ! grep -q '^sdk.dir=' local.properties; then
echo "sdk.dir=$ANDROID_HOME" >> local.properties
echo "Added sdk.dir=$ANDROID_HOME to local.properties"
else
echo "local.properties already has sdk.dir configured"
fi

echo "Setup complete. Run './gradlew :device-sdk:test' to verify."
"""
Loading