Aangan

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

FileWhat it is
HomeActivityThe home screen: clock, date and countdown, the weather pill, the dock and app grid, the app menu, and the scene layer.
Apps, DockThe TV's launchable apps, and the four in the dock: filled once, then changed only by you.
Ambient, BackdropThe focused app's coloured glow, and the background painted once into a bitmap.
Weather, WeatherGlyphOpen-Meteo forecast and air quality, the CPCB and EPA scales, and line-art weather icons.
TodayCardThe sheet behind the weather pill: hours, sun, UV, moon, the PM2.5 chart and what's coming up.
FestivalThe calendar: festivals from the CSV with the family's moves and hides on top, plus personal days.
FestiveEvery scene, drawn on a canvas, with its opening and gentle loop.
People, PlaceThe family's days, and the chosen city with its search: popular cities, older names, and results ranked by country and size.
SetupActivity, CityActivity, WelcomeActivitySettings, the city picker with results as you type, and the first-run welcome.
PhoneSetup, QrCodeThe phone setup page's tiny HTTP server and pages, and the QR encoder.
HomeButtonThe accessibility service that answers the Home button.
ClockDreamThe screensaver.
CrashThe 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, or PLAIN for 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

  1. Add a value to Festive.Theme with its glow and accent colours.
  2. Add a case to the drawing switch in Festive, composed from the existing pieces (stars, moon, diyas, lights, toran and so on) or a new drawing method.
  3. Use the timing helpers: part() for the opening, sway() and flicker() for the loop. Keep the clock, date, weather pill and app tiles clear.
  4. Use the scene in festivals.csv, preview it with am start … --es festival, and add it to SCENES in tools/site.py.

Stored settings

Everything lives in one SharedPreferences file, aangan, with backup disabled:

KeyHolds
dockJSON list of four package names, left to right; "" for an empty place
hiddenPackage names of hidden apps
peopleJSON list of {m, d, birthday, who}
festival_editsJSON object keyed "2026-11-08|Diwali": {date} and/or {hidden}
placeThe chosen city as JSON
weather keysThe last forecast and air readings, with their times
welcomed, tips2First-run state

Security model

The phone setup page

  • Lifetime: a server socket on port 8737 (or any free port) opens in SetupActivity.onResume and closes in onPause.
  • 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_HOME only and returns every other key unhandled.
  • Window events are limited by android:packageNames to 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.py draws the icon and banner and writes res/drawable/icon.xml, banner.xml and the SVGs. The wordmark is Fraunces, as a path in tools/wordmark.txt.
  • python3 tools/site.py builds this website from site/ into build/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.