Changelog
All notable changes to G2 Texting — the Android bridge app and the G2 glasses plugin.
1.2.0 — 2026-08-21
The bridge stops timing out. Measured on the phone, not reasoned about.
The finding that matters most is not in this code
Battery optimisation is exempted for com.example.g2bridge — the package name this app
used before the rename. The current package, com.doubleare.g2bridge, has never been on that
list. On this ColorOS phone that is not a nicety: the system log shows its app-freezing
framework repeatedly trying to demote the bridge and being blocked only because a foreground
service is running, and its kill manager sparing the process for the same single reason. A
frozen process still holds its listening socket, so the glasses connect and then wait — which
is exactly what "bridge timed out" looks like.
Open G2 Bridge, go to Setup, and tap Keep the bridge awake. Nothing below substitutes for it.
Fixed — the timeout loop
- An aborted request no longer takes the server down. The glasses give up on a request after eight seconds and abort it. That abort cancels the request coroutine, and the catch-all error handler was catching the cancellation and trying to reply on a connection that no longer existed — which failed again, out of the handler, and stopped the engine. The socket stayed open, connections were still accepted, and nothing was ever answered. Measured: three concurrent aborted requests killed the bridge until the service restarted. Since every timeout produces an abort, one slow moment became a permanently dead bridge. Now cancellation is left alone. Measured after: 1,440 aborted requests over three minutes, with latency and memory flat.
- The glasses no longer multiply their own retries. When a boot failed, the retry timer re-armed itself and the failed attempt started another chain, because the guard meant to stop it compared a counter that is deliberately not incremented for an unchanged screen. Chains doubled per failure. Simulated over two minutes of an unreachable bridge: 161,472 requests across 42,016 overlapping chains, against 24 requests on one chain now. The browser allows about six sockets per origin, so the retries alone starved the request the user was actually waiting on — and a recovering bridge did not help, because the storm was local.
- A dead WebSocket is now detected. The push socket sent and never listened, with pings off, so a client Android killed without a close frame stayed subscribed forever and each relaunch added another. Twenty-second pings; unanswered ones close the session.
Fixed — work that should never have been running
- The conversation-summary subsystem is gone from the phone. Its screen was removed in 1.0.0; the machinery was not. It still marked a row stale on every message stored, still woke every twenty seconds to recompute up to eight conversations, and still pushed the glasses into a full refetch when it finished — to produce data with no reader.
- A burst of messages is now one refresh instead of one per notification. Ten messages in five seconds meant twenty requests, each dragging an SMS scan behind it.
- The favourites list is no longer re-fetched on every push, and no longer builds two hundred conversations to return six.
- The app's own status page no longer queries the database once a second on the main thread.
Fixed — database work
- Write-ahead logging is on. Without it a writer excludes every reader, and this app writes constantly from several places while the one thing that must stay fast is a read.
- One store and one repository per process. Five components each opened their own connection pool to the same file and contended with each other.
- The SMS sync commits in batches instead of one transaction per statement — roughly five thousand flushes per pass before, dozens now.
- Contact-name lookups happen before the transaction opens, not inside it. Each is a call into another process, and the write lock was held across up to two hundred of them.
- An index for the duplicate-detection query, which had none. Measured on a thread of twelve thousand messages: 0.92 ms to 0.008 ms.
- A ledger of every message observation, so a text seen both as a notification and from the provider is recognised instead of rediscovered by scanning.
- The sync records how far it has read rather than inferring it afterwards. The inference was wrong in a way that grew: almost every text arrives as a notification first, so the provider copy merged into that row and never counted, and the re-read window widened by a day a day.
- A failed insert no longer reports success. It returned a sentinel, not an exception, and the sync counted the row as stored and advanced past it — losing exactly the messages that failed.
Note
onPause in the phone app contained a stray copy of the status refresh, which could raise the
pairing dialog as the screen was leaving. Removed.
1.1.0 — 2026-08-10
The bridge stops going away. Measured cause, not a guess.
Fixed — availability
- The foreground service type was
dataSync, which Android caps at six cumulative hours per 24. At the cap the system stops the service and then refuses to start it again until the app is opened by hand — andstartForegroundwas called unguarded fromonCreate, so every restart attempt crashed the process instead. For a bridge meant to run all day this is fatal, and it explains "constantly disconnects, fails repeatedly" exactly. NowspecialUse, which has no time budget (verified live on-device:types=0x40000000). - The foreground start is guarded. A refused start is survivable — a crash is not.
- A watchdog restarts the bridge when it should be running and is not. An AlarmManager alarm lives in the system, so it fires whether or not this app is alive. Verified: process killed → socket refused → watchdog → serving again in 2s, with no user involvement.
- Sideloaded updates no longer leave the bridge down. An update force-stops the app and
takes its alarm with it; the watchdog now also listens for
MY_PACKAGE_REPLACED. - Opening the app, an inbound SMS, or the notification listener reconnecting all repair the bridge. Each already woke the process and did nothing about the dead server.
- Swiping the app out of recents no longer ends the day:
onTaskRemovedre-arms the watchdog. - The
/conversationsSMS sync is serialized and rate-limited. Every concurrent request used to run its own full provider scan, all contending for one SQLite writer. - The glasses retry indefinitely with backoff instead of parking on "Bridge unreachable".
Changed
- The third column is gone from the conversation view. The messages now use the full width beside the action rail. On 576 pixels the messages are the product; a panel summarising them spent 40% of the width restating what the other 40% already showed.
Note
Not the cause, though it was the suspicion: there is no residual model data. The 975 MB model is gone, app storage totals 601 KB, and the endpoints answer in 44 ms.
1.0.0 — 2026-08-04
Reliability over features. The conversation summaries are gone, and the bridge is steady.
Removed
- Conversation summaries and the on-device model. They made the app unstable, and measurably so: with the model installed, a 3-minute stress of the bridge gave 7 successes and 17 timeouts; with it removed, the identical test gave 86 successes and 0 failures, worst response 0.14s against 8s before. During a summary pass the app held 2.4 GB of memory, the phone thrashed, and the glasses could not reach the bridge for the 40-120 seconds it took. No amount of scheduling fixes a gigabyte of resident model on a phone that is also trying to serve requests.
- The APK no longer carries a native inference engine, and nothing asks for a 975 MB download. Existing installs can delete the model; the app no longer looks for it.
Kept
- The panel beside a conversation, which was always the good part: what they last asked, verbatim; how they interact with you (who starts, who writes more); and their reply cadence. All counted from your messages, exact, instant, and free.
Fixed
- The brief worker ran on the main thread — database work and, previously, model inference. It has its own background thread now. That alone did not save the summaries, but main-thread database work is how the mistake starts.
- The glasses retry quietly before showing "Bridge unreachable", and keep retrying from that screen. A bridge waking from doze is a second of unavailability, not a failure worth a full-screen error.
0.9.6 — 2026-08-02
Changed
- Android only, said before anyone can waste time on it. A badge sits beside the site's name in the sticky nav — on screen on every page at every scroll position — and in the eyebrow above the headline, with the full explanation directly under the lede rather than below the demo. Also in the Try it requirements and the footer; the glasses app says it on the pairing and bridge-unreachable screens — the two places an iPhone owner would actually get stranded — and in its store permission text; the Android app's own walkthrough notes that the glasses half does nothing without it. There is no iPhone version and there cannot be one: iOS gives no app access to your messages.
0.9.5 — 2026-08-02
Fixed
- Unfolded on a foldable, the bottom of every page was cut off and unreachable. The inner screen is shorter than the outer one — 763dp against 805dp on a Find N6 — while being twice as wide, and every page was sized to fit the taller folded screen exactly. Pages scroll now, and look identical wherever they already fit.
- The artwork was the real culprit. Aspect-locked drawings grow with width, so the hero went from a comfortable 269dp folded to ~360dp unfolded, eating the height that had just shrunk. Every drawing now has a height ceiling.
Added
- The extra width is actually used. Side padding grows on wide screens, and the walkthrough switches to a two-pane layout — artwork in one column, the explanation and its button in the other.
0.9.4 — 2026-08-02
Fixed
- A message arriving in the conversation you are reading now appears in it. Pushes only ever re-rendered the conversation list; if you were inside a thread, a new message stayed invisible until you backed out and opened it again. The open thread now refreshes on a push — and only when that thread actually moved, since a rebuild resets the button selection and refreshing on every event would pull the highlight out from under someone about to press Reply.
Note
- If the glasses show nothing at all, check that the bridge is running: the widget's dot, or the app's status page. Messages keep being collected while it is stopped, but the glasses have nothing to talk to until it is started again.
0.9.3 — 2026-08-02
Changed
- Walkthrough pages fit without scrolling. Copy cut to roughly half, artwork capped by height rather than width — height is what competes with the content — and the layout tightened, so each step's explanation and its button land on one screen.
- Skip moved to the top right, out from under the forward button, where an escape belongs and where it no longer sits in the position the eye reads as "continue".
- A "▾ more below" hint with a fade appears when a page genuinely does overflow — a large system font will beat any fixed layout — and disappears once you reach the bottom.
0.9.2 — 2026-08-02
Changed
- The walkthrough now sets things up, not just explains them. Every step ends in the button that resolves it — allow messages, turn on notification access, allow background running, add the widget, add summaries — and each reads its real state, so a step you have already done says so instead of asking again.
- Artwork on every page, drawn on the same pixel grid as the rest of the app: the glasses projecting a message, a message being lifted out of the notification shade, the app card held open against the system, the widget tile in both of its states, and the model as a chip inside a wall nothing crosses.
- Responsive by construction — each drawing is aspect-locked to its width and capped at 420dp, so the pages hold from a small phone to an unfolded foldable with no alternate layouts.
0.9.1 — 2026-08-02
Added
- A walkthrough that explains the app before it asks for anything. Seven pages on first launch, re-openable any time from "How it works" on the setup page: what each permission is for and why the app cannot work without it, and — stated in the negative, which is the only form that means anything — no account, no server, no analytics, nothing sent to the developer, works in airplane mode.
- Three of those pages exist because each fact otherwise arrives as a bug report: RCS capture starts when the app does (there is no history to fetch — a conversation fills in from its next message onward), Android will close the app unless you lock it in the recents list and allow it past battery optimisation, and turning the bridge off does not stop messages being collected — capture and serving are separate, the widget toggles the second, and everything is there when you turn it back on.
- The site has a Try it section with the App Distribution link for the phone app and the Even Hub store status for the glasses app.
0.9.0 — 2026-08-02
The release that makes the app shareable: one public plugin build, one signed APK, no secrets inside either.
Added
- Pairing. A fresh install of the glasses app now asks the bridge to pair, and G2 Bridge shows the request on the phone: "Allow your glasses to connect?" One tap releases this install's own token; the plugin stores it and never asks again. The consent tap is the whole security story — localhost is shared by every app on a phone, so an app trying to pair itself lights up a visible prompt instead of succeeding silently. The store build ships with no token inside, because a constant in a public zip protects nobody.
- Signed release builds. The APK is now signed with a real release key, ready for distribution outside the Play Store.
Changed
- The app's identity is now
com.doubleare.g2bridge— permanent once strangers install, so it changed before any of them do. A dev-token fallback keeps development flows working unchanged.
0.8.0 — 2026-07-30
The profile grew from a note card into an analysis, and it now actually chases the conversation.
Added
- Five-section analysis per conversation: Topics, Now, Lately, an honest Read of the person — what they care about, how they communicate, what mood they've been in — and Say back: two or three concrete replies or questions worth sending next, built from what the thread actually discusses. ~2.5× the depth of the previous profile for about half a minute more compute; grounding still drops any section containing an invented name, place or number.
- Group threads are analysed as groups. Speakers are labelled by first name in the model's transcript — a group where every voice is "Them" is a profile of nobody. First names only, group threads only, and only to the model running on this phone; 1:1 profiles remain nameless.
Fixed
Three separate reasons profiles didn't update "on every message", all real:
- SMS-borne messages never refreshed the counted fields — only the RCS listener marked a conversation stale. The mark now happens inside the store's own write path, where no source can forget it.
- The CPU dozed mid-inference with the screen off. A foreground service keeps the process alive, not the processor awake; a minute of model work stretched to whenever the phone next woke. The pass now holds a partial wakelock.
- A new message waited for the next 20-second poll. Capture now pokes the worker directly, so a refresh starts within seconds of the message landing.
All existing profiles regenerate in the deeper shape automatically.
0.7.9 — 2026-07-30
Fixed
- Messages keep accumulating while the bridge is off, and appear when it comes back. They always did in principle — the notification listener writes to the store regardless of the bridge, and SMS lives durably in the system provider — but with the bridge off there is no foreground service protecting the process, and when the OS killed it the system unbound the listener and never rebound it on its own. RCS capture stayed silently dead even after Start. Now both starting the bridge and simply opening the app repair the binding (the component is toggled first — requestRebind alone is a no-op after a kill), and the reconnect backfill sweeps everything still in the notification shade, up to 25 retained messages per conversation. Starting the bridge also syncs SMS immediately rather than waiting for the first fetch.
- What this cannot recover, by platform design: a notification posted and dismissed while the process was dead. The next message in that conversation self-heals the gap.
0.7.8 — 2026-07-30
Fixed
- A message that arrived just before your first launch kept losing its dot. The first-run seed marks existing history as read so install doesn't produce sixty dots — but the text that came in five minutes before you opened the app is exactly what the mark is for, and the seed was swallowing it. Anything incoming within the last hour now keeps its dot.
- The seen-map is stored twice — bridge storage and the WebView's own — and loading prefers whichever survived. A map that fails to persist would re-seed on every launch, which silently disables the feature; now either store alone is enough.
- The boot log states
seen: N storedorseen: seeded, which is the entire diagnosis of a missing dot.
Notes
- A text you send to yourself will not dot: the rule reads your own last message as proof you saw the thread, and in a self-conversation the last message is always yours. Test with a message from someone else.
0.7.7 — 2026-07-30
Changed
- The conversation page reads as a transcript. The name leads, with the page count beside it — on every page, since a header should not scroll away with page one. Each message is a timestamp line, then the text: in a 1:1 the other person's messages carry only "(date time)" — their name is already the title — and "You:" marks yours. Groups keep sender names, because there they are information.
0.7.6 — 2026-07-30
Changed
- No profile, no panel — the messages get the width. A conversation without a bio (which is every conversation when Conversation Summaries is off) now renders actions in the left fifth and the conversation across the remaining 80%, instead of reserving two fifths of the display for a panel with nothing to say. The three-column layout returns per conversation as each profile lands.
0.7.5 — 2026-07-30
Changed
- Bio appears only when a profile exists. With Conversation Summaries off the button is gone entirely; with them on, it shows up per conversation as each profile lands. A button promising an empty page is worse than no button — the panel beside the messages shows the counted lines either way.
0.7.4 — 2026-07-30
Added
- A dot marks conversations you haven't opened yet. It appears when someone writes and clears the moment you open the thread on the glasses — or when you reply, since your own message means you saw theirs. "Read" is judged on the glasses, not the phone: a non-default SMS app cannot write the provider's read flags, and what the mark actually tracks is what you have seen HERE. State survives the WebView being reclaimed, and the first run counts existing history as read — sixty dots on install would make the mark meaningless before it ever meant anything.
0.7.3 — 2026-07-30
Changed
- The conversation list is back to one row per conversation — name, then the message, truncated to the row, full width. The two-line layout of 0.7.2 rendered exactly as designed and lost on arithmetic: eleven conversations a page against two. The platform fact it uncovered (items advance a fixed 40px; a taller row cannot exist) stays in the guide.
- The dictation screen dropped its gesture hint. "tap = Done, double-tap = Cancel" restated what the rail's two labelled buttons already say. The fallback behaviour itself is unchanged.
0.7.2 — 2026-07-30
Changed
- The conversation list is two lines per conversation — the name, and its latest message beneath it. Both lines open the thread, so the selection landing on either reads as selecting the conversation. The pager shows where you are ("Older (2/60)"), and going back a page is double-tap — the same gesture contract as everywhere else. Top 6 stays one line.
- Dictation and confirm moved to the rail layout: Done/Cancel — and Send/Redo — in the left 20%, the live content in the remaining 80%, no icons. The last glyph-prefixed labels are gone.
- The bio page is 20/80, actions and panel — the 60% reading of the sketch is corrected.
Fixed
- The two-line list rows exposed a platform fact now in the guide: list items advance a fixed 40px and centre as a group in their container, so a taller selectable row cannot exist — two-line entries spend two items. Measured, not assumed: two layouts' first-item offsets both solve to exactly 40.
0.7.1 — 2026-07-30
Fixed
- The bio panel sits in the last 60% of the display, as designed. 0.7.0 ran it from the action rail to the right edge on an anti-whitespace instinct; the designer reaffirmed the sketch, and the open fifth between controls and content is a choice, not a leak. The site's demo shows the same geometry.
0.7.0 — 2026-07-30
The two-phrase summary grew into a profile, and the profile learned to stay current.
Added
- A full profile per conversation — Topics, Now, Lately, Vibe — written on the phone in one model pass (~40 s in the background). Ten times the content of 0.6.1's two phrases for half again the time, because the transcript is paid for once and the answer budget went from 28 tokens to 200. Each section is grounded independently: an invented detail drops that section, not the profile.
- Profiles update themselves. Every profile records when the model last read its thread; any newer message re-queues it automatically, most recent conversation first. A profile is a cache with a clock, not a one-time artefact.
- The bio page matches the thread page. Back, Next and Prev stacked in the left rail — no icons — with the whole profile in one bordered, paged panel beside it.
Fixed
- "Friendly and casual" no longer dies in the grounding check for starting with a capital letter: sentence-initial words are exempt, mid-line capitals are still held to account.
- "No specific plans mentioned" and its endless rephrasings are filtered by shape.
- The native layer no longer stops generating at the first newline — that rule enforced the old one-phrase contract.
0.6.1 — 2026-07-30
0.6.0 said no local model earned its download. That verdict was overturned within a day — by re-testing after push-back, not by a bigger budget — and this release ships the model.
Added
- Optional conversation summaries, written on your phone. A new setup row offers Granite 4.0 1B (Apache 2.0) as a one-time 975 MB download. With it installed, the panel beside a conversation leads with what the thread keeps coming back to and what you're arranging — above the counted lines, which keep working without it. Summaries are computed in the background, one conversation at a time, a few seconds each, and never while you wait. Your messages never leave the phone, and no names or numbers are read to produce them.
- Anything the model says that isn't grounded in the messages — an invented name, place or number — is discarded, and the field ships empty instead. On the glasses an empty line simply isn't there.
Fixed
- The 0.6.0 evaluation that condemned the model was broken in two places: the Mac eval rig's
GPU path silently corrupted Granite's output into
@runs (its hybrid-Mamba blocks are miscomputed on Metal; CPU is correct), and the grounding filter rejected correct abstract words like "parenting" for not appearing verbatim. With both fixed, the 1B produced usable lines on most threads; the 350M really was as bad as reported and stays rejected. - First on-device run took ten minutes per conversation at 489% CPU: AGP's debug variant builds native code at -O0. Forced Release with the ARM dot-product extensions — the same job now takes 15-30 seconds.
0.6.0 — 2026-07-29
Open a conversation and the right-hand panel tells you who you are talking to — from the messages alone. No name, no number, no contact record is read to produce it.
Added
- A brief panel, from counting rather than guessing. Beside every conversation: what they most recently asked, verbatim; how they interact with you (who starts an exchange, who writes more, measured in characters so five one-word replies are not mistaken for carrying the conversation); and their cadence ("replies in minutes, usually evenings"). Each of these is counted from the last 100 messages, so none of them can be wrong.
- Three-column thread view. Actions down the left (Home, Reply, Bio, Next, Prev — words, not glyphs), the conversation in the middle, the brief on the right inside a real border. The Bio button opens the whole brief full-width.
- Briefs are computed on a background worker in small batches and cached, so the glasses never wait on one. A new message marks its conversation for recomputation.
Removed
- "What they like" is gone, and so is the local model that was going to write it. Both were measured on real threads rather than assumed. Interests: one extractable in ten threads under a strict test — people text about plans, not preferences — and asked loosely, the model simply invented them. Topics: three prompt shapes across three Apache-2.0 and near-Apache models (Granite 4.0 350M at 218 MB, Qwen3-0.6B at 364 MB, LFM2.5-350M at 209 MB) produced at best two useful lines in eight, plus roleplay, verbatim quoting, and one panel containing a different contact's name. On a panel whose whole value is being never wrong, a field that is right half the time is worse than an empty one. See the paper, §3.22.
- Two fields from the original sketch were never shipped for the same reason: "who this person is" returned the question reworded, and an attachment label gave three interchangeable answers for three very different relationships.
Fixed
- Column widths are now measured off rendered pixels instead of divided out of the pixel width. The font is proportional — prose runs 7.8–8.7px per character while a link runs a full 10px — so the ratio-based guess left a quarter of both the message column and the brief panel empty.
- The glasses artwork in the phone app used a real contact's name and message as its demo text.
0.5.1 — 2026-07-28
Added
- Home-screen widget. One tap toggles the bridge, and its state is on its face — a green dot and "Running", or a grey dot and "Stopped". Until now the only way to know whether the bridge was up was to open the app.
- A reply appears in your list the moment it is sent. Messages you send are recorded locally, so the conversation jumps to the top immediately rather than waiting for an echo that may never arrive — a non-default app cannot write to the SMS provider, and RCS replies land in Google Messages' own database. A locally-recorded send never matches against another local send, so two identical replies a minute apart stay two messages.
- Top 6 is now labelled "Top 6 | Quick Message" in both halves.
0.5.0 — 2026-07-27
SMS and RCS are one list now, and the six people you message most are one tap from dictating.
Added
One store, one list
- SMS and RCS are merged into a single time-ordered conversation list. The separate "RCS inbox" is gone — which network carried a message is the network's business, not a category you should have to hold in your head while scanning.
- RCS is persisted. It previously lived in memory and died with the service, which is why the inbox was always nearly empty. It is now in a database and survives restarts.
- Conversations are keyed on participant identity, so the two feeds land in one thread without anything joining them by hand. Verified on device: one contact's January SMS history and today's RCS now read as a single continuous conversation.
- Identity splits when uncertain and merges only when certain — enforced structurally, by putting confident and unconfident keys in separate namespaces so a wrong merge is not representable. A duplicate conversation is a nuisance; a wrongly merged one is how a reply reaches the wrong person.
- Reply transport is chosen at send time, not when the screen was drawn: RCS while a notification is live, SMS otherwise. A reply composed while RCS was available now falls back rather than failing.
Top 6
- Pick six people on the phone; they appear behind a
★ Top 6button on the glasses, and tapping one starts listening immediately — no thread to open, no confirm to press. - The picker has a search field, because scrolling sixty conversations to find one is not a picker. Slots drag to reorder and keep their positions when cleared, since a stable order is the entire point.
- Favourites are stored by conversation key rather than row id, so a store rebuild can never silently re-point a slot at a different person.
Fixed
- Message bodies from notifications are matched against their provider copies by containment with a truncation guard — a prefix match is allowed only when the shorter copy is exactly 1024 characters, the platform's hard truncation point. Without that, "ok" would match a later "ok thanks" and silently swallow a real message.
0.4.4 — 2026-07-27
Dictation kept losing words at pauses. The 0.4.2 fix was aimed at the wrong layer.
Fixed
- The recogniser was being fed audio ~100x faster than it was spoken. Its endpointer is a time-based state machine built for a live microphone; dumping a whole recording into the pipe at pipe speed makes its timers meaningless, and it silently discards entire segments. Audio is now paced at 4x real time. Measured on device against a clip with a deliberate mid-sentence pause: correct from 1x through 16x, and at 32x everything after the pause vanished — 4x keeps a 4-8x margin and costs about a second on a five-second reply.
- Every clip lost its final word. EOF on the pipe is not the "speaker stopped" signal the
recogniser needs to finalise; 700 ms of trailing silence is.
"Just test a new reply."had been coming back as"Just test a new?"even with no pause involved. - Recognition ended at the first pause. The session now uses
EXTRA_SEGMENTED_SESSIONkeyed to the audio source, so the recogniser segments at pauses as it likes but keeps going until the audio is exhausted; segments are stitched back together. The 0.4.2 approach — raising the silence thresholds to 60 s — was simply ignored by the recogniser, which is why the symptom survived that release. - A trailing silent segment could discard the whole transcript.
ERROR_NO_MATCHon the last segment marked the entire run failed. Recognised words are now kept regardless. EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLISwas set to 60 s, which on any device that honours it would have blocked completion until the 20 s timeout fired. Removed.- "Speech is not set up. Open G2 Texting on your phone" was shown for failures that had nothing to do with setup. Any null from the platform recogniser was reported as a missing engine, so an unrecognised phrase sent the user to their phone to fix nothing. Four causes now report separately: no audio captured, engine failed, model absent, model loading.
- The error screen reports how much audio was captured, and
/stt/statusexposes the recogniser's callback trace — which is what located all of the above.
0.4.3 — 2026-07-26
Group chats and your own replies were both being attributed to the wrong person.
Fixed
- A group chat appeared as several unrelated people. Google Messages posts a separate notification per speaker, so a six-person group filled the RCS inbox with six entries that looked unconnected. The inbox is now grouped by conversation — one row per chat, like the SMS list — and opening one shows the whole thread with each message attributed to whoever sent it. Long participant lists shorten to "Alex, Sam +4" so the row still has room for the message.
- Your own replies showed the name of the person you replied to. MessagingStyle marks the user's own messages by leaving the sender unset, and the code filled that gap with the notification title — which is the other person. Own messages now read "You". (The lock-screen-redacted case also has no sender, so it is filtered first and can't be mistaken for one of yours.)
- Who said what was hard to follow in any conversation. Direction was carried by a lone
"> " and a two-space indent, which does not survive this display: at 42 characters most
messages wrap, and every continuation line dropped the marker, so a long exchange became
an unattributed wall of text. Every line is now named —
3:06p You: yep, got it— using first names only, since a full "Alexandra Fitzgerald 💛" costs a third of the line and there is nobody to confuse them with inside a conversation. incomingnever reached the glasses at all. kotlinx.serialization omits any property equal to its default, soincoming = truewas dropped from every message on the wire and arrived asundefined— which would have labelled everyone's messages as your own. Defaults are now always encoded. Same failure mode as protobuf dropping zero values.
0.4.2 — 2026-07-26
Dictation was losing the beginning or the end of what you said. Three separate causes, all fixed. Plus: the backup speech engine stopped installing itself.
Changed
The backup speech model is no longer downloaded on your behalf
- Almost every phone transcribes speech itself, and G2 Texting just uses that. The 125 MB backup existed for the ones that can't — but it was fetched automatically the first time the bridge started on such a phone, spending 125 MB of someone's data and 204 MB of their storage without asking. It is now offered, explained, and installed only if they say yes.
- A phone that never needed it — but downloaded it under the old behaviour — is told so, and
offered the 200 MB back. Verified on device: reclaimed, and speech still reports
ready (google on-device)with nothing on disk. - The setup row opens a plain-language explanation of what speech uses on this phone, what it would cost if the phone can't do it, and that either way the audio never leaves the device. It is a tap away rather than a launch-time modal, because on most phones there is nothing to decide.
- When the model genuinely isn't installed, the glasses now say "Speech is not set up. Open G2 Texting on your phone" instead of the misleading "still loading".
The app is 15 MB, down from 22 MB — the Vosk native library shipped for four CPU architectures, two of which are emulator-only. A bridge to a pair of glasses never runs on an emulator.
Fixed
- The mic opened after the screen was drawn.
startDictationawaited the page render beforeaudioControl(true), and a render is a full page rebuild over BLE — hundreds of milliseconds during which you are already speaking. The mic now opens on the tap and the screen catches up. Capture is tracked in its own flag rather than inferred from which view is on screen, since the two are no longer simultaneous. - Audio still in flight was thrown away on Done.
audioControl(false)was followed immediately by snapshotting the buffer, so frames the glasses had already sent never landed. The mic is now held 400 ms past the tap (it usually lands mid-final-word) and in-flight frames are drained for a further 350 ms before the buffer is read. - The recogniser's endpointer was ending the utterance at a pause. Google's on-device recogniser is tuned for a live mic, where silence means the speaker stopped — so a mid-sentence breath finalised the result and every word after it was discarded. Since the audio is a complete recording the user already ended, the silence thresholds are now raised to 60 s and closing the pipe is the only end-of-audio signal.
0.4.1 — 2026-07-26
Battery. The bridge was holding the fallback speech model in memory around the clock.
Fixed
- The bundled speech model is no longer preloaded. It was loaded at service start and held for the lifetime of the bridge — measured at 404 MB PSS with 358 MB swapped, paid for continuously. Since the platform recogniser now handles every request, that was memory pressure serving a model that never ran. It is prepared only when the platform recogniser is unavailable, and loaded on demand otherwise.
- The model is released after 5 minutes unused, and on service shutdown. The glasses plugin only runs while G2 Texting is open, so between sessions there is nothing to serve.
Measured on device, bridge running: 404 MB PSS / 358 MB swap → 112 MB PSS / 83 KB swap. Speech and all endpoints verified working afterwards.
Not addressed: the Bluetooth link between the glasses and the phone is managed by the Even app and the glasses firmware. This app never touches Bluetooth and cannot influence whether the glasses stay connected while charging.
0.4.0 — 2026-07-26
Speech recognition moved to the platform's own engine, and sent replies now appear immediately instead of waiting on a store that may never show them.
Added
Sent replies appear straight away
- A confirmed reply is rendered in the thread the moment the bridge acknowledges it, marked
○until the real message comes back. This is not only a latency fix: Android blocks non-default apps from writing sent SMS to the system store and RCS replies live in Google Messages' database, so for many sends the local copy is the only record you will see. - A send whose request failed outright (timeout, bridge stopped) is marked
[?]instead — the outcome is genuinely unknown, so the UI claims neither success nor failure. - An explicit rejection from the bridge injects nothing.
- Provisional rows merge in by timestamp rather than pinning to the top, so an unresolvable one cannot sit above every later message forever.
Changed
Speech recognition now prefers Google's on-device recogniser
/sttusescreateOnDeviceSpeechRecognizer— the engine behind system voice typing — and falls back to the bundled Vosk model when it is unavailable or fails. Nothing extra is downloaded; the model belongs to the OS.- Glasses PCM is piped in via
EXTRA_AUDIO_SOURCE, so the recogniser never opens the microphone.RECORD_AUDIOis required regardless — declared, requested, and documented in the manifest so it does not read as overreach. - Transcripts come back capitalised and punctuated (
EXTRA_ENABLE_FORMATTING), and contact names are supplied as recognition hints (EXTRA_BIASING_STRINGS). /sttresponses carryengine(google-ondevice|vosk) and/stt/statusreportslastError.
Fixed
- Long threads showed ancient history and could never show a new reply.
messages()queriedDATE ASCand stopped at 200, returning the oldest 200 messages — so on any thread longer than that, the "recent tail" was years old. Now queriesDESCand reverses. - A freshly sent reply could be erased by an identical one sent weeks earlier. Reconciliation matched on message text with no time bound, so sending "ok" where an old "ok" existed deleted the new entry — and with the SMS mirror denied, deleted the only record of it. Reconciliation now requires the echoed message to be newer than the entry.
- One returned message cleared every pending entry with the same text. Matching was set-based; each echo now resolves at most one entry, so sending the same words twice no longer collapses into one.
- A silent Google-to-Vosk fallback made total failure look like success. The Google path
failed on every call for several test cycles while Vosk answered plausibly. Two causes:
EXTRA_ENABLE_FORMATTINGtakes a String ("quality"), not a boolean, andSpeechRecognizerrequiresRECORD_AUDIOeven when audio arrives through a pipe. - A render failure could rewrite a confirmed send as ambiguous. The
tryblock wrapped the post-send rendering; it now guards only the network call, and the outcome is recorded before anything draws. - A late send failure could hijack an unrelated screen — the catch path lacked the generation guard the other paths had.
- The failure message promised a
[?]marker that was never added for replies started from the RCS inbox.
0.3.0 — 2026-07-26
Everything below happened after the first working build. 0.0.1 could read SMS and show a
list on the glasses; it could not reply, could not see RCS, needed a laptop to run, and
exposed its API to the whole Wi‑Fi network. All four are fixed.
Added
Voice replies, transcribed on the phone
- Dictate a reply on the glasses mic; the transcript is shown for approval before anything sends. Nothing is sent without an explicit confirm.
- Speech recognition runs entirely on-device via Vosk. No cloud service, no API key, no account, works in airplane mode. The model is fetched once and then never touches the network again.
- New bridge endpoints:
POST /stt(raw 16 kHz PCM in, text out) andGET /stt/status.
RCS — receiving
- RCS never appears in Android's SMS provider, so it is captured from Google Messages
notifications via a
NotificationListenerService, with the full message text pulled from the MessagingStyle payload rather than the truncated shade preview. - New endpoint:
GET /notifications. Surfaced on the glasses as an RCS inbox.
RCS — sending (the one path that actually works)
- Replies go out as real RCS by populating the notification's own
RemoteInputaction — the mechanism Wear OS and Android Auto use. Correctly threaded, and mirrored back into Google Messages. POST /sendnow chooses its transport: RemoteInput when the conversation has a live notification,SmsManagerotherwise.POST /rcs-replyreplies directly from the inbox.- Responses report
transportandsentTo, and the glasses display both after sending. - Limits are inherent, not implementation gaps: reply-only, and only while a notification for that conversation is live.
Contact names — numbers resolve to names via READ_CONTACTS; short codes stay numeric.
Standalone operation
- The plugin packages to a
.ehpkfor private upload, so no laptop is in the runtime path. - A
BOOT_COMPLETEDreceiver restarts the bridge after a reboot, if it was running. - Monochrome app icons generated (24×24 foreground/background plus a 1-bit store variant).
Rebuilt bridge app interface
- Three non-scrolling swipe pages — status, setup, privacy — with one anchored action.
- Every status line reads real state, re-checked once a second: permissions, service, notification access, battery exemption, speech-model readiness. Nothing on screen is a claim the app has not verified.
- Setup rows are tappable and resolve their own step.
- Artwork is drawn on a virtual pixel grid in custom views (no bitmaps): the glasses
projecting their HUD, and the
BRIDGE → EVEN APP → glassesroute with a boundary marking where the phone ends.
Changed
Speech recognition now uses Google's own on-device recogniser
/sttpreferscreateOnDeviceSpeechRecognizer— the engine behind system voice typing — and falls back to the bundled Vosk model if it is unavailable or fails. Nothing extra is downloaded; the model belongs to the OS.- Our glasses PCM is piped in via
EXTRA_AUDIO_SOURCE, so the recogniser never opens the microphone. This requiresRECORD_AUDIOregardless — declared, requested, and documented in the manifest so it does not read as overreach. - Transcripts now come back capitalised and punctuated (
EXTRA_ENABLE_FORMATTING), and contact names are supplied as biasing hints (EXTRA_BIASING_STRINGS). /sttresponses includeengine(google-ondevice|vosk) and/stt/statusreportslastError. The previous silent fallback made a completely non-functional integration look like a working one for several test cycles.
Interaction model on the glasses — rebuilt around visible controls
- Was: actions hidden in gestures, with double-tap meaning different things per view.
- Now: swipe moves the selection, tap activates it, double-tap always goes back — enforced in one branch before any view-specific logic so it cannot be overloaded again.
- Back is a stack: it steps back through content pages first, then leaves the view.
- Content views are a split layout — a List of buttons that captures input, plus a Text
container that renders the conversation as prose. Page turns use
textContainerUpgrade, so they neither rebuild the page nor reset the selection. - Threads open on the newest page, ordered newest-first, with inline timestamps
(
7/25 2:12a), no blank lines between messages, and no redundant back button. - Lists remember where you were: returning re-anchors the window on the item you opened.
Speech model upgraded — from small-en-us-0.15 (39 MB) to en-us-0.22-lgraph
(125 MB download, 204 MB on disk). The small model mangled proper nouns; the larger
language graph addresses that class of error. Superseded models are now deleted
automatically.
Transport — HTTP only. Ktor's CIO engine cannot serve TLS (it throws at runtime), and a WebView rejects self-signed certificates regardless, so TLS was removed rather than faked. Loopback binding is the actual control.
Fixed
- A reply was delivered to the wrong conversation. A bidirectional
endsWithphone number match with a length guard on only one side let a short code capture a reply meant for a contact, andlastOrNullhanded it to the most recent notification. Matching is now strict: ≥10 digits on both sides, compared on the last 10, exact for short codes, and null on ambiguity so it falls back to SMS rather than guessing a recipient. - The RCS inbox duplicated every message. Deduplication keyed on
StatusBarNotification.postTime, which changes each time a notification is re-posted, so every re-scan re-admitted the whole inbox. It now keys on the message's own timestamp. - The top button of every view was dead. Protobuf drops zero-valued fields, so a
selected-row index of 0 arrives as
undefinedexactly likeCLICK_EVENTdoes — and a rebuild resets the selection to row 0. Both are now defaulted. - The app navigated itself on launch. The host emits a post-render "selection settled" event that is byte-identical to a real tap; a 600 ms settle window now ignores it.
- Screens with no way out. The sent confirmation and some error screens rendered with no selectable control. Both fixed, and the renderer now guarantees any view with body text gets a back control.
- Long messages were clipped into an unreachable scroll region. Pagination counted characters, which underestimates height for conversational text and cannot split a message taller than a page. The body is now pre-wrapped into rendered lines — simulating word wrap rather than dividing by width — and packed to exactly the lines that fit.
- Conversations rendered as one run-on paragraph. The list-label sanitizer collapsed all whitespace including newlines; body text now uses a sanitizer that preserves them.
- Dictation could not be stopped. While the mic is live, taps arrive via
sysEventrather thantextEvent. Handled, plus a 15-second auto-stop and a debounce. - A stopped bridge left the glasses on a blank screen. Requests had no timeout, so they hung forever. All requests now time out and surface a retryable error.
- The bridge crashed on start. An
sslConnectoron Ktor CIO throwsUnsupportedOperationExceptionat runtime (see Changed → Transport). - Navigation hierarchy — backing out of an RCS message returned to the primary thread list instead of the RCS inbox.
Security
- The bridge binds
127.0.0.1only. The SMS/RCS API is not reachable from the network at all; a request from another device on the same Wi‑Fi is refused. Previously it bound0.0.0.0and was exposed to the whole LAN. - The bearer token cannot be treated as secret — it is compiled into the
.ehpk, which is extractable. Loopback binding is what actually protects the API, which is why the binding change matters more than the token. - Still true, and verified in the source: no analytics, crash-reporting or tracking library
of any kind; the app never requests
ROLE_SMS; message notifications are left to Google Messages; and the only outbound address in the entire app is the one-time model download.
Known limitations
- A list's selected item cannot be set — the SDK exposes the index as report-only, and every rebuild resets it to row 0. Position is approximated by re-anchoring the list window.
- Text containers cannot be scrolled programmatically, so long content must be paginated.
- The paging button cannot show a live page number; lists have no in-place update, so the count sits on the button and the current page is rendered in the text.
- RCS replies require a live notification for that conversation; dormant threads fall back to SMS.
- On aggressive Android skins (tested on ColorOS) the bridge may be killed in the background. The setup page now surfaces the battery-optimisation exemption.
0.0.1
Initial build. SMS threads and messages readable on the glasses; token-authenticated HTTP/WebSocket bridge over a foreground service; non-default SMS companion.