An iMessage client for the Light Phone III,
talking to an always-on, self-hosted BlueBubbles Server
reached privately over Tailscale. Package
com.gios.lightchat.
Current version: v1.1.14. See Version history.
This is gi-os's fork of craigeley/chat. Craig Eley wrote the app; this fork renames it to LightChat (
com.gios.lightchat) to sit with the rest of the gi-os Light Phone tools and adds everything from Favorites / Known / Unknown tabs onward, below. Nothing here is upstream's responsibility — send bugs in these features to this repo, not to Craig. Craig's original README is preserved at the bottom of this file, as he wrote it, for attribution and history.Coming from the old
com.craigeley.chatbuild? The package id changed, so this installs alongside it rather than updating it. Uninstall the old one, re-run setup, re-run bothadbgrants, and re-add the app in Obtainium — it sees a different package. Starred chats don't carry over either.
Open the app to a list of conversations (newest activity first); tap one to read the thread, or tap New to start one (searches your contacts by name/number/email). Tap Messages at the top for settings, Refresh to re-pull.
Craig Eley built the original chat to talk to a BlueBubbles server over Tailscale, no
Google push involved — the app holds its own socket open. This fork exists to replace
OpenBubbles on Gio's LPIII, which was found to be both a battery hog and flaky about
ordering messages correctly, and it adds:
- Favorites / Known / Unknown tabs. The conversation list is three lists behind an icon bar: starred chats, chats with a name in the Mac's address book, and everything else. Long-press a row to star it. Exiting a thread returns you to where you were in the list rather than the top of it.
- Full-colour photos. Tapping a photo lifts LightOS's greyscale for exactly as long as the viewer is open. Needs a one-time adb grant — see Optional features.
- Heads-up messages. A minimal box over whatever you're doing when a text arrives, with the sender, the message, and a buzz. Needs a one-time adb grant.
- A photo picker that works. The system one reads MediaStore, which nothing keeps current on LightOS, so photos you just took were never offered. This one reads DCIM and Pictures directly. Multi-select, an inline camera, and the whole thing runs in colour — with the grayscale grant, picking and framing a photo aren't guesswork.
- The brightness wheel scrolls. Threads, the conversation list, contact search, the photo grid — see The wheel.
- Background delivery that survives sleep. See Don't miss messages while the phone sleeps.
You need three things running before the app is any use:
- A LightPhoneIII with the full Android layer exposed (see this guide).
- A BlueBubbles server on an always-on Mac, signed into your iMessage account.
- Tailscale installed on that Mac and on the modified LPIII.
1. Install BlueBubbles Server from bluebubbles.app on the Mac. During setup it asks for a server password — remember it, the app uses it to authenticate. Skip the Google Firebase section entirely, and set the Proxy Service to LAN only; Tailscale handles the rest.
2. Put the Mac and the phone on the same tailnet. Install
Tailscale on both, same account. On the Mac, expose
the BlueBubbles port (default 1234) over HTTPS:
tailscale serve --bg 1234This gives the Mac a stable https://<machine>.<tailnet>.ts.net URL with TLS, reachable
only from your own devices. tailscale serve status shows the URL.
3. Install the app. Grab the newest signed APK from Releases or track this repo in Obtainium.
4. Configure it. On first launch, enter the https://…ts.net URL and the
BlueBubbles server password. The app validates them against the server and stores them
on the device.
That's the whole path to a working thread list — the parts that take real time are setting up BlueBubbles and Tailscale on the Mac, not the app itself.
By default BlueBubbles can only send via AppleScript, which can't send tapbacks, mark chats read, or send typing indicators. Those need BlueBubbles' Private API, which injects a helper into Messages — and that requires turning off two macOS protections. Skip this and everything else still works.
-
Disable Library Validation (lets the helper load into Messages):
sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist DisableLibraryValidation -bool true -
Disable System Integrity Protection (SIP). Boot into Recovery (Apple Silicon: hold the power button → Options; Intel: hold ⌘R at boot), open Terminal, run
csrutil disable, reboot, verify withcsrutil status(should readdisabled). On Apple Silicon this also disables running iOS apps on the Mac. A VM snapshot first is wise. -
Flip it on in the server. BlueBubbles Server → Settings → Private API toggle on — the server injects the helper itself, no separate bundle. Hit refresh on the Private API Status box; it should report the helper connected. (
GET /api/v1/server/infoshows"private_api": trueand"helper_connected": true— the app reads this to decide whether to offer tapbacks etc.)
Full-colour photo viewing. The Light Phone's grayscale is Android's accessibility color-correction filter, which apps can lift with a permission grantable only over adb. Tapping a photo in a thread then shows it in full colour for exactly as long as the viewer is open — the phone returns to grayscale the moment you dismiss it (the same trick as zero's red-text mode):
adb shell pm grant com.gios.lightchat android.permission.WRITE_SECURE_SETTINGSOne-time; survives app updates. Without it, photos open in grayscale like the rest of the phone.
Heads-up messages. A text arriving while you're elsewhere buzzes and drops a small box over whatever you're doing: sender, two lines of message, gone in four and a half seconds. Tap it to open the thread, swipe up to dismiss it early. It also wakes the panel, so a message arriving with the phone face-down still shows. Getting a window up from the background needs one appop — on Android 14 this is what exempts an app from background-activity-start restrictions:
adb shell appops set com.gios.lightchat SYSTEM_ALERT_WINDOW allowOne-time; survives reboots and app updates. Without it you still get the buzz and the notification, just not the box.
Delivery is a socket LightChat holds open itself — there's no Google push on this phone
— and a socket doesn't survive the phone sleeping. LightChat backs it with an
setAndAllowWhileIdle alarm, the one kind that fires during Doze, which re-pulls the
list and notifies for anything missed.
What actually throttles that isn't Doze, it's App Standby buckets. The longer the
phone goes unused without LightChat being opened, the further Android demotes it —
active → working set → frequent → rare → restricted — and each step defers its alarms
harder. By rare, an alarm asking for five minutes is held for two hours;
restricted holds it for a day — which means the exact situation the poll exists for
(phone face-down overnight, app not opened) is the situation Android throttles it out
of. One command turns that off rather than negotiating with it:
adb shell dumpsys deviceidle whitelist +com.gios.lightchatThis puts the app in the exempt bucket: no alarm deferral at all, and network access during Doze instead of a ~10 second window per alarm. LightChat notices and polls every 5 minutes instead of 10. Survives reboots and app updates.
Settings tells you whether it took. The bottom of the Settings screen reads either
Background delivery: unrestricted or Background delivery: throttled (rare), plus
when the app last actually heard from the server — the only way to tell from the phone
that background delivery stopped hours ago rather than nobody having texted you.
For instant delivery after a reboot without opening the app, enable Tailscale's Always-on VPN (Android Settings → Network → VPN) and leave "Block connections without VPN" off — the live socket reconnects the moment the tunnel comes up.
Silent by default. An iMessage account that has been around a while gets a steady trickle from short codes, delivery notices, two-factor senders and whoever last had your number — and on this phone each one buzzes, wakes the panel and drops a box over whatever you were doing. So out of the box only your contacts and named groups do that.
The messages still arrive: they appear in the list with an unread mark and open normally, they just don't interrupt. Settings → Unknown senders switches it, and "known" is the same test the list's Known tab uses — a named group, or any participant in your address book.
Roll, the camera app, sends straight here: its send button opens your contacts, you pick a person, and LightChat opens on that thread with the photograph already sent. No chooser in between.
That works because LightChat registers as an image share target and reads the recipient from
the share's address extra — the same convention the stock messaging apps use, so anything
else that shares a photo to LightChat works too, it just lands on the conversation list and
waits for you to pick a thread.
Tap a conversation's title — a group or a 1:1 — and you get everything it has accumulated: the people in it, a note, every photograph anyone sent, and every link.
The photographs are a grid of what the phone holds for that conversation, newest first, and tapping one opens it full screen, in colour, the same viewer the thread uses. The links are every URL anyone sent, once each, with who sent it and when.
Neither is the whole conversation, and the page says so. It reads the newest couple of hundred messages; scrolling to the bottom — or tapping Look further back, for a page too short to scroll — asks the Mac for the page under it. So a conversation you never scroll costs nothing, and one you do costs a page at a time. A chat whose thread has never been opened holds nothing at all, and the page says that rather than claiming there are no photographs in it.
The Note row opens LightNotebook, which keeps one note per conversation and makes it on the first tap. Nothing is read back across the gap — the row is a door, not a preview — and if Notebook isn't installed the row says so and does nothing.
The note is keyed by the conversation's normalised handles (+12125550148), never by its
chat guid: a guid belongs to one Mac's chat.db, so restoring a backup, or moving to
another Mac, would strand every note ever written. The cost is that changing a group's
membership changes its key, and the old group note stays in Notebook but stops being
reachable from here. A 1:1, which is what the note is mostly for, never moves.
Turning the brightness wheel scrolls whatever is up: a thread, the conversation list,
contact search on the new-message screen, a group's member list, the photo grid, and
the setup form. Only the turns — the wheel click and the camera button belong to
LightControl, which owns them phone-wide and
passes bare notches through to com.gios.* for exactly this.
It works because the wheel arrives as an ordinary key event. Light patched
/system/usr/keylayout/Generic.kl to label scancodes 19 and 20 WHEEL_CCW/WHEEL_CW,
and nothing in PhoneWindowManager intercepts them, so they reach the focused window
like any other key — which is also why an app that ignores the keycode appears to have
a dead wheel. hw/LightKeys.kt resolves the labels at runtime and falls back to the raw
scancode, gated on the sensor's device name so a paired keyboard's r can't scroll a
thread.
Reading a text is the case that makes the handling fussy. The keys are claimed in
dispatchKeyEvent, above the view hierarchy, so a notch reaches the thread rather than
the compose bar that has focus — and both halves of each DOWN+UP pair are swallowed,
because a wheel that types into a half-written message is worse than one that does
nothing. The thread's list is reverseLayout, which reverses its scroll axis too, so
the sign is flipped for that one list. Notches are frame-timed rather than applied as
they land — the sensor fires every ~35 ms, faster than a frame — and the first notch
after a pause is held until a second confirms it, since the wheel sits under a thumb and
a stray brush shouldn't move the message you were reading. hw/Wheel.kt has the
numbers.
./gradlew :app:assembleDebugSolo repo, no PR workflow for the gi-os additions: commits go straight to develop,
which is this repo's default branch — not main. Every push to develop triggers
CI (the house build.yml, adopted in v1.0.7), which builds, signs, and publishes a
GitHub Release. A push is a release, not a cosmetic action — verify before pushing,
not after.
Signing is mandatory and the keystore is not committed (unlike the other gi-os
repos) — it lives in repo secrets KEYSTORE_BASE64 / KEYSTORE_PASSWORD /
KEY_ALIAS / KEY_PASSWORD, with the certificate pinned in signing-fingerprint.txt.
versionCode is the workflow run number; versionName in the committed
build.gradle.kts (currently 1.0.0) is only the major.minor base — CI stamps
major.minor.RUN at build time and tags it vX.Y.Z.
Real tags, oldest to newest (the early 0.x history predates the gi-os fork's
vX.Y.Z CI convention and was tagged directly off build-script version bumps):
| Version | What changed |
|---|---|
| v0.1.0 | First release: BlueBubbles client over AppleScript send |
| v0.1.1 – v0.1.2 | App icon, capitalized name |
| v0.1.3 – v0.1.4 | Group-chat send fixes, including forked-group sends via AppleScript |
| v0.1.5 | Replies, group management, failed-send surfacing, absolute list times |
| v0.1.6 | Unread markers, notification deep-links, full-screen image viewer |
| v0.5.0 | Full-colour photo viewing (lifts LightOS's grayscale while the viewer is open) |
| v0.6.x–ci / v0.7.1–v0.7.2 | Renamed to LightChat (com.gios.lightchat, repo gi-os/LightChat); Favorites/Known/Unknown tabs; own photo picker with inline camera; heads-up box + buzz for incoming messages |
| v0.7.3 | Inline camera, photo picker runs in colour |
| v0.7.4 | Heads-up overlay, mark-all-read, double-tap tapbacks, no null chats |
| v0.7.5 | Fix: don't miss messages that arrive while the app is away |
| v0.7.6 | Fix: actually check for messages while the phone is asleep |
| v1.0.7 | Fix: always notify, even after the phone has been off for hours; adopts the house CI workflow |
| v1.0.8 | Hardware wheel scrolls threads, the conversation list, contact search, and the photo grid |
| v1.0.9 | README: documents what the wheel needs |
| v1.0.10 – v1.0.13 | Unknown senders are silent by default; messages are kept on the phone and only the delta is synced; receiving photos shared from Roll |
| v1.1.14 | A contact page for every conversation: photos, links, and a note kept in LightNotebook |
MIT.
The following is Craig Eley's original README for craigeley/chat, kept as he wrote
it, for attribution and history. It describes the base app this fork builds on; some
details (package id, install instructions) are superseded by the sections above.
An iMessage client for the Light Phone III, inspired by and based on the apps created by vandamd. The app works by talking to an always-on, self-hosted BlueBubbles Server, reached privately over Tailscale.
I built this to replace OpenBubbles on my LPIII, which I found to be both a battery hog and very flaky in terms of displaying and ordering messages correctly.
Open the app to a list of conversations (newest activity first); tap one to read the thread, or tap New to start one (searches your contacts by name/number/email). Tap Messages at the top for settings, Refresh to re-pull.
