Skip to content
Open
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
63 changes: 63 additions & 0 deletions .github/workflows/api-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,66 @@ jobs:

- name: Check API Compatibility
run: ./gradlew :device-sdk:japicmp

# japicmp only compares classes.jar, so nothing else guards the parts of the
# AAR that decide who can consume it. AGP derives minCompileSdk,
# minCompileSdkExtension and minCompileMinorSdk from the library's own
# compileSdk, so a compileSdk bump can raise the consumer floor without
# anyone editing this repo. The whole file is compared rather than a few
# keys, so an added or changed key has to be acknowledged here deliberately.
#
# minAndroidGradlePluginVersion=1.0.0 holds only while
# android.aar.metadata.autoEncodeMinAgpVersion stays off. Its default flips
# in AGP 10, after which AGP derives the value from compileSdk. If this
# fails during an AGP upgrade, that is a real consumer-facing change to
# decide on, not a stale expectation to edit away.
- name: Check AAR consumer metadata
if: ${{ !cancelled() }}
run: |
set -euo pipefail

# Build what we inspect: japicmp happens to produce the AAR today, but
# only as a side effect of its own task graph.
./gradlew :device-sdk:bundleReleaseAar
aar=device-sdk/build/outputs/aar/device-sdk-release.aar
tmp=$(mktemp -d)

expected=$(printf '%s\n' \
'aarFormatVersion=1.0' \
'aarMetadataVersion=1.0' \
'coreLibraryDesugaringEnabled=false' \
'minAndroidGradlePluginVersion=1.0.0' \
'minCompileSdk=30' \
'minCompileSdkExtension=0')
metadata=$(unzip -p "$aar" META-INF/com/android/build/gradle/aar-metadata.properties)
Comment on lines +40 to +58

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.

minor — this step consumes an artifact it doesn't produce, and it's skipped exactly when you'd want both signals.

device-sdk-release.aar exists only because the previous step's :device-sdk:japicmp happens to pull in bundleReleaseAar (via extractCurrentClasses' dependsOn at device-sdk/build.gradle.kts:228). Nothing in the workflow states that dependency, and there's no if:, so the step is skipped whenever the japicmp step fails — including when japicmp fails for unrelated reasons like the Maven Central baseline download being unavailable.

Two consequences: a future refactor of the japicmp pipeline (comparing intermediates/ classes.jar instead of unzipping the AAR — a perfectly natural cleanup) removes the AAR from the graph and this guard starts failing with unzip: cannot find or open, which reads like infra flake rather than a real signal. And when an API break and a metadata break land in the same PR, the author only sees the first, fixes it, and gets a second red run.

Have the step build what it inspects — ./gradlew :device-sdk:bundleReleaseAar as the first line of the run block, a no-op when japicmp already built it — and consider if: always().

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.

Claude here, replying on behalf of @oschwald.

Fixed in d260d8e. The step now runs ./gradlew :device-sdk:bundleReleaseAar as its first line, so it builds what it inspects instead of relying on extractCurrentClasses' dependsOn, and it runs under if: ${{ !cancelled() }} so an unrelated japicmp failure — a baseline download outage, say — no longer hides a metadata break. Both of your consequences applied: a future japicmp refactor comparing intermediates/ would have turned this into unzip: cannot find or open.

echo "$metadata"
if ! diff <(sort <<<"$expected") <(sort <<<"$metadata"); then
echo "::error::AAR consumer metadata changed (< expected, > actual)"
exit 1
fi

manifest=$(unzip -p "$aar" AndroidManifest.xml)
echo "$manifest"
if ! grep -q 'android:minSdkVersion="27"' <<<"$manifest"; then
echo "::error::expected minSdkVersion 27 in the AAR manifest"
exit 1
fi
Comment on lines +65 to +70

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.

nit — this branch prints no context on failure, unlike the properties branch which does echo "$metadata" at line 38 first.

On failure the log shows only expected minSdkVersion 27 in the AAR manifest with no indication of what the manifest actually declares. Whoever hits this — most likely on a toolchain upgrade, if AGP changes how library manifests are emitted — has to reproduce the build locally to see the actual value. The AAR manifest is ~12 lines of plain text, so echo "$manifest" after line 51 is free.

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.

Claude here, replying on behalf of @oschwald.

