Android: modernize build stack (AGP 8.7 LTS, targetSdk 35, 16 KB page alignment, RxJava → Coroutines) #791

Merged
ab300819 merged 7 commits from android/modernization-2026 into main 2026-09-07 15:18:19 -05:00
ab300819 commented 2026-08-03 11:24:45 -05:00 (Migrated from github.com)

Implements the plan from #752. Every change is confined to android/ — no files outside that directory are touched.

Why

Two of these are hard Play Store blockers:

  • targetSdk 35 — required for all new submissions and updates since Aug 2025
  • 16 KB ELF page alignment — required by May 31, 2026 for apps shipping native .so files on Android 15+

The rest is the dependency chain needed to get there (AGP 8+ requires JDK 17 and namespace; Moshi codegen needs Kotlin 1.9; NDK r28 is the first release whose CMake toolchain understands ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES), plus removing RxJava 2.x, which has been EOL since 2021.

Commits

Each commit builds and is independently reviewable. Reviewing commit-by-commit is probably easier than reading the squashed diff.

Commit Scope
Phase 0: baseline hygiene jcenter() → mavenCentral(), drop lifecycle-extensions:2.2.0, -Xuse-experimental → -opt-in, kotlin-stdlib-jdk7 → kotlin-stdlib, disable Jetifier
Phase 1a: AGP 8.7 + Kotlin 1.9 + JDK 17 + namespace + NDK pinning Gradle 7.6 → 8.11.1, AGP 7.4.1 → 8.7.3, Kotlin 1.8.0 → 1.9.24, JDK 8 → 17, add namespace, plugins {} DSL, pin NDK, Moshi 1.14.0 → 1.15.2, two native-code fixes
Phase 1c: targetSdk 35 + edge-to-edge + Predictive Back compileSdk/targetSdk 34 → 35, WindowInsetsControllerCompat, OnBackPressedDispatcher, IntentCompat
Phase 3: migrate RxJava2 to Coroutines/Flow Room DAOs → Flow/suspend, BehaviorSubject → MutableStateFlow, CompositeDisposable → viewModelScope/lifecycleScope, delete RxLiveData.kt, AndroidX bumps
Phase 2: version catalog + kapt → KSP add gradle/libs.versions.toml, kapt → KSP 1.9.24-1.0.20, Room schema export
Phase 4: minimal test scaffolding 16 JVM unit tests (MacAddress parsing, Moshi adapter, ControllerState merge)
Phase 1b: 16 KB page alignment + Oboe 1.10.0 ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON + explicit max-page-size/common-page-size linker flags, Oboe 1.9.3 → 1.10.0

Commit order differs from the numbering in #752 (the numbering was assigned before the work was done); dependency order between commits is correct as committed.

Version changes

Before After
Gradle 7.6 8.11.1
AGP 7.4.1 8.7.3
Kotlin 1.8.0 1.9.24
JDK target 1.8 17
compileSdk / targetSdk 34 35
minSdk 24 24 (unchanged)
NDK not pinned 28.2.13676358 (r28c)
Annotation processing kapt KSP 1.9.24-1.0.20
Oboe 1.9.3 1.10.0
Room 2.5.0 2.6.1
Moshi 1.14.0 1.15.2
Lifecycle 2.5.1 2.8.7
AppCompat 1.6.0 1.7.0
core-ktx 1.9.0 1.13.1
Material 1.8.0 1.12.0
ConstraintLayout 2.1.4 2.2.0
RecyclerView 1.2.1 1.3.2
Preference 1.2.0 preference-ktx 1.2.1
RxJava / RxKotlin / RxAndroid 2.2.20 / 2.4.0 / 2.1.1 removed
Coroutines — 1.8.1

Per CONTRIBUTOR_GUIDE.md, dependency versions were checked in all three places: neither the NDK nor the Oboe version appears in the root CMakeLists.txt, scripts/, or .github/workflows/ — both are declared only under android/.

Notes on #752

1. No changes needed outside android/. The original issue asked whether to propagate linker flags into the ExternalProject_Add builds (Option A/B/C). That turned out to be unnecessary: OpenSSL, json-c, and miniupnpc are all built statically under CHIAKI_ENABLE_ANDROID=ON and linked into libchiaki-jni.so. The APK ships only three shared libraries per ABI. No coordination on cmake/ is required, and none of the three options was needed.

2. NDK is pinned to r28c (28.2.13676358), matching what #752 proposed. r28 is the first release whose CMake toolchain understands ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES, and its Clang 19 is what the native-code fixes in Phase 1a are for.

