some backend fixes and add android app

This commit is contained in:
timka123 committed 2026-10-19 14:52:20 +03:00
1 parent 772fdbfded
commit 1e55144291
69 files changed
+7111 -7

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
# Kadence for Android
The fourth client, for the Nothing Phone (3). Same backend, same editorial design, plus
the three things only a native phone client can do: enforce the focus guard on the phone
itself, surface the running interval as an Android 16 Live Update, and draw the timer on
the rear Glyph Matrix.
## Two modules, and why
| Module | What it is | Needs the Android SDK? |
| --- | --- | --- |
| **`core`** | Plain Kotlin on the JVM: the matchers, the policy clock, the timer maths, the outbox reconciliation, the Glyph frame renderer, the design tokens | **No** |
| **`app`** | The Android client: Compose UI, services, Room, the HTTP client, the Glyph toy | Yes |
Everything with interesting behaviour lives in `core`, deliberately. The result is that
the hard parts — offline reconciliation, glob and mask matching, the blocking policy,
even the 25×25 rear display — are **testable on a laptop with nothing but a JDK**, with
no emulator, no SDK and no phone in the loop:
```bash
make android-test # both modules: 219 tests, no device, no backend
make android-test-core # just the pure-logic suite: needs only a JDK
```
`settings.gradle.kts` only includes `:app` when an SDK is actually present, so that
command works on a machine that has never seen the Android tooling.
## Running the tests
```
./gradlew :core:test :app:testDebugUnitTest
```
**219 tests, none of which need a device or a running backend.** The backend lives only
on the dev machine, so the suite feeds on committed JSON fixtures under
`app/src/test/resources/fixtures/` and a `MockWebServer`; there is no step that curls a
live server.
`:core` (186 tests):
| Suite | What it pins down |
| --- | --- |
| `GlobTest` | Python `fnmatch` parity — expectations generated from CPython, because the block lists are shared with the backend |
| `BlockerTest` | `blocker_service.py` parity: `expand_entry`, allow-list subtraction, the policy decision and its reason strings, schedule windows incl. midnight wrap |
| `DomainMatcherTest` / `AppMatcherTest` | hostname and app matching, allow-list precedence, and the masks a hosts file cannot express |
| `NormalizeHostTest` | what a browser URL bar actually hands over: bare hosts, full URLs, search phrases, `chrome://`, punycode |
| `ReconcileTest` | one test per row of the conflict matrix — another device taking over, our own replay landing, view-only mode |
| `OutboxCoalesceTest` / `PostconditionTest` / `PartitionForReplayTest` | the offline queue: coalescing, the at-most-once probes, and quarantining intervals that cannot be filed honestly |
| `TickMathTest` / `DialGeometryTest` | the countdown anchor and the dial's tick rule |
| `Glyph*Test` | the rear matrix, asserted as ASCII art |
| `TokenParityTest` | reads `frontend/src/tokens.ts` and `lib/viz.ts` and fails if a single colour channel drifts |
| `AccentTest` | the system-accent policy and its contrast guard |
`:app` (33 tests, on the JVM via Robolectric):
| Suite | What it pins down |
| --- | --- |
| `DtoToleranceTest` | every endpoint's fixture decodes — then decodes again with keys stripped, unknown keys added and nulls where values are declared. The backend has no response models, so this is the contract |
| `KadenceApiTest` | `X-API-Token`, the session cookie, 401 routing (and *not* routing on the login path), both FastAPI `detail` shapes, a 422 rendered as `field: message`, an unreachable server as a typed error |
| `ThemeTest` | the palette actually reaching a composable, and that the serif clock is genuinely fixed-advance |
## Toolchain
Gradle 9.6 (via the wrapper) on **JDK 21** — newer JDKs are ahead of what AGP
supports:
```bash
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
```
For the `app` module you also need an Android SDK. AGP 9.4 with Compose 1.12 wants
`compileSdk 37`, which is published under Android's new minor-version scheme as
`platforms;android-37.0` (plain `android-37` does not exist). `targetSdk` stays 36,
because Android 16 is what the phone runs. Point the build at the SDK with
`ANDROID_HOME`, or `android/local.properties`:
```
sdk.dir=/home/you/Android/Sdk
```
## The Glyph Matrix SDK
`glyph-matrix-sdk-2.0.aar` is **not on Maven** — it has to be downloaded from
[Nothing's developer kit](https://github.com/Nothing-Developer-Programme/GlyphMatrix-Developer-Kit)
and dropped into `glyph/libs/`. It is git-ignored, and the Glyph code sits behind an
interface with a no-op implementation, so a missing `.aar` disables the rear display and
breaks nothing else. The renderer itself is in `core` and needs none of this to be
tested.
## Design notes
* **Fraunces has no tabular figures.** Its digits are proportional — `1` is 1024 units
against `0` at 1461 — and the font ships no `tnum` feature, so the web app's global
`font-variant-numeric: tabular-nums` does nothing for the serif dial clock. Here the
clock goes through `TabularFigures`, which lays every glyph in a box as wide as the
widest digit; `ThemeTest` measures it. Inter *does* ship `tnum`, so the sans numbers
are fixed with a font feature instead.
* **The canvas is Kadence's, the accent is the phone's.** Warm paper, ink type, hairline
rules and Fraunces figures are fixed; the single accent comes from Android's dynamic
palette when available, falling back to the brand ember. `onAccentArgb()` picks the
label colour by WCAG contrast, because a wallpaper-derived accent can be any lightness
— that guard is what keeps "use the system colour" from producing unreadable buttons.
* **Chart colours never follow the accent.** They come from the validated, colour-blind
safe palette in `viz.ts`. A chart has to stay readable; that is a property of the
palette, not of the wallpaper.
* **No `VpnService`, ever.** Android allows exactly one, and it belongs to the user's
always-on VPN. Website blocking reads the browser URL bar through an accessibility
service instead; app blocking polls `UsageStatsManager`. Neither touches the VPN slot.
* **The server owns the clock.** The phone polls, ticks locally from a monotonic anchor,
and re-anchors on every reply. Offline writes go to an outbox and the server's answer
always wins the final word.
## Verifying on the device
See the plan at `~/.claude/plans/make-an-app-for-logical-russell.md` for the full manual
script. The short version, once the SDK is installed:
```bash
make up # backend + web UI on :8000
ip -4 addr show | grep inet # find the LAN address for the phone
make android-install # ./gradlew :app:installDebug
adb logcat -s Kadence:*
```
Note that an always-on VPN can swallow a LAN address; `adb shell curl -s
http://<lan-ip>:8000/api/health` is the first thing to check if onboarding cannot reach
the backend.