Fixed in d260d8eecho "$manifest" before the check, matching the properties branch. Free, and the failure it guards against would most likely arrive during a toolchain upgrade when nobody wants to reproduce the build locally to see one attribute.

Comment on lines +65 to +70

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.

Claude here, replying on behalf of @oschwald.

This conflates AARs with APKs. AndroidManifest.xml is binary AXML inside an APK; inside an AAR it is plain text, because the AAR is a library intermediate that the consumer's build still has to merge and only then compile. Reading it straight out of the published artifact:

$ unzip -p device-sdk/build/outputs/aar/device-sdk-release.aar AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.maxmind.device" >

    <uses-sdk android:minSdkVersion="27" />

    <!-- Required permissions for device data collection -->
    <uses-permission android:name="android.permission.INTERNET" />
    ...

So the grep is reading text, not stumbling over binary, and the step passes against the real artifact — verified by running it verbatim.

Switching to aapt2 dump xmltree would mean putting the Android build-tools on PATH in this workflow and depending on an SDK component it does not currently need, to parse something that is already text. That is a net loss.

Keeping the current check.


# Direct witness for the pinned Kotlin module name. japicmp catches a
# change to it only while DeviceTracker still has internal members whose
# mangled names reach the ABI, so it would stop guarding this after an
# ordinary refactor.
unzip -p "$aar" classes.jar >"$tmp/classes.jar"
entries=$(unzip -l "$tmp/classes.jar")
if ! grep -q 'META-INF/device-sdk_release\.kotlin_module' <<<"$entries"; then
echo "::error::expected META-INF/device-sdk_release.kotlin_module in classes.jar"
grep -i 'kotlin_module' <<<"$entries" || true
exit 1
fi

# Consumer ProGuard rules have to actually ship.
unzip -p "$aar" proguard.txt >"$tmp/proguard.txt"
if [ ! -s "$tmp/proguard.txt" ]; then
echo "::error::proguard.txt is missing or empty in the AAR"
exit 1
fi
31 changes: 30 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ jobs:
# Use the MIT-licensed basic caching provider instead of the
# default enhanced caching, which is proprietary.
cache-provider: basic
# Checked in as a binary, so verify it against Gradle's published
# checksums. On by default, but stated explicitly so it cannot vanish
# in an action upgrade without a visible diff.
validate-wrappers: true

- name: Build SDK
run: ./gradlew :device-sdk:build
Expand All @@ -39,8 +43,33 @@ jobs:
- name: Build sample app
run: ./gradlew :sample:assembleDebug

# Nothing else compiles the androidTest sources, and a toolchain upgrade is
# the likeliest thing to break them. Cheap, so it runs before the R8 build.
- name: Compile instrumented tests
if: ${{ !cancelled() }}
run: ./gradlew :device-sdk:assembleDebugAndroidTest
Comment on lines +46 to +50

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.

minor — no if: always(), so an R8 failure hides this.

The two new steps close two independent verification gaps and neither depends on the other. On the exact scenario this commit is written for — a toolchain upgrade — an R8 failure means you never learn whether the androidTest sources still compile, so the upgrade gets fixed and re-pushed one failure at a time. Same serial-masking the existing steps have, but this commit is explicitly adding independent checks, so it's worth breaking the pattern. Consider putting the cheaper androidTest compile first, too.

Two related notes on what this step actually buys:

  • It's compile-only, and nothing anywhere ever runs the instrumented tests — no emulator job, no Gradle managed device, and dev-bin/release.sh doesn't run connectedAndroidTest either. That matters most for GpuCollector, which has no unit test at all (src/test/ has no GpuCollectorTest), so its only tests never execute and real-device EGL behaviour — including the repeated-call/leak case the test was written for — is entirely unverified. Either run them via a managed device, or reword the comment to say compile-only and add unit coverage for GpuCollector.
  • The packaged test APK may not contain its declared runner (pre-existing): testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" is declared, but androidx-test-runner is only wired to testImplementation via the android-testing bundle; the androidTest configuration has androidx-test-junit, androidx-test-core and mockk-android only. This step stays green regardless, so it gives more confidence than it earns. Adding androidTestImplementation(libs.androidx.test.runner) would make the APK match its declaration.

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.

Claude here, replying on behalf of @oschwald.

