Developers
Small enough to read in an afternoon.
Aangan is about 6,700 lines of plain Java with no third-party libraries and no Gradle. It builds with one shell script and the Android SDK. Here's how it fits together.
Principles
- No dependencies. Only the Android framework and
org.json. Even the QR code encoder is ours, in 268 lines. - No work while you watch. Nothing animates on an ordinary day; scenes and the focus glow run only while the home screen is showing; the network is touched only while it's on screen.
- No data leaves the TV except a forecast request for a city the user chose. A new kind of request is a privacy-policy change, made first.
- Drawn, not downloaded. Scenes, weather icons and the logo are drawn in code or as vectors, which keeps the app around 150 KB.
- Readable over clever. Every file starts with a comment saying what it is and why.
Building
You need the Android SDK (build-tools 37.0.0, platform android-37.2) and JDK 17 or later. By default the script looks in ~/.local/share/android; set ANDROID_HOME to point elsewhere.
git clone https://github.com/vaari-dev/aangan
cd aangan
./build.sh # build/aangan.apk, signed with a local development key
./build.sh install # and install it over adb (set ANDROID_SERIAL with several devices)
./build.sh bundle # also build/aangan.aab for Google Play
The script runs aapt2 → javac → d8 → zipalign → apksigner, in about ten seconds. The bundle step fetches a pinned bundletool once and checks its SHA-256 digest.
To preview a festival's scene on a connected TV, with Aangan open:
adb shell am start -n dev.vaari.aangan/.HomeActivity --es festival Diwali
Any festival name from the calendar works, as does a family day such as "Asha's birthday". Press Back for Settings, or Home to clear it.
Releases
Release builds are signed with the project's key, kept off any repository:
AANGAN_KEYSTORE=~/keys/aangan-release.jks AANGAN_KEY_PASS=… ./build.sh bundle
The same key signs the APK on GitHub Releases and is the Google Play app-signing key, so either can update the other. Raise VERSION_CODE and VERSION_NAME in build.sh for each release.
Source map
| File | What it is |
|---|---|
HomeActivity | The home screen: clock, date and countdown, the weather pill, the dock and app grid, the app menu, and the scene layer. |
Apps, Dock | The TV's launchable apps, and the four in the dock: filled once, then changed only by you. |
Ambient, Backdrop | The focused app's coloured glow, and the background painted once into a bitmap. |
Weather, WeatherGlyph | Open-Meteo forecast and air quality, the CPCB and EPA scales, and line-art weather icons. |
TodayCard | The sheet behind the weather pill: hours, sun, UV, moon, the PM2.5 chart and what's coming up. |
Festival | The calendar: festivals from the CSV with the family's moves and hides on top, plus personal days. |
Festive | Every scene, drawn on a canvas, with its opening and gentle loop. |
People, Place | The family's days, and the chosen city with its search: popular cities, older names, and results ranked by country and size. |
SetupActivity, CityActivity, WelcomeActivity | Settings, the city picker with results as you type, and the first-run welcome. |
PhoneSetup, QrCode | The phone setup page's tiny HTTP server and pages, and the QR encoder. |
HomeButton | The accessibility service that answers the Home button. |
ClockDream | The screensaver. |
Crash | The app's Application: if Aangan crashes, it writes a short note to files/crash.txt on the TV (versions, TV model, stack trace) and sends nothing. Settings and the phone page offer to report it on GitHub, which the person does themselves, or delete it. |
Other files: assets/festivals.csv (the calendar), assets/phone/ (the phone page's stylesheet, font, logo and the small script that suggests cities as you type), res/drawable/ (the icon and banner) and res/xml/home_button.xml (the service's limits).
The festival calendar
assets/festivals.csv has one line per festival day:
# date,name,greeting,scene[,expected]
2026-11-08,Diwali,Happy Diwali,DIWALI
2027-03-10,Eid al-Fitr,Eid Mubarak,EID,expected
- scene is a name from
Festive.Theme, orPLAINfor the greeting alone. - expected marks a date that depends on a moon sighting.
- Days between two entries with the same scene, up to nine days apart, share it, so Diwali week and Navratri glow throughout.
- Dates follow Drik Panchang for New Delhi; national days and Eid follow the Government of India's list. Add each year once the calendars are published.
- A family's moves and hides are keyed by the original date and name, so they survive calendar updates as long as that line doesn't change.
The festival page on this site is generated from the same file.
Adding a scene
- Add a value to
Festive.Themewith its glow and accent colours. - Add a
caseto the drawing switch inFestive, composed from the existing pieces (stars, moon, diyas, lights, toran and so on) or a new drawing method. - Use the timing helpers:
part()for the opening,sway()andflicker()for the loop. Keep the clock, date, weather pill and app tiles clear. - Use the scene in
festivals.csv, preview it witham start … --es festival, and add it toSCENESintools/site.py.
Stored settings
Everything lives in one SharedPreferences file, aangan, with backup disabled:
| Key | Holds |
|---|---|
dock | JSON list of four package names, left to right; "" for an empty place |
hidden | Package names of hidden apps |
people | JSON list of {m, d, birthday, who} |
festival_edits | JSON object keyed "2026-11-08|Diwali": {date} and/or {hidden} |
place | The chosen city as JSON |
| weather keys | The last forecast and air readings, with their times |
welcomed, tips2 | First-run state |
Security model
The phone setup page
- Lifetime: a server socket on port 8737 (or any free port) opens in
SetupActivity.onResumeand closes inonPause. - Authorisation: every path is under a 10-character secret drawn from a 32-letter alphabet with
SecureRandom(50 bits), new each time Settings opens and shown only on the TV. Anything else gets a plain "link expired" page that links to nothing. - Input: bodies are capped at 8 KB; names at 40 characters; dates are validated, and festival moves are limited to a year either side of the calendar's date. All output is HTML-escaped.
- Reach: the server listens only on the TV's home-network address, not on every interface (such as a VPN or a public IPv6 address). It refuses any request that doesn't name that address as its host, which stops a web page elsewhere from pointing its own domain at the TV ("DNS rebinding").
- Limits: a request's line and headers are capped at 8 KB, its body at 8 KB, and the whole request must arrive within 10 seconds, so a slow or endless sender can't hold the server. City searches are capped at 100 characters and recent ones are cached. A city from the phone must have real coordinates and a known time zone, and there can be at most 200 family days.
- Headers:
Cache-Control: no-store,Referrer-Policy: no-referrer,X-Content-Type-Options: nosniff, and a Content Security Policy that lets the page load only the TV's own stylesheet, font, logo and script, and send forms and requests only to the TV. No cookies and no outside resources. - Script: one small file of the page's own, for city suggestions and the dock's choices. It builds everything with
textContent, never HTML, and the page works without it. - Transport: plain HTTP on the local network. The threat model assumes a trusted home network; an attacker who can watch it during setup could see what's entered there, but not reach the page later.
The Home-button service
- Key filtering consumes
KEYCODE_HOMEonly and returns every other key unhandled. - Window events are limited by
android:packageNamesto the stock launchers (Google TV, Android TV, Leanback, PatchWall). canRetrieveWindowContent="false": it can't read any screen.- It starts one activity, Aangan's, and does nothing else.
Network
usesCleartextTraffic="false": outgoing requests must use HTTPS.- Coordinates are rounded to two decimals before they're sent.
Found a problem? Please report it privately through GitHub's vulnerability reporting.
Icon and website
python3 tools/icon.pydraws the icon and banner and writesres/drawable/icon.xml,banner.xmland the SVGs. The wordmark is Fraunces, as a path intools/wordmark.txt.python3 tools/site.pybuilds this website fromsite/intobuild/site/; CI publishes it to GitHub Pages on every push to main.
Contributing
- Open an issue first for anything large, so we can agree on the shape.
- Keep to the principles above: no libraries, no tracking, no work while watching.
- Match the surrounding code: names, comments and idiom.
- Test on a real TV if you can, and say which one in the pull request.
- By contributing, you agree to license your work under the MIT licence.