Everything you subscribed to, on the Light Phone III. RSS and Atom feeds in one section,
Gmail newsletters in the other, both read in the same reader. Built on the Light SDK; tool
id com.lightrss.reader. Feeds, labels, read state, saved items, images and search stay on
the phone. Current release: v2.4.0 (tool/lighttool.toml: versionCode 22).
Formerly LightRSS, and before that a fork of
zachattack323/LightRSS on
lightphone/light-sdk's tool scaffold. This fork added article/list images, subscribing by
QR code instead of typing a URL on the phone keyboard, an in-app reader for the page an
article links to (with session-carrying sign-in for sites that gate content), and
hardware-wheel scrolling. v2.0.0 absorbed LightNews,
which was a separate newsletter app; that repo is retired. Work happens on the
images-and-qr branch, which is this repo's default branch — see
Branches and CI, because the trigger layout
here does not match the rest of the portfolio.
A Gmail label is a feed. A subscribed label is a
FeedEntitywithsourceType = "GMAIL", and each of its messages is anArticleEntitylike any other. That is the whole design: the home list, search, saved items, read state, the article cache and the trim policy are the ones that were already there, and the second source cost a sync (NewsletterSync) and a renderer (NewsletterHtml) rather than a second app. OnlyRssRepository.refreshFeedInternalbranches on where an article came from.Three things changed shape on the way in, and each is worth knowing about:
- Sign-in moved into the app. LightNews opened Google's consent page in the system browser and waited for a custom-scheme redirect through an intent filter on its
MainActivity. A Light SDK tool ships no manifest and owns no activity, so there is nothing for the OS to hand a redirect back to.GmailSignInScreenloads the consent page in a WebView the tool owns and catches the redirect inshouldOverrideUrlLoadingbefore it is ever fetched — so it needs no browser, no intent filter and no resolvable address, and the authorization code never leaves the process. This removes the single largest unknown in LightNews's release notes.- Newsletter bodies became a table. LightNews wrote them to
filesDir, which cost it a staging-write dance, an orphan sweep and ahasBodycheck on every sync.newsletter_bodiesis a child table withON DELETE CASCADE: the body cannot outlive its article and cannot go missing while the row survives, and all three mechanisms are gone.- Background sync went away. LightNews polled hourly through WorkManager. Newsletters now refresh with everything else, when you open the app or pull the refresh button, which is what the RSS side always did.
Read state still round-trips to Gmail. Opening an issue clears
UNREADon the server, andArticleEntity.pendingReadis what makes that honest offline — the flag flips locally first, the push happens on the next sync, and a row carrying an unpushed read is excluded from both directions of reconciliation so a stale server answer cannot erase it.
Nothing outside a label you explicitly follow is ever read. The app asks for gmail.modify,
which is a restricted scope, because clearing UNREAD is a write — point it at an account
you don't mind it touching first.
- In Gmail, filter your newsletters into a label.
LightNewsletteris a reasonable name. - In Google Cloud, create a Desktop app OAuth client and enable the Gmail API. Leave the
consent screen in Testing and add your address under Test users. Publishing it to
production is the wrong move here and worth understanding:
gmail.modifyis a restricted scope, and an unverified app in production is blocked from requesting one outright — Google answers the consent screen with a flatAccess blocked … invalid_request. Testing works, at the price of a grant that expires after seven days. The app treats the resultinginvalid_grantas "ask for consent again" rather than retrying forever, so the cost is re-running step 4 weekly until you submit the app for verification. - On the phone: News → Newsletters → list button → Add client ID. Type it, paste it, or scan it as a QR code off another screen.
- Sign in, then + to pick the label.
Or skip the phone entirely: run scripts/authorize.py on a computer, and scan the QR code it
prints. That carries a refresh token straight in and never opens a consent screen on the
device.
Reading an issue, the top and bottom bars slide away once you are scrolling forwards — seven grid units of a twelve-unit panel, given back to the document. They return near the top of the issue and on any deliberate scroll up, which matters because the back button lives up there.
Two render modes, one keypress apart in the reader. DARK forces white-on-black and unwraps the layout tables so the copy reflows to the panel — the better read, but it hides dark logos drawn on transparent backgrounds. PAPER keeps the newsletter's own design and only fixes the width, which is what brand-heavy issues want; a panel showing black on white is the closest thing to the paper these were designed for. Sponsor blocks are cut by default and a marker is left where each one was, so a wrong guess is visible rather than silent.
The Light Phone III's panel is a full-colour AMOLED. Its black-and-white look is Android's accessibility daltonizer pinned to simulate-monochromacy — a SurfaceFlinger colour matrix over everything on screen — and LightOS lifts it itself for photos and video. News does the same while a picture is on screen, and puts it straight back when you leave the article or the app.
It needs one grant, once per install, because WRITE_SECURE_SETTINGS is
signature|privileged|development:
adb shell pm grant com.lightrss.reader android.permission.WRITE_SECURE_SETTINGSWithout it every call quietly does nothing and pictures stay grey — the SecurityException is
swallowed, so the app degrades rather than breaks. Settings → COLOUR turns it off if you would
rather the phone never changed. Colour is only held where there is something to see in it: an
article with images, an extracted page with images, an HTML newsletter. A text-only article never
touches the setting.
The mechanism is a straight port of ColorMode from LightCamera,
itself a port of LightChat's — the reference counting and foreground handling are load-bearing and
were arrived at the hard way. Two things differ because this is an SDK tool rather than an app: the
android.content.Context import and the contentResolver access are blocked by the SDK build
policy and are marked exempt line by line rather than by deleting the rules (grep -rn 'light-sdk-allow' tool/src finds all four), and there is no Application to hook, so the
foreground handling comes off the screen's own lifecycle instead.
git clone https://github.com/gi-os/LightRSS.git
cd LightRSS
export JAVA_HOME="<path-to-jdk-17>"
export ANDROID_HOME="<path-to-android-sdk>"
export GH_PACKAGES_USER="<github-username>"
export GH_PACKAGES_TOKEN="<token-with-read:packages>"
./gradlew :tool:testDebugUnitTest :tool:lintDebug :tool:assembleDebugThe GitHub token is required even for a local build — the Light SDK keyboard dependency
is hosted on GitHub Packages, and an unauthenticated maven.pkg.github.com request fails
with 401. gpr.user/gpr.key in local.properties work instead of the env vars; never
commit either. The debug APK lands in tool/build/outputs/apk/debug/.
To sideload immediately without building, grab a release APK instead:
adb install -r LightRSS-<version>.apk # from github.com/gi-os/LightRSS/releasesor track the repo in Obtainium. INSTALL.md has the signing fingerprint to verify against.
Unlike every other repo in this portfolio, a push to LightRSS's default branch does not build or release anything. Verified directly from the workflow files:
.github/workflows/build.yml(Trusted build: test, lint,assembleDebug) triggers onlyon: push: branches: [main]..github/workflows/release.yml(Release APK, the one that publishes a signed GitHub Release) triggers onlyon: push: tags: ['v*'], and fails on purpose if the tag doesn't matchversionNameintool/lighttool.toml.- The repo's default branch — confirmed via
git branch --show-current/remote show originon a fresh clone — isimages-and-qr, which sits one commit ahead ofmainon unreleased work and is not a target of either workflow.
So: pushing to images-and-qr alone runs no CI at all. To actually test a change, push
the same commit to main as well:
git push origin images-and-qr:images-and-qr # keeps the default branch current
git push origin images-and-qr:main # runs build.yml (test + lint + assembleDebug)To cut a real release, bump both versionName and versionCode in
tool/lighttool.toml, then tag:
git tag v1.13.0
git push origin v1.13.0 # runs release.yml, publishes the signed APK- Subscribing. Subscriptions → + → paste a URL, use the on-screen keyboard, or
Scan QR code. The scanner accepts a bare URL, a
feed:///rss://scheme, or a code that wraps an address inside other text. gi-os.github.io/LightRSS generates a matching code from any feed or site address, client-side. - Images (this fork's main addition). A thumbnail per article row and full-width
images in the reader, downsampled to the width the panel needs and shown in colour — see
Colour. Pulled from
enclosure,media:content,media:thumbnail,itunes:imageand inline<img>markup; 1x1 tracking beacons,data:URIs and known tracking hosts are dropped. Lazy, on-screen-row-only downloads into an 8 MB memory cache / 24 MB disk cache. A Settings switch disables the whole feature. - Reader mode. OPEN fetches the linked article and renders it with no WebView — no script, ad or tracking pixel loads. If a site answers with a bot check or a paywall, SIGN IN opens that one page in the app's only WebView, keeps the cookies it hands back per host, and records the user agent that earned them (the two travel together); subsequent reader fetches for that host send both, feed refreshes never do.
- The wheel scrolls the list, the article, the reader page and the settings menu.
It needed one addition to the vendored SDK: a tool has no window of its own
(
LightActivityowns it, and build rules block tool code from the window/LocalView), soLightHardwareKeysinsdk/clientis a seam called fromLightActivity.dispatchKeyEventbefore the event reaches the view hierarchy — the only place a key can be claimed ahead of a focused WebView or text field. Both halves of a notch are consumed there; an unclaimed key in an SDK tool is otherwise forwarded to the LightOS server, which reads a turn as a brightness change. LightControl (optional, separate app) owns the wheel click, the camera button and brightness phone-wide, and passes bare turns through tocom.lightrss.reader(alongsidecom.gios.*, LightFastread and LightPhono) so per-notch scrolling inside the app is unaffected by installing it.
ArticleList keys its LightLazyScrollView rows by article id
(items(articles, key = { it.article.id })), so a sync inserting newer articles at the
top of the feed doesn't silently anchor the viewport to what used to be the first row —
without a stable key, LazyColumn/LightLazyScrollView can throw on reordering, or
just leave new rows sitting above where the user is looking.
HomeViewModel pairs that with a jumpToNewest counter: a jumpPending flag set once
at construction and again in onAppPause() is consumed in onScreenShow() by bumping
jumpToNewest, which the screen observes to call listState.scrollToItem(0). The net
effect: opening the app, or coming back to it after it was backgrounded, lands on the
newest article; returning from the in-app reader does not re-trigger the jump, because
onAppPause() only fires for the screen actually on top, so a half-read list keeps its
place instead of snapping back to the top underneath you.
RSS 2.0/Atom/RDF-RSS parsing with namespaced content, feed/site-URL discovery,
ETag/Last-Modified conditional refresh, a local Room database for subscriptions and
articles, unread/all/per-feed/saved/archive views, local search, and defensive XML
parsing (DTD processing and external entities off). Removable NASA/BBC World/Hacker News
subscriptions ship by default.
./gradlew :tool:testDebugUnitTest :tool:lintDebug :tool:assembleDebugTo run in the SDK emulator: set serverPackage = "com.thelightphone.sdk.emulator" in
tool/lighttool.toml, follow docs/system_app/README.md, then set it back to
com.lightos before a device build. lighttool.toml requests INTERNET and CAMERA
(the QR scan screen only). Local debug/release builds use the SDK's public development
keystore — not production artifacts; the release workflow supplies the real one from
LIGHTRSS_KEYSTORE_BASE64.
No account, no analytics, no server of LightRSS's own. Feed hosts see an ordinary
request with the LightRSS/1.1 (Light Phone III) user agent; with images on, image
hosts see requests for on-screen articles only. Unfollowing removes local articles;
uninstalling removes the database. Full disclosure in PRIVACY.md.
Read CONTRIBUTING.md before opening an issue or PR, and
SECURITY.md for reporting a vulnerability privately rather than in a
public issue. The tool id com.lightrss.reader is treated as permanent — confirm
ownership before any first official Tool Library submission, since community-tool
distribution guidance from Light keeps changing (see the upstream SDK docs before
relying on anything stated here). CHANGELOG.md and
THIRD_PARTY_NOTICES.md track detail this README doesn't.
Tag → commit, from git tag --sort=-creatordate / git log. Several releases fold in
one or more untagged commits that shipped as part of them; those are noted.
| Version | Commit | Change |
|---|---|---|
| v2.3.0 | — | Pictures show in colour on a phone that stays grey, via the daltonizer and a one-time adb grant |
| v2.2.0 | — | Images keep their colour. The app no longer flattens them; the phone's own daltonizer does that, and can be switched off |
| v2.1.0 | — | The newsletter reader's bars get out of the way while you read forwards, and come back on any scroll up |
| v2.0.1 | — | A stored OAuth client can be seen, replaced and removed; signing out now takes the cached issues with it |
| v2.0.0 | 29e3685 |
Renamed to News. Absorbed LightNews: Gmail labels are feeds, newsletters read in the app's own reader, sign-in moved into an in-app WebView, bodies moved from files to a cascading table, hourly WorkManager polling dropped |
| v1.12.0 | c472d9e |
Scroll with the brightness wheel |
| v1.11.1 | b3c3ec3 |
Put the article actions at the end of the text, not in a fixed bar |
| v1.11.0 | 76ecc8a |
Release 1.11.0 — includes ab2a0fc, open on the newest article and let home follow only favourite feeds (the keyed-list fix above) |
| v1.10.0 | c86ca3c |
Keep markup out of article text, and drop the adaptive icon layer |
| v1.9.0 | a757f60 |
Add a launcher icon in the sibling tools' style |
| v1.8.0 | 94f3548 |
Use the screen's url in the sign-in WebView, not WebView.getUrl() |
| v1.7.0 | db996fe |
Recognise bot checks and offer to open the link for signing in — includes b1719c9, sign in inside the app and reuse the session for reader fetches |
| v1.6.1 | 7f4b42e |
Restore initialize() and addFeed(), lost in the reader-page edit |
| v1.6.0 | 040d108 |
Add a reader view for the page an article links to — includes 1c170af, open the real article and get past sites that refuse the reader |
| v1.5.0 | b14ca45 |
Paste and the phone keyboard for feed entry, plus an Android camera prompt |
| v1.4.0 | 10a3693 |
Run the camera the way LightPass runs it |
| v1.3.0 | 55c2840 |
Handle the camera permission the way LightPass does |
| v1.2.1 | 149278e |
Stop the scanner dead-ending when LightOS refuses the permission check |
| v1.2.0 | 0751349 |
Fix the QR scanner in release builds, enlarge images, add a code generator |
| v1.1.0 | d3cdc0a |
Pick a single apksigner when several build-tools versions are installed |
Earlier, untagged commits (83a35e6 "Ship signed sideload APKs on tag", f5ee3c5 "Add
feed images and QR-code feed subscribing", 3a9745a "Document Light RSS workflows and
harden repository hygiene") are the initial fork work, predating the current tag/release
scheme; the commits below that point are unmodified upstream zachattack323/LightRSS
and lightphone/light-sdk history.
- zachattack323 wrote the upstream LightRSS: the parser, Room schema, repository layer, conditional refresh, and the original LightOS screens.
- lightphone/light-sdk and lightphone/light-keyboard supply the client library, UI kit, keyboard and builder (MIT).
- gi-os/LightPass supplied the camera code —
the SDK scanner wouldn't start reliably in a release build, so
RssScanner.ktnow owns a CameraXLifecycleCameraControllerand reads the camera permission withContext.checkSelfPermission, the same way LightPass does. - gi-os/LightQR supplied the browser-side QR
generator behind
docs/index.html. - Obtainium handles update checks.
MIT, the same as upstream. See LICENSE.