android: the real client — backend, notifications, Live Update, Glyph
The app module was a design gallery: tokens, type and a dial, with no backend, no notification and nothing driving the rear matrix. This is the rest of it. **One engine, three views.** `TimerEngine` lives on the Application, not in a ViewModel, because the Compose screen, the foreground service's notification and the Glyph must show the *same* second. Two engines polling the same backend drift apart within seconds and the user sees a notification disagreeing with the dial. It polls, ticks from a monotonic anchor, and puts every reply through `:core`'s `reconcile`. `pomodoro.state()` does not finish an interval whose time is up — it reports `remaining_seconds = 0` and waits for a client to call `complete`. The tick loop does, behind the same re-entry guard `desktop/controller.py` uses, since without one a 1 Hz tick at zero fires `complete` every second until the reply lands. **Notifications.** Two channels, because the running interval must be silent and its *end* must not; one channel cannot be both, and a user who silences a buzzing countdown has silenced the only part that mattered. The ongoing one is an Android 16 Live Update: promotion needs `setOngoing` + `setRequestPromotedOngoing` + a `ProgressStyle`, and missing any one renders it as an ordinary notification with no error and no log. Its segments come from `:core`'s `dialTickCount`, so the status bar, the dial and the Glyph ring agree on what one tick is worth. **Glyph.** `GlyphMatrixManager.init()` binds `com.nothing.thirdparty/.GlyphService` and *discards* the boolean `bindService` returns — so on Android 11+ a missing `<queries>` entry makes the package invisible, the bind silently fail, and the matrix stay dark with no exception, no log and `onServiceConnected` never arriving. That was the whole of "the glyph does not work". The manifest now declares it and `MatrixGlyphDisplay` checks visibility itself rather than leaving the next person with the same dead end. The SDK's licence forbids redistribution (clause 2.1(c)), so the .aar stays git-ignored and `fetch-glyph-sdk.sh` downloads it. Without it a second source set compiles a no-op display: the app builds, installs and runs with an inert rear panel, which is also the correct behaviour on any phone that is not a Nothing. Verified on the device (Phone (3), Android 16), debug and release: onboarding against kadence.timka.su, the Live Update with `shortCriticalText` and a 600s progress track, the matrix accepting 625-dot frames, tasks opening in the Jira app, and R8 leaving the DTOs decodable. 225 tests, none needing a device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
32 files changed
+4118
-162
No files matched your search
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* What someone can type into the backend field, and what it has to become.
|
||||
*
|
||||
* `normalizeBaseUrl` is the one piece of input handling between a phone keyboard and
|
||||
* okhttp, and getting it wrong is indistinguishable from the backend being down. The
|
||||
* interesting rule is the scheme guess: a hostname gets HTTPS, but a LAN address gets
|
||||
* HTTP, because no certificate will ever cover `192.168.1.40` and defaulting it to
|
||||
* HTTPS turns "make up on my laptop and point the phone at it" into a TLS error.
|
||||
*/
|
||||
package tech.kadence.android
|
||||
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import tech.kadence.android.data.store.KadencePrefs.Companion.normalizeBaseUrl
|
||||
|
||||
class NormalizeBaseUrlTest {
|
||||
|
||||
@Test
|
||||
fun `an explicit scheme is left alone`() {
|
||||
assertEquals("https://kadence.timka.su", normalizeBaseUrl("https://kadence.timka.su"))
|
||||
assertEquals("http://192.168.1.40:8000", normalizeBaseUrl("http://192.168.1.40:8000"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a bare hostname gets https`() {
|
||||
assertEquals("https://kadence.timka.su", normalizeBaseUrl("kadence.timka.su"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `trailing slashes and spaces go`() {
|
||||
assertEquals("https://kadence.timka.su", normalizeBaseUrl(" kadence.timka.su/ "))
|
||||
assertEquals("https://kadence.timka.su", normalizeBaseUrl("https://kadence.timka.su///"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a LAN address gets http, because no certificate covers it`() {
|
||||
assertEquals("http://192.168.1.40:8000", normalizeBaseUrl("192.168.1.40:8000"))
|
||||
assertEquals("http://10.0.2.2:8000", normalizeBaseUrl("10.0.2.2:8000"))
|
||||
assertEquals("http://localhost:8000", normalizeBaseUrl("localhost:8000"))
|
||||
assertEquals("http://kadence.local", normalizeBaseUrl("kadence.local"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a path is preserved, because the backend may sit under one`() {
|
||||
assertEquals("https://example.com/kadence", normalizeBaseUrl("example.com/kadence"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty stays empty rather than becoming a scheme`() {
|
||||
assertEquals("", normalizeBaseUrl(""))
|
||||
assertEquals("", normalizeBaseUrl(" "))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user