Commit Graph
5 Commits
Author SHA1 Message Date
timka123andClaude Opus 5 7b7b50bc8e blockers: the report's cooldown was deciding whether to block
Both clients made the same mistake, in the same shape: the throttle that keeps
the daily report from filling with one row per poll was placed in front of the
enforcement call. So blocking worked exactly once per target, and the second
launch of a banned app — or the second visit to a banned site — simply went
through.

Android, `BlockerEngine`: `claim()` now gates only `report()`/`announce()`.
Enforcement runs on every decision behind a 1.2s debounce whose only job is to
stop the accessibility event and the usage-stats poll both pressing home for one
visit. An app still in front on the next pass is pressed again — one
GLOBAL_ACTION_HOME is a request, not a result — and a visit is no longer
consumed when no accessibility service is attached to consume it with, which
turned a missing grant into a silently unblocked app. `lastForeground` and
`lastBlockedApp` are @Volatile like every other field here; they are written
from the service's main thread and the poll coroutine both. The usage-stats
cadence now follows the server's `app_poll_seconds`, and the first look happens
at once rather than four seconds in.

Linux, `BlockerManager._enforce_apps`: the pid went into the dedupe set *before*
the kill was attempted, so a process whose signal was refused was exempt for the
rest of the interval, and a pid is reused — a relaunch landing on a recycled
number inherited the exemption. Processes are now identified by (pid, start
time) from /proc/<pid>/stat, the kill is attempted on every pass, and only the
report is throttled. The books are pruned of dead processes so an all-day
interval cannot leak.

`processes.terminate()` no longer busy-waits two seconds for the process to die
before escalating — on the desktop that wait ran on the thread drawing the
countdown. SIGTERM now, SIGKILL on a later pass once the grace has gone by,
which is also gentler than the old fixed two seconds. With nothing blocking,
the scan gets its own timer at the cadence the server asks for instead of
riding the five-second policy poll.

Neither enforcement path had any test: the desktop's blocker suite hardcoded an
empty app list and nothing anywhere touched BlockerEngine. 16 new tests, on
fakes and the committed fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-22 17:16:56 +03:00
timka123 50e9c68b2f android: the focus guard on the phone itself
The Guard screen shows the shared policy; two Android facilities enforce it.
An accessibility service reads the browser's URL bar (no /etc/hosts exists,
and the VPN slot belongs to the user's always-on VPN), sends blocked pages
home and records the attempt; UsageStatsManager spots blocked apps in front
and the service presses home — 'kill' cannot mean force-stop, and the KDoc
says so.

Masks mean what they mean on every client: a bare '*' in the expansion list
makes '*.site' cover every subdomain, and masks no hosts file can express
are matched as ordinary globs against every URL, so they work here even
where the desktop cannot route them. Offline, the kept blocker settings are
re-evaluated with :core's evaluatePolicy, so losing signal does not lose the
guard. Two limits are documented rather than papered over: in-app WebViews
are invisible to the URL-bar reader, and blocking degrades to reporting
without the service.

Core keeps the policy/matcher semantics testable on a plain JVM: 190 core
and 44 app tests. The screen's copy says junk labels are ignored, a '*' is
kept, and unroutable masks are enforced on this phone only.
2026-10-19 18:45:50 +03:00
timka123andClaude Opus 5 535830bacc android: two reasons the Glyph stayed dark
Both silent, and each alone was enough to leave the matrix blank while
everything else in the app worked.

**1. `setAppMatrixFrame` does nothing on this firmware.** The developer kit is
explicit that app-driven control must use the `App` variants, so that is what
the first cut used. On Phone (3) build 2608141733 the call is accepted without
an exception and then dropped: it forwards to `IGlyphService.setAppMatrixColors`
and the system service never reaches the hardware. Measured over 20 seconds of
1 Hz pushes: `setAppMatrixFrame` produced 0 writes to the matrix light,
`setMatrixFrame` produced 232. Both paths now use the plain variant. The kit's
warning that it can fight a selected Glyph Toy is real, but a rear display that
never lights is the worse failure.

**2. `getDeviceMatrixLength()` returns the side, not the dot count.** The name
invites the opposite reading, and the SDK settles it: `DEVICE_23112_MATRIX_LENGTH`
is 25 and `DEVICE_23112_SIZE` is that squared. Taking a square root of 25 gave a
5x5 frame, so the renderer drew a few stray dots in one corner and `GlyphFrame.set`
silently discarded everything outside the grid — 7 lit dots instead of 78.

Also: `clear()` blanks by drawing an empty frame rather than calling
`closeAppMatrix()`, which was closing a channel we no longer open and so left
the last frame lit.

Verified on the device: 78 of 625 dots for a running interval, 234 hardware
writes in 20s, the frame rendering as minutes over seconds inside a draining
ring. 225 tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-19 16:16:30 +03:00
timka123andClaude Opus 5 a7148b69c6 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>
2026-10-19 16:05:13 +03:00
timka123 1e55144291 some backend fixes and add android app 2026-10-19 14:52:20 +03:00