Skip to content

build: move the app to AGP 8 / Gradle 8 and AndroidX#8

Merged
xroche merged 6 commits into
masterfrom
phase3-agp8
Jul 16, 2026
Merged

build: move the app to AGP 8 / Gradle 8 and AndroidX#8
xroche merged 6 commits into
masterfrom
phase3-agp8

Conversation

@xroche

@xroche xroche commented Jul 16, 2026

Copy link
Copy Markdown
Owner

The app half of the modernization. The native layer was already done; this makes Gradle and the Java side build under a current toolchain, with no runtime change at all.

AGP moves 2.3.2 to 8.6.0, Gradle 3.3 to 8.7, compileSdk 11 to 35, minSdk 9 to 24, and support-v4 to androidx.core/androidx.fragment. The dead jcenter() gives way to google()/mavenCentral() in settings.gradle. AGP 8.6 is the floor for compileSdk 35, and that pins Gradle 8.7.

targetSdk deliberately stays at master's 25, because 25 is the only value that changes nothing at runtime. I had it at 28 first, on the reasoning that anything below 29 keeps the legacy storage the File-based crawl needs. That reasoning was incomplete: 28 also crosses API 26, where a NotificationChannel becomes mandatory, and this app creates none. Every notification would have been dropped by the system with only a log warning, across four call sites including "mirror finished". Nothing user-visible, nothing a build catches. Notification channels belong with the targetSdk 35 move next phase, where an emulator can verify them.

./gradlew could not run on a clean checkout at all before this. .gitignore had /gradle, a typo for the /.gradle already on the line above, and it matched the wrapper directory, so gradle-wrapper.jar was never committed. The jar is now tracked, and the distribution is pinned by sha256.

Three things only broke once the toolchain was current. ButtonStyle set parent="@android:attr/buttonStyle", which aapt2 rejects outright because an attr is not a style, so the module cannot build under AGP 8 until it goes. aapt1 did accept it and did resolve the parent, but the only thing it inherited was the attr bag's reserved type key, which no widget ever queries, so it contributed nothing. Buttons already inherit the theme's buttonStyle as their default style, so dropping the parent leaves the appearance exactly as it has always been. OptionsActivity builds its tabs from @Fields({R.id.x}) annotations, which need R fields to be constants, hence android.nonFinalResIds=false. And unserialize(getParcelableExtra(...)) turned ambiguous, because Java 8+ can infer T as File & Parcelable and match the unserialize(File) overload too.

One deviation from the plan worth checking in review: it said to rename the FileProvider FILE_PROVIDER_PATHS meta-data to an androidx key, but androidx.core.content.FileProvider still reads android.support.FILE_PROVIDER_PATHS. I checked that against the compiled 1.13.1 artifact. Renaming it would have broken sharing at runtime with nothing in the build to catch it, so the key is unchanged.

./gradlew clean assembleDebug assembleRelease lint is green against a local SDK 35 / NDK r26d install, and both APKs carry arm64-v8a and x86_64 only, targetSdkVersion 25, versionCode 63. AGP 8's packaging flip is fine here: the .so entries are stored uncompressed with extractNativeLibs=false, which needs API 23+ and minSdk is 24, and they come out 16 KB page aligned. Depending on modern androidx.core also pulls profileinstaller in, so the merged manifest gains androidx.startup.InitializationProvider and a ProfileInstallReceiver gated behind the DUMP permission; worth an ack rather than a surprise.

Lint is non-fatal (abortOnError false) and reports 13 pre-existing errors, mostly the OptionsActivity$*Tab manifest entries that are registered as activities but extend Tab; that wants its own PR. Nothing was installed on a device: there is no KVM access on this machine, so the emulator smoke job still waits for Phase 4.