On the 32-bit question raised in #752 — the answer there was right, with a caveat worth stating precisely, since alignment turns out to depend on where each .so comes from rather than on the ABI alone. See the table under Verification.

Verification

./gradlew testDebugUnitTest testReleaseUnitTest --rerun-tasks — 16 tests per variant, 0 failures, 0 errors.

./gradlew assembleRelease — BUILD SUCCESSFUL, 0 compiler errors, all four ABIs.

Page alignment of every shared library in the release APK — all 12, every LOAD segment (llvm-readelf -l):

lib/arm64-v8a/libc++_shared.so     LOAD x4   align 0x4000
lib/arm64-v8a/libchiaki-jni.so     LOAD x3   align 0x4000
lib/arm64-v8a/liboboe.so           LOAD x3   align 0x4000
lib/x86_64/libc++_shared.so        LOAD x4   align 0x4000
lib/x86_64/libchiaki-jni.so        LOAD x3   align 0x4000
lib/x86_64/liboboe.so              LOAD x3   align 0x4000
lib/armeabi-v7a/libc++_shared.so   LOAD x4   align 0x1000
lib/armeabi-v7a/libchiaki-jni.so   LOAD x3   align 0x4000
lib/armeabi-v7a/liboboe.so         LOAD x3   align 0x4000
lib/x86/libc++_shared.so           LOAD x4   align 0x1000
lib/x86/libchiaki-jni.so           LOAD x3   align 0x4000
lib/x86/liboboe.so                 LOAD x3   align 0x4000

Alignment follows the origin of each library, not the ABI:

Library Origin 32-bit 64-bit
libchiaki-jni.so built here; governed by the linker flags this PR adds 0x4000 0x4000
liboboe.so prebuilt in the Oboe AAR — 1.10.0 ships 16 KB aligned, which is why this PR bumps it 0x4000 0x4000
libc++_shared.so prebuilt in the NDK (ANDROID_STL=c++_shared); outside this project's control 0x1000 0x4000

All six 64-bit libraries are 16 KB aligned, which is what the Play requirement covers — 16 KB page devices are arm64-only, so 32-bit code never loads on them. The NDK itself follows this policy: it ships its 64-bit prebuilts 16 KB aligned and leaves the 32-bit ones at 4 KB.

An earlier revision of this description claimed every library in the APK was 16 KB aligned, and listed 8 of the 12 rows. That was wrong on both counts; the two 0x1000 rows above are the ones it omitted. The conclusion is unchanged, but the reason is narrower than stated: the linker flags only affect libchiaki-jni.so, and the other two libraries arrive already aligned (Oboe) or partly aligned (NDK).

Dependency health:

$ ./gradlew :app:dependencies | grep -iE 'rxjava|reactivestreams|jcenter|lifecycle-extensions|support-v4'
(empty)

Note: this repository has no Android CI workflow, so none of this is verified automatically — the above was run locally (macOS arm64, JDK 17.0.16, NDK r28c, CMake 3.22.1). Happy to add an android job to build-pr.yaml as a follow-up PR if you want coverage.

What's NOT changing

  • No Compose migration — Views + ViewBinding are fine for 6 Activities
  • No multi-module split — 45 Kotlin files + JNI, no reuse case
  • No Hilt/Dagger — manual ViewModel factories are ~5 lines each
  • No AGP 9 / Kotlin 2 — AGP 8.7 LTS is supported through 2026; avoids NDK/prefab/parcelize unknowns
  • No ABI drops — all four ABIs retained
  • Nothing outside android/

Notes for review

This is one PR rather than the series described in #752. Every commit is scoped to one concern and builds on its own, so it can be reviewed as a chain. If you'd rather have separate PRs, say so and I'll split it — the commits are already partitioned along those lines.

Phase 3 (RxJava removal) is the largest commit and touches every ViewModel, but the changes are mechanical type substitutions with no logic changes. The one place worth a careful look is the JNI boundary: Session.eventCallback and the DiscoveryService hosts callback fire from native threads. MutableStateFlow.value = is thread-safe, so StateFlow replaces BehaviorSubject without adding a main-thread bounce; MutableLiveData.postValue is retained where it was already used from native callbacks.

Closes #752