Fixed in 75d21ab. Both new steps now run under if: ${{ !cancelled() }}, and the cheap androidTest compile goes first as you suggested. Your framing was the convincing part: these two steps close independent gaps, so serialising them defeats the point on exactly the scenario they exist for.

The three related notes are real and none of them are this branch's doing — instrumented tests are compiled but never executed anywhere, GpuCollector has no unit test at all, and androidx-test-runner is wired only to testImplementation so the test APK does not contain its declared runner. All three are item 5 of STF-1190; the step's comment still says "compile", which is what it does.


# device-sdk's release build type and sample's debug build type are the only
# places isMinifyEnabled is set, both to false, so this is the only step
# that runs R8 over the SDK's classes at all. It does not reproduce the
# missing-class failure that motivates minCompileSdk: that needs a consumer
# compiling below 30, and the sample is at compileSdk 36. The floor itself
# is pinned by the metadata assertion in api-compat.yml.
#
# AGP also wires lintVitalRelease into the release assemble, so a failure
# here is not necessarily an R8 or keep-rule problem.
- name: Build minified sample app
if: ${{ !cancelled() }}
run: ./gradlew :sample:assembleRelease

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
Comment on lines 65 to 67

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.

minor — the artifact upload still collects only device-sdk/build/reports/tests/, so an R8 failure uploads nothing useful.

When :sample:assembleRelease fails, the artifacts that actually explain it — sample/build/outputs/mapping/release/missing_rules.txt, configuration.txt, mapping.txt, plus any lint report — are discarded with the runner. Diagnosis is then limited to whatever R8 truncated into the console log, which is the common failure mode for missing-class errors with long reference lists.

Extend path: to a multi-line list including sample/build/outputs/mapping/release/ and sample/build/reports/.

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.

Claude here, replying on behalf of @oschwald.

Fixed in 75d21ab — the upload now collects sample/build/outputs/mapping/release/ and sample/build/reports/ alongside the test reports. Missing-class errors are the specific case where the console log is truncated and missing_rules.txt is the file you actually need, so discarding it was the wrong default for a job that now runs R8.

name: test-results
path: device-sdk/build/reports/tests/
# R8 failures are explained by missing_rules.txt and configuration.txt,
# not by the test reports, and those are otherwise discarded with the
# runner.
path: |
device-sdk/build/reports/tests/
sample/build/outputs/mapping/release/
sample/build/reports/
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 0.4.0 (TBD)

- The published AAR now declares a `minCompileSdk` of 30. Consumers that minify,
or that set a Java 9 or later `sourceCompatibility`, already needed
`compileSdk` 30: `InstallationInfoHelper` references
`android.content.pm.InstallSourceInfo`, added in API 30, so R8 reported it as
a missing class, and the Android Gradle plugin separately refuses to configure
Java 9 or later compilation below 30. For consumers that do neither, this is a
new requirement rather than a pre-existing one. The API-30 call is guarded by
an `SDK_INT` check and is safe at runtime, so the floor trades an obscure R8
failure for a clear Gradle error. If your build fails with a `minCompileSdk`
error, raise `compileSdk` to 30 or later, or stay on 0.3.1. Note this is your
app's `compileSdk` only: the SDK still supports API 27 and later devices, and
`minSdk` is unchanged.
- The SDK is now built with Gradle 9.6.1 and the Android Gradle plugin 9.3.1.
The public API and the published ABI are unchanged, and the artifact still
imposes no Android Gradle plugin floor on consumers, so the toolchain change
itself requires no action from consumers.
Comment on lines +17 to +20

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.

nit — the 0.3.1 entry a few lines below now contradicts this release.

CHANGELOG.md:34 still states in the present tense that the published artifact "imposes no compileSdk or Android Gradle Plugin floor on consumers". As of the entry above, the AAR declares minCompileSdk=30, so the compileSdk half of that sentence no longer describes the current artifact. (The AGP half is still correct — minAndroidGradlePluginVersion=1.0.0 is what AGP 9.3.1 writes, verified.)

Consumers grep the changelog for compatibility statements, and two entries a dozen lines apart now say opposite things about whether a compileSdk floor exists. Scope the historical entry ("as of 0.3.1 the artifact imposed no…") or point it at the 0.4.0 bullet.

