Android app
The APX Android app is a native shell around the daemon’s phone surface. It keeps the existing React mobile UI as the single interface implementation, while Android owns the floating mascot, system notifications, and app launch.
The app opens /mobile, which the panel redirects to /m/chat — the phone
surface’s current address, where it sits alongside /m/tasks and
/m/commitments behind a bottom tab bar.
Pair once
Section titled “Pair once”- Make the daemon reachable from the phone, over LAN or an ADB reverse port.
- Run
apx pair webon the daemon machine. - Enter the daemon URL and printed pairing code in APX Android.
- Grant the notification permission when Android asks.
- Grant Display over other apps only when the floating mascot is wanted.
The app receives its own kind: android client token. It does not reuse the
token stored by Chrome or an installed PWA because Android isolates each app’s
storage. Existing web pairing remains valid; both clients appear in
apx pair list and can be revoked independently.
One-time car setup
Section titled “One-time car setup”Android does not let an app approve notification access or Do Not Disturb access for itself. A new installation therefore needs two explicit system switches for the complete car experience:
| Capability | One-time action | Required for |
|---|---|---|
| APX notifications | Accept Android’s notification prompt | Every APX notification |
| Driving alerts | APX menu → Allow driving alerts, then enable APX in the system screen | Heads-up cards while Android Auto enables Do Not Disturb |
| Trip detection | APX menu → Enable trip detection, then enable APX notification access | Reading Google Maps navigation state, and detecting an Android Auto session |
| Location | Choose While using the app | Supplying the trip origin and the live position during a trip |
| Overlay | Enable Display over other apps | Floating mascot only; not needed for car cards |
| Battery | APX menu → Remove battery restriction, then confirm Android’s dialog | Trip start and end arriving on time while the phone is in a pocket |
In Android Auto settings, keep Show message notifications enabled. Enable Show first line of messages when the card should expose APX text instead of only announcing a new message.
Native behavior
Section titled “Native behavior”- Normal launch opens
<daemon>/mobileinside the app, not Chrome. - The mascot floats above the launcher and other apps, can be dragged, and remembers its position.
- Its avatar follows Settings → Super-agent → Avatar. The authenticated WebSocket sends the current value on connect and every later change; Android stores it locally and repaints the overlay immediately.
- It hides while the APX app itself is open and returns when APX goes to the background.
- Inbound APX events trigger the same short message bubble as the desktop mascot, the bundled message sound, and an Android notification.
- Tapping the mascot or notification opens the native app at
/mobile. - The persistent mascot notification includes a Hide action.
- An app-only ellipsis beside Preferences opens native options. Chrome and installed PWAs do not show it.
- Native options can mute/unmute message sounds without disabling the mascot.
- Preferences → Notifications reports Android’s real permission state. Tap it to open the app’s system notification settings.
- The native menu can enable APX trip detection. After notification-listener
access is granted, the app detects Google Maps navigation start and end from
notification metadata, then shows a native car banner and a shortcut back to
Maps. A new trip posts an authenticated
trip.startedevent to/api/mobility/events, with the latest available location as origin. When Maps omits destination, an Android Auto card asks where the user is going; Reply accepts voice dictation and completes the same trip. Changing the destination starts a new evaluation. It uploads no route, location history, or complete notification content. - The trip banner opens the trip’s errands, not Maps: tapping it shows one card
per pending errand, nearest first, carrying the same four chips the Telegram
card offers — Navegar, Sumar a la ruta, Voy, En la siguiente. A “voy” pressed here
reaches the same record a “voy” pressed in Telegram does, and earns the same
follow-up after the trip; a card already answered shows the answer instead of
re-offering the question. When an errand still has several possible shops the
card says so, rather than presenting the nearest one as a decision APX has
made. Its right-hand label carries the live count (“8 cerca”). The list comes
from
GET /api/mobility/trip, which is read-only — asking never fires a reminder — and answering goes toPOST /api/mobility/errands/answer. Capped at eight errands. - Trip messages are intentionally rate-limited. A known destination must remain stable for 45 seconds; navigation without destination waits 10 minutes. Reopening the same destination stays silent for 30 minutes, and changing to a different destination cannot trigger another Roby evaluation for 5 minutes. Rapid cancel/restart tests update local state but produce no message burst. APX sends trip context to the daemon as soon as it detects it; these delays gate evaluation and alerts, not Roby’s awareness.
- As an explicit fallback, choose Share trip progress → More → Share trip
with APX in Maps on the phone. APX accepts Google Maps links only, attaches
the permitted current location, and updates the active trip. Current Maps
live-progress shares commonly contain only an opaque
maps.app.goo.gllink, not destination text; APX must label those as shared progress and must not invent a destination. Android Auto may expose contacts only and hide app share targets; use the phone or APX voice reply in that case. - The daemon asks Roby to inspect pending tasks and commitments with read-only
tools. User-configured trip events run immediately outside proactive rounds
and interruption budgets, but they update Roby’s mobility context instead of
forcing a Telegram notice. Roby sees current trip, last mobility question,
and the user’s last button response in later chats and secretary rounds.
It sends only a new concrete opportunity;
SILENTis the default when nothing useful changed. Roby must not claim proximity or a detour without enough data. - When origin and destination are available, the daemon geocodes with Nominatim, computes the route with Valhalla/OSRM, and finds OpenStreetMap shops within 700 m. It sends at most three matches and Google Maps links; a verified route with no matches stays silent.
- Telegram offers Yes, now, I cannot, Remind me later, and No more today. Remind later records a pending delivery for a later round. Closing Maps before delivery cancels the pending notice.
- Android Auto is a second trip source, independent of Maps. The same
notification listener recognises Android Auto’s own session notification
(“Android Auto · Connected to the car”) and opens a trip the moment the phone
projects to a head unit, with or without a route loaded. The banner then reads
You are in the car. Announcements (“tap to set up Android Auto”) and the
developer head-unit-server notice are never treated as a session. A trip ends
only when BOTH sources are gone: a route that finishes with the phone still
plugged in keeps the same trip and the same
trip_id. - While a trip is active, APX reports position to
/api/mobility/positionsevery 30 s / 150 m from a foreground service typedlocation. That service is why APX does not ask forACCESS_BACKGROUND_LOCATION: the ongoing “Trip in progress” notification is the permission model, and tracking stops with the trip. Each sample is one point plus accuracy; no route geometry and no location history is uploaded or stored. - The daemon matches each position against pending tasks that name a physical errand and alerts once when a matching place is within 2 km. One alert per errand per trip — a “buy ibuprofen” task that matches thirteen pharmacies gets the nearest one, not thirteen cards — and at most two cards per sample. It never repeats, even after a daemon restart.
- A proximity card carries four chips: Navigate and Add stop (Google Maps deep links; “Add stop” keeps the current destination and inserts the place as a waypoint), plus I’ll go and Tell me at the next one. Answering “I’ll go” is a promise, not an action: when the trip ends APX asks Did you make it to that place? with Done — which closes the task — and Not yet, which leaves it open.
- “Tell me at the next one” declines the shop, not the errand. Waving past the branch you happen to be driving by leaves the task owed another reminder, so the next shop that satisfies it can still speak up later in the same trip — it just never asks about the branch you already declined. When there is no next shop it goes quiet for the rest of the trip, which is what the old “Not today” did to the whole errand on the first press.
- While a trip is active, every automatic Telegram reply goes out twice: a
voice note first, then the same words as text under a quiet
[Transcript]line — a label, not a banner: the audio is the message and the text is there to be skimmed. The turn itself runs in voice mode, so the reply is one or two spoken sentences rather than a paragraph read aloud. TTS orffmpegfailing costs the audio, never the message. Turn it off withvoice.mobility_replies: false. The end-of-trip follow-up is plain text — it is read while parked. - Mobility deliveries also create an APX Android card and play the local sound when native message sounds are enabled.
- Direct APX messages use Android’s messaging notification format. Android Auto can render them as heads-up notices and read them aloud without opening Telegram. The native menu includes Test Android Auto notice for DHU or vehicle testing. Android Auto enables Do Not Disturb while connected; grant Driving alerts once if APX should bypass it.
What it costs when nothing is happening
Section titled “What it costs when nothing is happening”APX is a WebView shell, and idle it is meant to cost nothing. A trip is the one state that is allowed to be expensive — that is when GPS runs and when a dropped socket has to be repaired in seconds.
- The mascot animates while it is dragged, hopping, or showing a message. At rest it holds its pose and only blinks, instead of redrawing thirty times a second over whatever app is actually on screen.
- Leaving APX pauses the embedded
/mobileand stops its JavaScript timers. Coming back resumes it. Native notifications are unaffected — they arrive through the foreground service, not the WebView. - The socket’s reconnect ladder is capped at 15 seconds during a trip and at 5 minutes outside one, and it stops entirely when the phone has no network, waking on Android’s connectivity callback instead of on a timer. A phone away from the daemon used to redial it every 15 seconds all day. Opening a trip reconnects immediately.
- APX menu → Remove battery restriction opens Android’s own dialog. It is worth granting for trip detection: without it Android defers the start and end of a trip while the phone is in a pocket. The menu row shows the current state, and once APX is exempt the same row opens the system list, which is where the exemption is taken back.
- It also decides whether GPS tracking can start on its own. Location is a while-in-use permission, so granting it is not enough — Android additionally requires the app to be in an “eligible state” to start a location-typed foreground service, and the notification listener that detects a trip runs in the background. Without the exemption that start is refused, APX logs it and carries on: the daemon still learns the trip began, but there is no position stream and therefore no proximity reminders until you open APX, which makes the app eligible and retries. With the exemption, tracking starts by itself.
The trip plan: how many searches a drive costs
Section titled “The trip plan: how many searches a drive costs”An errand is not always a place. “Buy bread” can be satisfied by the bakery on the corner or by either supermarket, and which one is right depends on where the car is at the time. Answering that question by searching again on every GPS sample would be a place-provider call every few seconds; answering it once at the start would send the driver past three better options.
So the daemon keeps a trip plan: one entry per errand, holding every place that could satisfy it and the one it is currently pointed at.
- The plan is built when you get in the car, from the origin the phone reports — not lazily on the first sample taken 800 m down the road.
- Re-ranking is free. Picking the nearest candidate as the car moves is arithmetic on coordinates already held. No call, no model.
- Re-searching is rare. It happens only when the car leaves the searched area by 8 km, or when the open-errand list itself changes — a local file read, checked on every sample, so a task added at a red light is live on the next one.
- A settled errand stops looking. One candidate is the decision, and so is a “voy”: both lock the errand, and a locked errand is never re-searched or re-pointed. Once every errand is locked the plan is finished and the drive costs nothing more.
A normal drive with a settled shopping list therefore costs one place
search. A drive where every task carries its own pin costs zero — and zero
model calls either way, because a trip task states what it is and where,
instead of asking a model to read the title and guess.
If you ever want to pay for better places
Section titled “If you ever want to pay for better places”The free stack (Nominatim for places, Valhalla/OSRM for routes) is what ships, and the plan above is what makes swapping it in cheap. For reference, at 2026 prices the Google alternatives bill per call, so the number of calls is the whole game:
- Places API Nearby/Text Search — 5,000 free calls a month, then USD 32 per 1,000. One search per drive is affordable; one per GPS sample is not.
- Routes API
computeRoutes— from USD 5 per 1,000, and it can optimise the order of up to 25 intermediate stops. This is the piece that would build a real multi-errand route rather than a nearest-first list. - Search Along Route —
computeRoutesreturns an encoded polyline, and Text Search accepts it assearchAlongRouteParameters, returning only places actually on the way. Two calls per trip, and strictly better than a radius search around a point when a destination is known. - Android’s own geofencing (
GeofencingClient, part of Play Services and not billed) is the other half: it hands up to 100 fences to the OS, which wakes the app when one is crossed using batched hardware geofencing, instead of the app sampling GPS on a timer. The plan above already produces exactly the small, stable set of places such an API wants.
Which route APX knows
Section titled “Which route APX knows”APX does not receive Google Maps’ exact polyline. Android sends the latest available GPS point as the origin and destination text exposed by Maps, explicitly shared, or confirmed by voice. The daemon geocodes that destination and builds its own driving route with Valhalla, falling back to OSRM.
That reconstructed route is enough for corridor and nearby-place matching, but it can differ from Google Maps because it has no Google traffic, avoidance, or private-road decisions. Without a destination, Roby receives current position only; APX does not claim a route or run proximity matching.
APX-controlled head unit (experimental)
Section titled “APX-controlled head unit (experimental)”Android Auto also sends semantic navigation messages to the connected head unit. This is a private projection-protocol channel, not an Android app API. During a real USB/DHU test, APX captured:
0x8003: navigation active/inactive state;0x8006: current maneuver text, road names, and destination address;0x8007: remaining distance and arrival information.
The verified 0x8006 payload contained guidance such as Bosque
Petrificado and destination address R8400, Río Negro. This proves an
APX-controlled head unit can detect route context automatically, without
notification scraping, voice confirmation, Google Maps sharing, or a Google
Maps API key.
Target production flow:
Android phone → APX head unit/proxy → navigation decoder → authenticated /api/mobility/events → RobyHead-unit connection itself proves driving context. Emit trip.started only
after navigation state becomes active and a useful destination appears. Emit
trip.ended when navigation becomes inactive or projection disconnects. Keep
last semantic state in memory, deduplicate repeated 0x8006 frames, and never
store video/audio projection data.
Getting the app
Section titled “Getting the app”Installing it, and choosing which address it connects to, is its own page: Install on Android. The short version:
apx android install # over USB: installs, tunnels and pairs in one stepor open the published APK on the phone.
Build it yourself
Section titled “Build it yourself”cd src/interfaces/android./gradlew testDebugUnitTest assembleDebugadb install -r app/build/outputs/apk/debug/app-debug.apkA release build needs a keystore in the environment (APX_KEYSTORE,
APX_KEYSTORE_PASSWORD, APX_KEY_ALIAS, APX_KEY_PASSWORD); without one,
assembleRelease produces an unsigned APK that Android refuses to install. CI
does this on every change under src/interfaces/android/ and republishes the
release — see .github/workflows/android.yml.