# mygram-droid design Date: 2026-10-22 Native Android TV client for a self-hosted Foldergram instance. Lean-back browsing of folders, photos and (mostly vertical) videos with the remote, plus shared likes. ## Target - Chromecast with Google TV HD: Google TV 14 (API 34), 32-bit ARM, about 1.4 GB RAM. - Foldergram on the LAN, plain HTTP, default port 4141. The app allows cleartext traffic. - The instance runs in viewer-password mode: every route, including `/thumbnails/...` and `/previews/...`, needs a session. ## Stack - Java, minSdk 34, one `app` module. - Built in a throwaway Docker container (JDK 17, SDK 34), same pattern as the `tv-debloat` launcher. - Dependencies: Glide (images) and Media3 ExoPlayer (video). Everything else from the platform: `HttpURLConnection`, `org.json`, `SharedPreferences`, `RenderEffect`, `MediaMetadataRetriever`. ## Visual style - Icons instead of text buttons wherever an icon is clear: top menu, Continue tile, Retry / Change instance / Rescan / manual entry, viewer overlays (pause, heart). - Icons are Material Symbols vector drawables (Apache 2.0) copied into `res/drawable` as XML, no icon-font or library dependency. - The focused icon shows its label in small text beside or below it, so icon-only controls stay readable from the couch. Unfocused icons show no text. - Every icon has a `contentDescription` (TalkBack). - Text stays where content is text: instance names, URL and password fields, error messages, Settings values. ## Screens ### Instance picker (first launch, or "Change instance" in Settings) - Scans the TV's /24 subnet on port 4141: `GET /api/health` on every address, about 32 in parallel, short timeout, about 3 to 5 s total. - A host is accepted only if the reply is JSON with `ok: true` and a `storage` object. - Each result shows its reverse-DNS name when available, else the IP. - Extra entries: "Enter URL manually", "Rescan". - On selection: `GET /api/auth/status`. If `accessMode` is `password`, prompt for the password and `POST /api/auth/login`. If `public`, no password. Save URL and password, then open Home. - Limits: misses instances on another port or behind a reverse proxy; manual entry covers those. ### Top menu Icon bar, always visible except in the viewer. Up from the content reaches it. ``` [Foldergram] [Reels] [Liked] [Folders] [Settings] left centered right ``` - Home is the Foldergram logo, loaded at runtime from the instance's `/icon-192.png` (via Glide, cached). It is not bundled: Foldergram is AGPLv3 and its assets do not go into this GPLv2-only APK. Fallback: the Material home icon. - Left / Right moves focus across all five in that order. ### Home - Background: a random image from `GET /api/feed?mode=random`, blurred and scaled up, crossfading every N seconds (Settings, default 15 s). - One horizontal row per folder from `GET /api/folders`, ordered by `latestImageMtimeMs` descending, rows loaded lazily on scroll. - Each row: about 10 random items from that folder, then a **Continue** tile. - OK on an item: viewer at that item, continuing through the folder's shuffled list. OK on Continue: folder grid. ### Folders Grid of all folders as avatar tiles (`avatarUrl`). OK: folder grid. ### Folder grid - Fetches every page of `GET /api/folders/:slug/images` (60 per request) and shuffles locally once per open. The API has no random mode for folders. - OK: viewer at that item, continuing in grid order. ### Liked - Grid from `GET /api/likes`, newest like first. - OK: viewer in that order. - Posts unliked in the viewer are gone from the grid on return. ### Reels Opens the viewer directly on `GET /api/reels?mode=random&seed=`, videos only, endless, paged with the same seed. ### Viewer (fullscreen) - Item centered at its own aspect ratio over a blurred, scaled-up backdrop that fills the side bars. - Photo backdrop: the photo's own preview, decoded small (about 200 px) and blurred with `RenderEffect`. - Video backdrop: the server thumbnail, blurred, shown at once. It is then replaced by a frame from the preview mp4 at a random time between 20 % and 80 % of `durationMs` (`MediaMetadataRetriever.getFrameAtTime`, off the main thread, cancelled on post change). On failure the thumbnail stays. First and last frames are avoided because they are often black. - Remote: - Up / Down: previous / next post. - Left / Right: seek -10 s / +10 s on video; previous / next slide on a carousel post. - OK: pause / play, applied immediately. - Double OK (second press within about 300 ms): undo that pause toggle and toggle the like instead. A large heart flashes in the center. - Back: exit. - Auto-advance: video end, or photo timer (Settings, default 8 s), goes to the next post. Pausing stops the timer. - Thin progress bar along the bottom edge for photos (timer) and videos (playback position). Frozen while paused. - Small pause icon at the bottom center while paused. - Small heart at the bottom right while the post is liked. - Media: `previewUrl` (images up to 1500 px wide, video mp4). One ExoPlayer instance, reused across posts. ### Settings - Change instance (reopens the picker). - Viewer password. - Photo duration (default 8 s). - Background interval (default 15 s). ## Architecture | Unit | Job | | --- | --- | | `Api` | `HttpURLConnection` + `org.json`. Folders, folder images, feed, reels, likes, like toggle, health, auth status, login. Resolves relative media URLs against the server URL. Sends `x-foldergram-intent: 1` on mutations. | | `Discovery` | Subnet scan for the instance picker. | | `Post` | Data class from feed-item JSON: `id`, folder, `mediaType`, width/height, `durationMs`, `thumbnailUrl`, `previewUrl`, `postType`, `mediaItems` (carousel slides). | | `Likes` | In-memory set of liked post IDs, loaded from `/api/likes` at start, updated on each toggle. Drives every heart indicator. | | `MainActivity` | Top menu and content area; Home, Liked, Folders, Settings as Fragments. | | `HomeFragment` | Vertical RecyclerView of horizontal rows, rotating background. | | `FolderGridActivity` | One folder, all pages, shuffled grid. | | `ViewerActivity` | Plays a list of posts from a start index, or pages an endless source (Reels). Backdrop, progress bar, pause icon, hearts, double-OK. | | `Prefs` | `SharedPreferences`: server URL, password, photo duration, background interval. | - Lists reach the viewer through a static in-memory holder; intents cannot carry hundreds of posts. - Session: `CookieHandler.setDefault(new CookieManager())` at app start. `Api`, Glide's default fetcher and ExoPlayer's `DefaultHttpDataSource` all use `HttpURLConnection` and share the cookie. `MediaMetadataRetriever` gets the `Cookie` header passed explicitly. - Login runs on app start with the stored password; the cookie lives in memory only. - Memory: Glide decodes at view size, backdrops decode small, the viewer uses previews not originals. ## Errors - Server unreachable or login fails at start: full-screen message with Retry and Change instance. Never a crash. - `401` mid-session: one silent re-login and retry; on second failure open Settings. - Thumbnail load failure: plain dark tile. - Viewer media failure: message over the blurred thumbnail, auto-advance after the photo timer. - Like toggle failure: revert the heart, show a toast. - Page fetch failure in Reels: retry on the next Down press. ## Testing - One JVM unit test covering: `Post` parsing (single image, video, carousel), URL resolution, Foldergram health-reply detection, and the double-OK logic as a plain class with an injected clock. - On-device check for every UI change: `KEYCODE_WAKEUP`, drive with `adb shell input keyevent`, inspect with `adb exec-out screencap -p`. - Before the first build, probe the live instance with `curl` to confirm field names (`postType`, `mediaItems`, `/api/likes` item shape) instead of trusting the upstream docs. ## Out of scope Search, stories, moments, places, collections, admin features, uploads and deletes, phone or tablet layouts, mDNS discovery.