Separately: "The public API and the published ABI are unchanged" is asserted here but nothing in CI verifies the ABI claim at the Kotlin-metadata level — see the comment on libs.versions.toml.

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.

Claude here, replying on behalf of @oschwald.

Declining this one. The 0.3.1 entry describes 0.3.1, and it was accurate for that release — the artifact really did impose no compileSdk floor. Retrofitting shipped release notes so they account for a later release is a worse habit than the apparent inconsistency: it makes the changelog a rolling summary of the present rather than a record of what each version did, and it means a reader cannot trust that an old entry says what it said when it shipped.

The 0.4.0 entry twelve lines above states the floor plainly and now carries the remedy (ed1ab49), which is where a reader checking current compatibility should land.


## 0.3.1 (2026-07-24)

- Removed the unused `androidx.lifecycle:lifecycle-runtime-ktx` dependency. In
Expand Down
52 changes: 23 additions & 29 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Follow Kotlin conventions (kotlinlang.org/docs/coding-conventions.html):
# Build SDK library only (fastest for SDK development)
./gradlew :device-sdk:assemble

# Build debug variants (skips minification issues)
# Build debug variants (no minification, so faster)
./gradlew assembleDebug

# Build SDK library with all variants
Expand All @@ -46,14 +46,12 @@ Follow Kotlin conventions (kotlinlang.org/docs/coding-conventions.html):
### Testing

```bash
# Run unit tests for SDK
# 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 tests with coverage (JaCoCo)
./gradlew :device-sdk:testDebugUnitTest jacocoTestReport
```
Comment on lines 52 to 55

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.

nit — AGP 9 halved what this command runs, and that's recorded only in 4461a1b's commit message.

AGP 9 no longer registers testReleaseUnitTest, so ./gradlew :device-sdk:test and :device-sdk:build now run the suite once against debug instead of twice. SETUP.md presents the same commands unqualified, and dev-bin/release.sh gates the release on ./gradlew :device-sdk:test.

Practical risk is low — the SDK's release variant sets isMinifyEnabled = false and unit tests never see R8 — but the release gate is now weaker than the person running it believes, and there's no in-tree record that release-variant unit tests were dropped deliberately rather than lost by accident. A later reader diffing coverage against 0.3.1 has no way to tell. A one-line note here would do it; alternatively re-enable the release unit-test component explicitly via androidComponents { beforeVariants { … } } if that coverage is wanted.

Separately, CLAUDE.md:36 still annotates ./gradlew assembleDebug with "skips minification issues" — the same "minification is a known quirk, avoid it" framing this branch deliberately replaced 240 lines below. The two now contradict each other.

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.

Claude here, replying on behalf of @oschwald.

Added the note in 40d7e8a, right on the command:

# Run unit tests for SDK. Under AGP 9 this runs the suite once, against the
# debug variant: AGP 9 no longer registers testReleaseUnitTest.

Took the note rather than re-enabling the release unit-test component: with no src/release, isMinifyEnabled = false, both BuildConfig fields in defaultConfig and no variant-specific dependencies, testReleaseUnitTest was a duplicate run, and AGP unit tests never see R8 output regardless. Your underlying point stands though — the release gate is now weaker than the person running it believes, and asserting a non-zero test count is item 6 of STF-1190.

Also fixed CLAUDE.md:36's "skips minification issues" annotation in the same commit.


### Code Quality
Expand All @@ -66,7 +64,7 @@ Follow Kotlin conventions (kotlinlang.org/docs/coding-conventions.html):
./gradlew ktlintFormat

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

Expand Down Expand Up @@ -237,24 +235,22 @@ mise run setup # Accepts licenses, installs platform packages, creates loca

**Manual setup:**

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

```
org.gradle.java.home=/home/greg/.local/share/android-studio/jbr
```
1. Java 21. The version is pinned by `mise.toml`, not by the build scripts —
there is no `org.gradle.java.home` in `gradle.properties`. Without mise, put
a Java 21 JDK on `JAVA_HOME`.

2. Android SDK with API 36 at `~/Android/Sdk`
2. Android SDK with the platform and build-tools matching `compileSdk` in
`gradle/libs.versions.toml`

