Build log · Software & apps

DadFinder — a phone that rings at full volume when my dad presses one button

Software & appsdone4 versionsAndroid (no Gradle)Cloudflare Worker + DODND bypassIPv4 pinning9/9 self-checks

reflection not written yet · every fact below is generated from this project’s own spec and files

What it is

My dad needs to reach me and be certain he reached me, without learning anything new. He gets one button on a plain web page — no app, no link with a key in it, no QR. My phone rings at full volume even in silent mode or Do Not Disturb, shows a full-screen alarm, and his page tells him whether I stopped it. Shipping it meant fixing four real failures: a second device stealing the ring, an ‘online’ flag that stayed online, a press queued for three minutes that fired out of nowhere, and a line that would not turn on over mobile data because the app dialled an IPv6 address that never connected.

Version history

Straight from the project’s own card data — what each version added.

V0

Prove the phone can be made to scream: foreground service + full-screen intent + DND access, tested in an emulator with total silence set (zen=2). Log line that decided it: dnd filter was 3, lifted, then alarm volume 0 -> 7 / max 7.

V1

Make Dad's side one button and no key. The key is injected server-side so the address he opens is the bare domain, and the relay refuses rather than queues so nothing rings later out of nowhere.

V2

Survive being closed: stopWithTask=false, START_STICKY, a persisted JobScheduler watchdog and a boot receiver. kill -9 respawned the process in 3 s but not always the service, which is why the watchdog exists rather than a claim.

V3

Fix what the tests exposed: per-install client ids so no device can steal a ring, stage=off for instant offline, IPv4 pinned before any socket opens so mobile data arms the line, and shownAt retained across pause so one ring makes one alarm.

Files on the site

What broke, and what I’d change

Not written yet. This is the part only I can write — what actually went wrong, and what I’d do differently. It lives in builds/reflections/dadfinder.md (see _TEMPLATE.md); as soon as that file has words in it, this section fills in on the next build.

Everything else on this page — the spec, the version history, the file counts — is generated from the project’s own build notes and files, so nothing here is guesswork.

Build spec

The project’s own build notes, rendered. Open the raw .md ↗

Show the spec

Build spec — DadFinder emergency line

The ask

Isaac's dad needs to reach him and be sure he was reached. Dad must do nothing new: no app, no link, no QR, no access key. Isaac's phone rings at full volume even in silent mode / DND, and Dad sees confirmation rather than guessing.

Shape

  • One Cloudflare Worker (dadfinder.isaac1804.workers.dev) + one Durable Object hub holding the ring
  • state, plus the dad page served from the same worker.

  • One native Android app on Isaac's phone (no Gradle: shell + javac + aapt2 + apksigner) holding a
  • foreground service that long-polls the hub and a full-screen-intent alarm activity.

  • The website is the only surface Dad touches. The app is installed once, by Isaac.

Contract (asserted in the simulator's self-test)

  1. One ring id is offered to every connected phone, not consumed by the first poller.
  2. No phone listening → 409 refused:offline; never queue a ring that would fire minutes later.
  3. Turning the line off posts stage=off and the hub reports offline immediately.
  4. Rate limit: one press per 20 s per IP (429 too soon).
  5. The phone acknowledges stage=start when the alarm starts and stage=stop when it is closed.
  6. DND is lifted and the alarm stream is raised to max before the siren starts.

Failure modes found and fixed

  • Ring stolen by a second device. The hub marked a ring consumed on first delivery, so a test
  • emulator swallowed the ring meant for Isaac's phone. Fixed with per-install client ids and a deliveredTo list.

  • "Online" was sticky. Offline only appeared after a 90 s poll timeout, and any device kept it warm.
  • Fixed with an explicit stage=off ack.

  • Queued surprise ring. A press with no phone listening was parked for 3 minutes and fired when the
  • line came back. Now refused outright.

  • Mobile data could not arm the line. The app dialled the IPv6 address of the worker and died there
  • (ConnectException … 2606:4700:…:443); carriers hand out an IPv6 route that does not work. Fixed by pinning IPv4 before any socket opens.

  • Double alarm. One ring produced two alarm screens; RingActivity.onPause() re-posted the
  • full-screen intent. Fixed by retaining shownAt across pause/resume.

Verified

  • Emulator: DND total silence (zen=2) → dnd filter was 3, lifted, alarm volume 0 -> 7 / max 7,
  • siren playing, ack stop -> 200.

  • Killed process (kill -9): START_STICKY respawn in 3 s; a persisted JobScheduler watchdog restores the
  • service within 15 min when it does not.

  • Two pollers, one ring: both receive the same id.
  • Simulator: 9/9 checks, zero network requests, no console errors, no horizontal overflow at 390/768/1440.

Not built

  • No iOS app. No SMS fallback. The watchdog cannot beat a force-stop; the card says so.