Make per-item scheduling work on Android 7 instead of blanking the screen

ScheduleEval uses java.time — Instant, LocalDate, ZoneId — which is API 26. minSdk is 24, and core
library desugaring was never enabled, so on Android 7.0/7.1 the first evaluation threw
NoClassDefFoundError. Those API levels are still common on cheap signage sticks and older TV boxes.

The damage was much worse than a failed check, because NoClassDefFoundError is an Error, not an
Exception. The evaluator's deliberate fail-open guard — written so that "a blank screen is worse
than an over-running promo" — did not catch it. The Error propagated out of scheduleAllows, through
firstActiveIndex and updatePlaylist, past another catch(Exception), and was only swallowed at the
service boundary. Because updatePlaylist aborted before the download block, no content was fetched
either; and on a cold start from cache the same Error reached a handler that clears the playlist
cache. So the moment anyone used dayparting or expiry, those panels sat on "waiting for content"
with nothing downloaded and nothing cached, and a reboot did not help. The stated contract was
inverted on exactly the hardware it was meant to protect.

Two changes. Desugaring is the real fix: java.time now exists on API 24/25, so the code runs as
written. The guard is widened to Throwable as well, so this class of failure can never again slip
past a catch that was written to be total — that is belt and braces, not the fix.

Release build assembles cleanly with desugaring on; 134 Android JVM tests green. Still to confirm on
a real API 24/25 image before release — the unit tests run on the JVM, where java.time always
exists, which is precisely why this was invisible to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
Claude 2026-07-30 21:20:41 -05:00
parent f4d309a0d4
commit d978a5d2a6
2 changed files with 15 additions and 1 deletions

View file

@ -43,6 +43,14 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
// ScheduleEval uses java.time (Instant/LocalDate/ZoneId), which is API 26 — but minSdk is
// 24. Without desugaring, per-item dayparting/expiry threw NoClassDefFoundError on Android
// 7.0/7.1, which are still common on cheap signage sticks and older TV boxes. Because that
// is an Error and not an Exception, the evaluator's deliberate fail-open guard did not
// catch it: the playlist update aborted before content downloaded, and the cold-start path
// then cleared the playlist cache — so the screen sat on "waiting for content" and a reboot
// did not help.
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@ -63,6 +71,7 @@ android {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// AndroidX
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")

View file

@ -43,7 +43,12 @@ object ScheduleEval {
val nowMin = zdt.hour * 60 + zdt.minute
val date = zdt.toLocalDate()
blocks.any { blockMatches(it, dow, nowMin, date) }
} catch (e: Exception) {
} catch (e: Throwable) {
// Throwable, not Exception. A missing java.time on an old API level surfaces as
// NoClassDefFoundError — an Error — which sailed straight through a catch(Exception)
// and turned this "fail open, a blank screen is worse than an over-running promo"
// contract into its exact opposite: nothing played at all. Desugaring (see
// build.gradle.kts) is the real fix; this makes the guard mean what it says.
true // fail open
}
}