Target API 36, Compose UI, modernised dependencies, screenshot tests - #27
Open
cyberb wants to merge 1 commit into
Open
Target API 36, Compose UI, modernised dependencies, screenshot tests#27cyberb wants to merge 1 commit into
cyberb wants to merge 1 commit into
Conversation
cyberb
force-pushed
the
api-36-and-dependency-upgrade
branch
from
August 31, 2026 19:31
b5dd403 to
8cce969
Compare
cyberb
force-pushed
the
api-36-and-dependency-upgrade
branch
11 times, most recently
from
August 31, 2026 21:06
37c582e to
fa9f300
Compare
cyberb
force-pushed
the
api-36-and-dependency-upgrade
branch
5 times, most recently
from
September 4, 2026 08:52
87b2c2d to
516f115
Compare
Google Play requires new apps and updates to target API 36 from
31 Aug 2026, so no further release could be published while the app
targeted 35. Existing installs and the listing were never at risk —
the API 35 target already met the bar for staying visible to new
users — but the next update of any kind needed this first.
compileSdk was declared inside defaultConfig, where Groovy's
owner-first closure resolution quietly applied it to the android
extension instead. The effect was a build compiling against API 33
while targeting 35. It is now set explicitly, at 37, with targetSdk
left at 36 because that is what Play requires; compiling against a
newer API than you target does not opt the app into its behaviour.
The applicationVariants block is gone. It existed only to rename the
output APK through the legacy variant API that AGP 9 removes, and the
replacement there is a PackageApplication hook or an artifact
transform, which is a lot of machinery for a filename. Gradle emits
its default name and the collect step renames once, reading
versionName out of build.gradle, so the pipeline still publishes
syncloud-<version>.apk. Removing it is what allowed AGP 9, which in
turn unpinned androidx.core, okhttp and the compose BOM; all three had
been held a release back because their -android artifacts require
AGP 9.1 or compileSdk 37.
AGP 9 compiles Kotlin itself and rejects an explicit
org.jetbrains.kotlin.android plugin, so that is removed and the
toolchain and compiler options move to the top level kotlin
extension. The module DSL uses explicit assignment throughout.
multiDexEnabled is dropped: minSdk is 23 and multidex has been native
since 21, so the flag did nothing.
Dependencies, removed:
log4j 1.2.17 + de.mindpipe android-logging-log4j 1.0.3
Unmaintained since 2015 and 2011. The appender only ever wrote to
logcat, so an android.util.Log wrapper keeping the
Logger.getLogger/info/error shape replaces them with no new
dependency. ACRA still captures logcat, so crash reports are
unaffected. Tags are truncated to 23 chars, the platform limit.
guava 27.0.1
Three uses of Maps.newHashMap, Lists.newArrayList and
Sets.newHashSet. Kotlin stdlib covers all three.
commons-lang3
No references anywhere in the source.
androidx.legacy:legacy-support-v4
Was transitively supplying androidx.swiperefreshlayout, which the
device screens used; Compose replaces it outright.
materialloadingprogressbar 0.5.8
Abandoned in 2016 and JCenter-era.
jackson-module-kotlin is pinned to 2.19.4, not latest. From 2.20.0 the
module references java.lang.invoke.MethodHandle, which D8 cannot
desugar below API 26 and which fails dexing at minSdk 23. Raising
minSdk to 26 would have fixed it too, but that drops Android 6.0 and
7.x devices and is not part of this change.
The UI is rewritten in Jetpack Compose with Material 3, removing every
XML layout, both ArrayAdapters, both dialog classes, the
PreferenceFragmentCompat settings screen, the options menu and all
findViewById calls, and with them appcompat, material views,
swiperefreshlayout and preference-ktx. Activities extend
ComponentActivity and go edge to edge. Settings previously reached
SharedPreferences through androidx.preference; with that gone the app
opens the same file directly, "<package>_preferences", which is the
name PreferenceManager used. Anything else would strand existing
users' saved credentials and server choice.
The mDNS resolver moves off NsdManager.resolveService and
NsdServiceInfo.host, deprecated in API 34 and hard errors at
compileSdk 36 under allWarningsAsErrors. API 34 and above use
registerServiceInfoCallback with hostAddresses, and the old path stays
behind a version check down to minSdk 23. The callback is unregistered
once a service resolves, since unlike ResolveListener it otherwise
keeps delivering updates. Resolver's queue was racy — checkQueue was
synchronized but endResolving wrote isBusy outside the lock, and a
plain LinkedList was reached from callback threads — and now uses an
AtomicBoolean guard and a ConcurrentLinkedQueue.
The manifest declared org.acra.CrashReportDialog, a class that has not
existed since ACRA 4; the real one is org.acra.dialog.CrashReportDialog
and ACRA's own manifest already contributes it, so DialogTheme never
reached the crash dialog.
Testing has two halves. Screenshots of all six screens render under
Robolectric with Roborazzi, on the JVM inside the existing gradle test
step, and land on the artifact server next to the APK. CI runs
testDebugUnitTest rather than test, because the compose rule launches
ComponentActivity from ui-test-manifest, which belongs on
debugImplementation and so does not exist in the release variant.
Discovery is tested for real. The platform image runs as a drone
service in full systemd mode with avahi advertising, redroid runs
Android 14 as a sibling container, and an instrumented test drives
DiscoveryManager through the platform's own NsdManager. redroid rather
than an emulator because the emulator puts the guest behind qemu user
mode NAT, which does not carry multicast, so mDNS could never reach
it; redroid runs on the host kernel with ordinary docker networking.
It needs binder_linux, which ships with the 6.8 kernel and is loaded
and mounted on the build host. Both services need privileged, so the
repo must stay trusted in drone.
Device side discovery is unchanged: rootfs still advertises _ssh._tcp
named "syncloud on %h" and the app still browses that.
Separately, the pipeline had no trigger block, so drone built every
event and a pull request produced a second identical build alongside
the branch push. Restricting to push covers branches and master; tag
has to stay because the github-release step is gated on it, and a
trigger without it would silently end APK publishing.
cyberb
force-pushed
the
api-36-and-dependency-upgrade
branch
from
September 4, 2026 09:09
516f115 to
01422f2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Google Play requires new apps and updates to target API 36 from 31 Aug 2026. The listing and existing installs were never at risk (targeting 35 already met the bar for staying visible to new users), but no further release could be published without this. CI is green — build 117.
Build
compileSdkwas declared insidedefaultConfig, where Groovy's owner-first closure resolution silently applied it to theandroidextension — so the build compiled against API 33 while targeting 35. Now explicit.Stayed on AGP 8.x deliberately: AGP 9 removes the
applicationVariantsAPI this build uses to name the output APK. That choice costs exactly three dependency versions, all held one release back —core-ktx 1.18.0(1.19.0 wants AGP 9.1/compileSdk 37),okhttp 5.4.0(5.5.0 wants compileSdk 37), andcompose-bom 2026.06.01(2026.08.00 pins the compose 1.12.0 train, whose-androidartifacts want AGP 9.1/compileSdk 37). All three move together whenever AGP 9 lands.multiDexEnableddropped — minSdk is 23, multidex native since 21, no multidex artifact ever declared.UI rewritten in Compose + Material 3
All five screens. Removed: every XML layout, both
ArrayAdapters, both dialog classes,PreferenceFragmentCompat, the options menu, allfindViewById, and theappcompat/material/swiperefreshlayout/preference-ktxdependencies. Activities extendComponentActivityand go edge-to-edge.SwipeRefreshLayout→PullToRefreshBox,CircleProgressBar→CircularProgressIndicator,ListView+adapter →LazyColumn+ListItem, preferences XML → a plain Compose screen (it holds four entries).Migration detail: dropping
androidx.preferenceremovedPreferenceManager.getDefaultSharedPreferences, so the app now opens"<package>_preferences"directly — the exact filename PreferenceManager used. Any other name would strand existing users' saved credentials and server choice.Dependencies removed
android.util.Logwrapper keeping theLogger.getLogger/info/errorshape replaces both. ACRA still captures logcat, so crash reports are unaffected.Maps.newHashMap/Lists.newArrayList/Sets.newHashSet, all covered by Kotlin stdlib.androidx.swiperefreshlayout; now irrelevant since Compose replaced it.Upgraded: ACRA 5.13.1, Jackson 2.19.4, coroutines 1.11.0, junit 4.13.2, mockk 1.14.11.
Jackson is pinned to 2.19.4, not latest. From 2.20.0 the module references
java.lang.invoke.MethodHandle, which D8 cannot desugar below API 26 and which fails dexing at minSdk 23. Raising minSdk to 26 would also have fixed it, but that drops Android 6.0/7.x users.mDNS discovery
NsdManager.resolveServiceandNsdServiceInfo.hostwere deprecated in API 34 — invisible at compileSdk 33, hard errors at 36 underallWarningsAsErrors. API 34+ now usesregisterServiceInfoCallback+hostAddresses, with the old path behind a version check down to minSdk 23. The callback is unregistered on first resolve, since unlikeResolveListenerit otherwise keeps delivering updates.Also fixed a race:
checkQueue()was@SynchronizedbutendResolving()wroteisBusyoutside the lock, and a bareLinkedListwas reached from callback threads. NowAtomicBoolean+ConcurrentLinkedQueue.Device-side discovery is unchanged —
rootfsstill advertises_ssh._tcpnamedsyncloud on %h, and the app still browses that. A dedicated_syncloud._tcptype would be correct but needs a rootfs change plus indefinite dual-browsing for already-deployed devices.Screenshot tests
Six screens captured via Robolectric + Roborazzi, rendering real composables on the JVM inside the existing gradle test step — no emulator, no QEMU:
A
collectstep gathers these plus the APK and the HTML test report intoartifact/, which the existing scp step ships to/home/artifact/repo/android/<build>. It runswhen: status: [failure, success], so a red build still uploads whatever rendered.CI runs
testDebugUnitTestrather thantest: the compose rule launchesComponentActivityfromui-test-manifest, which lives ondebugImplementationso it never reaches a shipped APK, andtestReleaseUnitTesttherefore cannot resolve it.Composables carry
testTagidentifiers throughout, so tests select by tag rather than text or position.Note: Roborazzi runs in record mode — every run writes fresh PNGs, it does not diff against a baseline. Switching to comparison later is a one-line change once you're happy with how the screens look.
Duplicate CI builds
The pipeline had no
triggerblock, so Drone built every event and each PR produced two identical builds (103/104 were the same commit). Nowevent: ["push", "tag"].tagmust stay — thepublish to githubstep is gated onwhen: event: [tag], and a push-only trigger would silently end APK publishing.Not verified by CI
The
CircularProgressIndicatorandPullToRefreshBoxbehaviour, the Bridge-free theme, and real mDNS discovery against a device all need a look on an actual phone before release.