3. `local.properties` file (gitignored):
```properties
Comment on lines 245 to 246

@coderabbitai coderabbitai Bot Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a blank line before the nested code fence.

Markdownlint reports MD031 because the fence immediately follows the list item.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 246-246: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 245 - 246, Insert a blank line between the
`local.properties` list item and its nested code fence in `CLAUDE.md` to satisfy
Markdownlint MD031, leaving the properties example unchanged.

Source: Linters/SAST tools

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.

Claude here, replying on behalf of @oschwald.

Applied in 19b89c8, though for a different reason than the one given.

Markdownlint is not this repo's markdown standard. .precious.toml runs prettier --parser markdown --prose-wrap always over **/*.md, alongside ktlint for Kotlin and yamllint for YAML; there is no markdownlint anywhere in the repo or its pre-commit hooks. I checked both forms against prettier and it accepts each of them, so MD031 was not failing anything here. The violation is also pre-existing rather than introduced by this branch — those lines are context in the diff, not changes.

The reason worth acting on is internal consistency: SETUP.md already separates a numbered list item from its nested fence with a blank line in all three places it does this (lines 243, 251, 257), so CLAUDE.md was the only file doing it the other way. Fixed there, in its own commit, since it is formatting rather than the doc-accuracy work the surrounding commits are doing.

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.

Do you need to push that commit?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

sdk.dir=/home/greg/Android/Sdk
sdk.dir=/path/to/your/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`
- The project requires Java 17+; `mise.toml` pins Java 21
- If Gradle picks up a different JDK, check `mise current` and `JAVA_HOME`

## Common Issues

Expand All @@ -266,31 +262,29 @@ mise run setup # Accepts licenses, installs platform packages, creates loca
~/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:
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.

```bash
# Build debug variant instead
./gradlew :sample:assembleDebug
```
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.

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

When adding features, write unit tests that:

Expand All @@ -307,7 +301,7 @@ Uses Gradle version catalog in `gradle/libs.versions.toml`:
- `[plugins]` - Gradle plugins
- `[bundles]` - Grouped dependencies (e.g., `ktor`, `testing`)

Access in build files: `libs.ktor.client.core`, `libs.plugins.kotlin.android`
Access in build files: `libs.ktor.client.core`, `libs.plugins.android.library`

## Maven Publishing Configuration

Expand Down
25 changes: 25 additions & 0 deletions README.dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,28 @@ Review the versions from the dependency update check. If you want to update:
5. Merge

If you did this in the middle of releasing, start the release process over.

## Upgrading Gradle

`gradle/wrapper/gradle-wrapper.properties` pins `distributionSha256Sum`, so a
plain `./gradlew wrapper --gradle-version=X` fails on purpose rather than
silently dropping the checksum. Pass the new one instead:

```bash
# Checksum for the -bin distribution, from https://gradle.org/release-checksums/
# or https://services.gradle.org/distributions/gradle-X-bin.zip.sha256
# First pass: rewrites gradle-wrapper.properties
./gradlew wrapper --gradle-version=X --gradle-distribution-sha256-sum=<sha256>

# Second pass: regenerates gradlew, gradlew.bat and gradle-wrapper.jar using the
# new version's own templates
./gradlew wrapper --gradle-version=X --gradle-distribution-sha256-sum=<sha256>
```

Do not delete the checksum line to get past the failure. `retries` and
`networkTimeout` come from the `Wrapper` task configuration in the root
`build.gradle.kts`, so regeneration preserves them; only the checksum needs the
flag.

Note that the Android Gradle plugin constrains which Gradle versions work: AGP 9
requires Gradle 9.6 or later, and AGP 8.x cannot run on Gradle 9.6 at all.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Android SDK for collecting and reporting device data to MaxMind.

## Requirements

- Android API 27+ (Android 8.1+)
- Android API 27+ (Android 8.1+) on the device
- Your app must compile against `compileSdk` 30 or later, from 0.4.0 onwards
(the AAR declares `minCompileSdk = 30`)
- A recent stable version of Kotlin
- AndroidX libraries

Expand Down Expand Up @@ -189,7 +191,7 @@ To run the sample app:
### Generate Documentation

```bash
./gradlew :device-sdk:dokkaHtml
./gradlew :device-sdk:dokkaGenerate
```