xroche and others added 6 commits July 16, 2026 17:58
The .gitignore `/gradle` entry (a typo for `/.gradle`, already listed on the
line above) matched the whole gradle/ directory, so gradle-wrapper.jar was
never committed and ./gradlew could not run on a clean checkout at all — only
gradle-wrapper.properties had been force-added. Narrow the ignore and commit
the jar, generated by the 8.7 distribution after checking it against the
published sha256.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Replaces the 2017 build: jcenter() (dead) gives way to google()/mavenCentral()
in settings.gradle, AGP 2.3.2 to 8.6.0 via the plugins DSL, and compileSdk 11 to
35. AGP 8.6 is the floor for compileSdk 35, and it pairs with Gradle 8.7.
minSdk is 24, matching the API 24 the native layer already requires for
fseeko/nl_langinfo. targetSdk stays at 28 so the File-based crawl keeps legacy
storage; moving it to 35 belongs with the scoped-storage rework.

support-v4 is gone in favour of androidx.core/androidx.fragment. The
FileProvider meta-data key stays `android.support.FILE_PROVIDER_PATHS`:
androidx.core.content.FileProvider still reads that exact name, so renaming it
would break sharing at runtime. AndroidX drags in kotlin-stdlib 1.8.22 while
coroutines pins the jdk7/jdk8 shims at 1.6.21, whose classes 1.8.22 absorbed;
the Kotlin BOM aligns them and settles the duplicate-class clash.

Three things only failed once the toolchain was modern:

  - ButtonStyle set its parent to @android:attr/buttonStyle. An attr is not a
    style, so aapt1 quietly resolved it to nothing; aapt2 rejects the file.
    Buttons already inherit the theme's buttonStyle as their default style, so
    dropping the parent keeps the appearance the app has always had.
  - OptionsActivity drives its tabs off @fields({R.id.x}) annotations, which
    need R fields to be constants. AGP 8 made them non-final by default, hence
    android.nonFinalResIds=false.
  - unserialize(getParcelableExtra(...)) turned ambiguous: Java 8+ can infer T
    as File & Parcelable, matching unserialize(File) too. A Parcelable local
    pins it.

versionCode/versionName move from the manifest to defaultConfig, and the dead
HelpActivity entry (no such class) goes. proguard-rules.txt never existed and
was inert only because minifyEnabled is false; it is now a real file keeping the
jni package, which native code resolves reflectively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Turns the documented assemble stub into a real job. It runs in the GHCR image,
which already carries JDK 17, SDK 35 and NDK r26d, and stages the OpenSSL
statics and libiconv with the same two scripts the native job uses — AGP drives
ndk-build, so those inputs must exist before ./gradlew. Keeping the staging in
CI rather than a preBuild hook leaves Gradle offline-clean for local builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Review follow-ups. The comment on the unserialize fix said the opposite of what
the code does ("left inline" describing an extracted local); a dropped "if"
inverted it, and it is the one comment a future reader would misread.

gradle-wrapper.properties now pins distributionSha256Sum, so the 130 MB
distribution is checked rather than trusted on TLS alone; the wrapper jar was
already verified against the published checksum.

The rest trims comments back to the repo's one-line default, dropping the
roadmap narration on targetSdk and the restated mechanism on the keep rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
targetSdk 28 was chosen because anything below 29 keeps the legacy storage the
File-based crawl needs. That reasoning missed the other line 28 crosses: at API
26 a NotificationChannel becomes mandatory, and this app creates none, so the
system would drop every notification with nothing but a log warning. Four call
sites, "mirror finished" among them, and no build or lint check sees it.

28 buys this phase nothing that 25 does not, and only 25 leaves runtime
behaviour untouched. Channels belong with the targetSdk 35 move, where the
emulator job can prove they work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The assemble job went red with wget exit 4 after stalling two minutes on
ftp.gnu.org. Both jobs that need libiconv fetch it from that one host on every
run, with no retry and no timeout, so whenever it is slow or down the build
fails for reasons that have nothing to do with the change under test.

Try the GNU redirector first, then ftp.gnu.org, then the kernel.org mirror, and
let wget retry with a timeout. All three were checked to serve a tarball whose
sha256 matches the pinned value; that existing gate is what makes reading from a
mirror safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche merged commit 90eecbd into master Jul 16, 2026
5 checks passed
@xroche
xroche deleted the phase3-agp8 branch July 16, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant