Skip to content

Android 5.1 and modern Java

The Moto X runs Android 5.1, API level 22. Four version concepts must remain separate:

  1. Gradle JDK runs Gradle and the Android Gradle Plugin on the development computer.
  2. Java source compatibility controls Java language syntax accepted in app source.
  3. Java library availability controls classes and methods available at runtime; core-library desugaring backports a documented subset.
  4. Android API level controls framework APIs such as Activities, Bluetooth, storage, and notification behavior.

This project uses a current Android Gradle Plugin, Java 17 source and target compatibility, core-library desugaring, compileSdk = 36, and minSdk = 22.

Modern syntax is not the same as an unrestricted Java 17 runtime. Lambdas, method references, try-with-resources, and many newer language constructs can be transformed by D8/R8. Supported java.time, collection, stream, and other library APIs can be supplied by desugar_jdk_libs. Arbitrary JDK modules, desktop APIs, reflection behavior, and every post-Java-8 API are not thereby available.

Likewise, compiling against API 36 permits references to new Android APIs but does not put them on Android 5.1. Calls newer than API 22 require an API-level guard and a working fallback, or they must not be used.

Practical rules:

  • let Android lint inspect every change;
  • keep API-22 tests and a physical-device smoke test in the release path;
  • prefer platform APIs and small dependencies;
  • check every dependency's minimum SDK before adding it;
  • do not raise minSdk merely to accommodate a convenience library;
  • do not use hidden/private Android APIs.

Dashboard HTTPS trust

The dashboard transport keeps the platform's system root certificates and supplements them with the bundled ISRG Root X1 and ISRG Root YR certificates. It filters out user-installed CAs, applies the resulting socket factory only to connections for the configured API origin, and retains normal certificate chain and hostname verification. It does not pin the renewable server leaf or replace the process-wide TLS defaults. The DER resources are SHA-256 checked at startup against fingerprints in TrustAnchorSet; the fingerprints and validity periods were checked against Let's Encrypt's Chains of Trust and official certificate downloads on 2026-10-01; the local DER fingerprints match those official downloads. The self-signed X1 and YR certificates have notAfter dates 2035-06-04 and 2045-09-02, respectively. These certificate dates differ from trust-store policy: Let's Encrypt currently lists X1 trust until 2030-06-04 and YR as not yet included in root-program trust stores. The app adds both roots for its configured API transport. This is an app-scoped compatibility path; installing a root in Android's user certificate store is not required by the app.

The API-22 acceptance run on 2026-10-01 passed 31 instrumented tests on the XT1058. Its OriginTrust cases verify a successful request to the configured API, the API-22 platform-default failure, continued trust of another system root, and rejection of wrong-host, expired, self-signed, and untrusted-root certificates. Certificate-renewal acceptance, modern-device transport, and a real production-feed request remain follow-up checks.

Centering against asymmetric system-bar insets

A deployment check on the Moto X Play XT1563 running LineageOS 17.1 / Android 10 (API 29) found a modern-device layout issue:

  • the window frame covers the full 1920×1080 display;
  • Android reports a 144-pixel right navigation-bar content inset;
  • the root application View is consequently 1776×1080; and
  • centering within that View puts the clock about 72 pixels left of the physical panel center.

The unused strip is black and blends into the clock background, so this is a minor visual defect rather than a functional failure.

Do not fix it by assuming that every Android 10 device has a 144-pixel inset. Navigation mode, ROM, orientation, density, and which side owns the navigation bar can all change the value. For a right inset \(I_{\text{right}}\) and left inset \(I_{\text{left}}\), the horizontal error is

\[ x_{\text{correction}}=\frac{I_{\text{right}}-I_{\text{left}}}{2}, \]

which is 72 pixels for the measured device. The app does not compute this from WindowInsets; it measures the resulting geometry instead.

Implemented approach

CenteredFrameController (package display.frame) observes the root View's layout changes and system UI visibility changes. Each time it reads three measurements: the root View's size, its getLocationOnScreen origin, and the real panel size from Display.getRealSize (API 17). The pure CenteredContentFrame.compute function then returns the largest rectangle inside the root that is symmetric about the physical display center on each axis. With root origin \(x_0\), root width \(w\), and display width \(W\), the display center in root coordinates is \(c=W/2-x_0\), and the frame spans

\[ [\,c-h,\;c+h\,],\qquad h=\min(c,\;w-c). \]

For the Moto X Play, \(c=960\), \(w=1776\), \(h=816\), giving the span \([144,1776)\); a left-side inset mirrors it, and an unobstructed root yields the full width. The vertical axis uses the same rule. Degenerate geometry (zero sizes, or a root that does not contain the display center) falls back to the full root rectangle.

The two clock faces live in a content_frame FrameLayout; the controller writes the frame's margins, and only when they change, so the extra layout pass settles immediately. The faces keep their own burn-in reserved margins inside the frame, and the control overlay and Preview panel stay outside it.

Why measuring avoids double-application

The rectangle depends only on where the root actually sits on the panel, never on a previously applied correction. Feeding the frame back in as a root returns the same center, which the unit tests assert. A rectangle that is already symmetric is returned unchanged.

Verification status

  • Unit tests (CenteredContentFrameTest) cover the Moto X Play right inset, the mirrored left inset, equal and zero insets, status-bar and bottom insets, odd sizes, degenerate input, and idempotence. They prove the geometry only; they do not exercise Views or the Android window system.
  • Moto X XT1058 (Android 5.1): the root fills the display, so the frame equals the root. A launch screenshot with the controller attached is byte-identical to one with it disabled, and no layout loop appears in logcat. The "Performed 6 layouts in a row" WindowManager message at launch also appears without the controller and is not caused by it.
  • Moto X Play XT1563 (LineageOS 17.1 / Android 10, API 29): the physical DisplayFrameAcceptanceTest passed on 2 October 2026. With system bars exposed, it measured a 1920×1080 panel, a 1776×1080 root, and a frame at x=144 with width 1632; the frame center is x=960. Restoring immersive mode returned both root and frame to 1920×1080 without a second correction. The full 33-test instrumented suite passed. The clock artwork intentionally moves within the frame for burn-in protection (an anchor up to about four percent of display width plus a smaller periodic shift), so a single screenshot does not measure the frame's center.

Remaining acceptance checks:

  • physical left-side navigation placement is not available on this ROM; mirrored geometry and idempotence remain covered by JVM tests;
  • longer-running theme, burn-in, control-overlay, and Preview visual checks remain part of the broader bedside soak.

Android 10 also enforces a cleartext policy that Android 5.1 did not apply to the device-local HTTP test fixture. The debug variant now allows cleartext only for 127.0.0.1 through a debug-only network security configuration. The app's origin policy still rejects other HTTP hosts, and the release manifest contains no cleartext exception. Production cards continue to use HTTPS.