Documentation will be generated in `device-sdk/build/dokka/`.
Expand Down
32 changes: 17 additions & 15 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,26 +93,25 @@ Replace `/path/to/your/Android/Sdk` with your actual SDK location:
./gradlew :sample:installDebug

# Generate documentation
./gradlew dokkaHtml
./gradlew :device-sdk:dokkaGenerate
```

## Publishing to Maven Central

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.

minor — same stale-Gradle-task bug class this commit set out to fix, five lines below this one, in a file the commit already had open.

SETUP.md:104 documents ./gradlew :device-sdk:publishReleasePublicationToMavenCentralRepository, and SETUP.md:110-112 documents signing.keyId / signing.password / signing.secretKeyRingFile in local.properties.

Publishing is done by the Vanniktech plugin: dev-bin/release.sh:173 and CLAUDE.md:91 both use :device-sdk:publishAndReleaseToMavenCentral, and device-sdk/build.gradle.kts:188-190 uses signing { useGpgCmd() }, so those keyring properties are never read. The Vanniktech Central Portal repository is also only configured when mavenCentralUsername is non-empty (device-sdk/build.gradle.kts:141-146), so the documented …ToMavenCentralRepository task doesn't exist for a developer following this file — they get a task-not-found error and configure signing properties that have no effect.

Point this section at README.dev.md / publishAndReleaseToMavenCentral, and drop the signing.* keyring block in favour of the gpg-command setup already documented in CLAUDE.md.

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.

Claude here, replying on behalf of @oschwald.

Fixed in 40d7e8a. Confirmed both halves before changing anything: the Vanniktech plugin's task is publishAndReleaseToMavenCentral, and signing { useGpgCmd() } means signing.keyId / secretKeyRingFile are never read — so a developer following that file got a task-not-found error and then configured properties with no effect. It now points at README.dev.md and documents the credentials that are actually used.

While in the file I also swept the same class of staleness in its Troubleshooting section, which told people to point JAVA_HOME at Android Studio's JDK; the version is pinned by mise.toml.


The SDK is configured for Maven Central publishing:
Releases are cut with `./dev-bin/release.sh`. See [README.dev.md](README.dev.md)
for the full process, including Central Portal credentials and GPG setup.

The underlying task, if you need it directly:

```bash
./gradlew :device-sdk:publishReleasePublicationToMavenCentralRepository
./gradlew :device-sdk:publishAndReleaseToMavenCentral
```

Required credentials (set in `local.properties` or environment variables):

```properties
signing.keyId=YOUR_KEY_ID
signing.password=YOUR_KEY_PASSWORD
signing.secretKeyRingFile=/path/to/secring.gpg
mavenCentralUsername=YOUR_USERNAME
mavenCentralPassword=YOUR_PASSWORD
```
Credentials come from `~/.m2/settings.xml` (server id `central`), or
`mavenCentralUsername` / `mavenCentralPassword` as Gradle properties or
`ORG_GRADLE_PROJECT_*` environment variables. Signing uses the system `gpg`
command via `signing { useGpgCmd() }`, so `signing.keyId` and
`signing.secretKeyRingFile` are not used.

## Environment Variables

Expand All @@ -139,12 +138,15 @@ $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses

### Wrong Java Version

The Java version is pinned in `mise.toml`. Check what is actually active:

```bash
# Use Android Studio's JDK
export JAVA_HOME=~/.local/share/android-studio/jbr
./gradlew build
mise current
java -version
```

Without mise, put a JDK matching `mise.toml` on `JAVA_HOME`.

### SDK Not Found

Ensure `local.properties` exists with correct SDK path:
Expand Down
13 changes: 12 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.dokka) apply false
alias(libs.plugins.detekt) apply false
Expand All @@ -18,3 +17,15 @@ allprojects {
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}

// gradle-wrapper.properties is generated, so any hand edit there is lost the next
// time someone runs `./gradlew wrapper`. Declaring the values here means a
// regeneration reproduces them. distributionSha256Sum is deliberately not set
// here: it changes with every Gradle version, and Gradle already refuses to
// regenerate the wrapper for a new version while it is present unless
// --gradle-distribution-sha256-sum is passed. See README.dev.md.
tasks.withType<Wrapper>().configureEach {
networkTimeout = 10000
retries = 2
retryBackOffMs = 500
}
Loading
Loading