Implements the plan from #752. Every change is confined to `android/` — no files outside that directory are touched. ## Why Two of these are hard Play Store blockers: - **targetSdk 35** — required for all new submissions and updates since Aug 2025 - **16 KB ELF page alignment** — required by **May 31, 2026** for apps shipping native `.so` files on Android 15+ The rest is the dependency chain needed to get there (AGP 8+ requires JDK 17 and `namespace`; Moshi codegen needs Kotlin 1.9; NDK r28 is the first release whose CMake toolchain understands `ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES`), plus removing RxJava 2.x, which has been EOL since 2021. ## Commits Each commit builds and is independently reviewable. Reviewing commit-by-commit is probably easier than reading the squashed diff. | Commit | Scope | |---|---| | `Phase 0: baseline hygiene` | `jcenter()` → `mavenCentral()`, drop `lifecycle-extensions:2.2.0`, `-Xuse-experimental` → `-opt-in`, `kotlin-stdlib-jdk7` → `kotlin-stdlib`, disable Jetifier | | `Phase 1a: AGP 8.7 + Kotlin 1.9 + JDK 17 + namespace + NDK pinning` | Gradle 7.6 → 8.11.1, AGP 7.4.1 → 8.7.3, Kotlin 1.8.0 → 1.9.24, JDK 8 → 17, add `namespace`, `plugins {}` DSL, pin NDK, Moshi 1.14.0 → 1.15.2, two native-code fixes | | `Phase 1c: targetSdk 35 + edge-to-edge + Predictive Back` | compileSdk/targetSdk 34 → 35, `WindowInsetsControllerCompat`, `OnBackPressedDispatcher`, `IntentCompat` | | `Phase 3: migrate RxJava2 to Coroutines/Flow` | Room DAOs → `Flow`/`suspend`, `BehaviorSubject` → `MutableStateFlow`, `CompositeDisposable` → `viewModelScope`/`lifecycleScope`, delete `RxLiveData.kt`, AndroidX bumps | | `Phase 2: version catalog + kapt → KSP` | add `gradle/libs.versions.toml`, kapt → KSP 1.9.24-1.0.20, Room schema export | | `Phase 4: minimal test scaffolding` | 16 JVM unit tests (MacAddress parsing, Moshi adapter, ControllerState merge) | | `Phase 1b: 16 KB page alignment + Oboe 1.10.0` | `ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON` + explicit `max-page-size`/`common-page-size` linker flags, Oboe 1.9.3 → 1.10.0 | Commit order differs from the numbering in #752 (the numbering was assigned before the work was done); dependency order between commits is correct as committed. ## Version changes | | Before | After | |---|---|---| | Gradle | 7.6 | 8.11.1 | | AGP | 7.4.1 | 8.7.3 | | Kotlin | 1.8.0 | 1.9.24 | | JDK target | 1.8 | 17 | | compileSdk / targetSdk | 34 | 35 | | minSdk | 24 | 24 (unchanged) | | NDK | not pinned | 28.2.13676358 (r28c) | | Annotation processing | kapt | KSP 1.9.24-1.0.20 | | Oboe | 1.9.3 | 1.10.0 | | Room | 2.5.0 | 2.6.1 | | Moshi | 1.14.0 | 1.15.2 | | Lifecycle | 2.5.1 | 2.8.7 | | AppCompat | 1.6.0 | 1.7.0 | | core-ktx | 1.9.0 | 1.13.1 | | Material | 1.8.0 | 1.12.0 | | ConstraintLayout | 2.1.4 | 2.2.0 | | RecyclerView | 1.2.1 | 1.3.2 | | Preference | 1.2.0 | preference-ktx 1.2.1 | | RxJava / RxKotlin / RxAndroid | 2.2.20 / 2.4.0 / 2.1.1 | removed | | Coroutines | — | 1.8.1 | Per `CONTRIBUTOR_GUIDE.md`, dependency versions were checked in all three places: neither the NDK nor the Oboe version appears in the root `CMakeLists.txt`, `scripts/`, or `.github/workflows/` — both are declared only under `android/`. ## Notes on #752 **1. No changes needed outside `android/`.** The original issue asked whether to propagate linker flags into the `ExternalProject_Add` builds (Option A/B/C). That turned out to be unnecessary: OpenSSL, json-c, and miniupnpc are all built **statically** under `CHIAKI_ENABLE_ANDROID=ON` and linked into `libchiaki-jni.so`. The APK ships only three shared libraries per ABI. No coordination on `cmake/` is required, and none of the three options was needed. **2. NDK is pinned to r28c** (`28.2.13676358`), matching what #752 proposed. r28 is the first release whose CMake toolchain understands `ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES`, and its Clang 19 is what the native-code fixes in Phase 1a are for. On the 32-bit question raised in #752 — the answer there was right, with a caveat worth stating precisely, since alignment turns out to depend on where each `.so` comes from rather than on the ABI alone. See the table under Verification. ## Verification `./gradlew testDebugUnitTest testReleaseUnitTest --rerun-tasks` — 16 tests per variant, 0 failures, 0 errors. `./gradlew assembleRelease` — BUILD SUCCESSFUL, 0 compiler errors, all four ABIs. Page alignment of every shared library in the release APK — all 12, every LOAD segment (`llvm-readelf -l`): ``` lib/arm64-v8a/libc++_shared.so LOAD x4 align 0x4000 lib/arm64-v8a/libchiaki-jni.so LOAD x3 align 0x4000 lib/arm64-v8a/liboboe.so LOAD x3 align 0x4000 lib/x86_64/libc++_shared.so LOAD x4 align 0x4000 lib/x86_64/libchiaki-jni.so LOAD x3 align 0x4000 lib/x86_64/liboboe.so LOAD x3 align 0x4000 lib/armeabi-v7a/libc++_shared.so LOAD x4 align 0x1000 lib/armeabi-v7a/libchiaki-jni.so LOAD x3 align 0x4000 lib/armeabi-v7a/liboboe.so LOAD x3 align 0x4000 lib/x86/libc++_shared.so LOAD x4 align 0x1000 lib/x86/libchiaki-jni.so LOAD x3 align 0x4000 lib/x86/liboboe.so LOAD x3 align 0x4000 ``` Alignment follows the origin of each library, not the ABI: | Library | Origin | 32-bit | 64-bit | |---|---|---|---| | `libchiaki-jni.so` | built here; governed by the linker flags this PR adds | `0x4000` | `0x4000` | | `liboboe.so` | prebuilt in the Oboe AAR — 1.10.0 ships 16 KB aligned, which is why this PR bumps it | `0x4000` | `0x4000` | | `libc++_shared.so` | prebuilt in the NDK (`ANDROID_STL=c++_shared`); outside this project's control | `0x1000` | `0x4000` | **All six 64-bit libraries are 16 KB aligned, which is what the Play requirement covers** — 16 KB page devices are arm64-only, so 32-bit code never loads on them. The NDK itself follows this policy: it ships its 64-bit prebuilts 16 KB aligned and leaves the 32-bit ones at 4 KB. An earlier revision of this description claimed every library in the APK was 16 KB aligned, and listed 8 of the 12 rows. That was wrong on both counts; the two `0x1000` rows above are the ones it omitted. The conclusion is unchanged, but the reason is narrower than stated: the linker flags only affect `libchiaki-jni.so`, and the other two libraries arrive already aligned (Oboe) or partly aligned (NDK). Dependency health: ``` $ ./gradlew :app:dependencies | grep -iE 'rxjava|reactivestreams|jcenter|lifecycle-extensions|support-v4' (empty) ``` Note: this repository has no Android CI workflow, so none of this is verified automatically — the above was run locally (macOS arm64, JDK 17.0.16, NDK r28c, CMake 3.22.1). Happy to add an `android` job to `build-pr.yaml` as a follow-up PR if you want coverage. ## What's NOT changing - **No Compose migration** — Views + ViewBinding are fine for 6 Activities - **No multi-module split** — 45 Kotlin files + JNI, no reuse case - **No Hilt/Dagger** — manual ViewModel factories are ~5 lines each - **No AGP 9 / Kotlin 2** — AGP 8.7 LTS is supported through 2026; avoids NDK/prefab/parcelize unknowns - **No ABI drops** — all four ABIs retained - **Nothing outside `android/`** ## Notes for review This is one PR rather than the series described in #752. Every commit is scoped to one concern and builds on its own, so it can be reviewed as a chain. If you'd rather have separate PRs, say so and I'll split it — the commits are already partitioned along those lines. Phase 3 (RxJava removal) is the largest commit and touches every ViewModel, but the changes are mechanical type substitutions with no logic changes. The one place worth a careful look is the JNI boundary: `Session.eventCallback` and the `DiscoveryService` hosts callback fire from native threads. `MutableStateFlow.value =` is thread-safe, so `StateFlow` replaces `BehaviorSubject` without adding a main-thread bounce; `MutableLiveData.postValue` is retained where it was already used from native callbacks. Closes #752
streetpea commented 2026-09-07 15:17:40 -05:00 (Migrated from github.com)

there is no android version of chiaki-ng but I'll merge this for others that are looking at it...

there is no android version of chiaki-ng but I'll merge this for others that are looking at it...
Sign in to join this conversation.
No description provided.