# 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 at once, then the folder pages in lazily (random page order, shuffled within each page). OK on Continue: folder grid. ### Folders Grid of all folders as avatar tiles (`avatarUrl`). OK: folder grid. ### Folder grid - Lazy: `GET /api/folders/:slug/images` (60 per request) pages are fetched in a random order (page numbers from `imageCount`, empty pages skipped), each shuffled locally; the next page loads when the last visible tile is within 12 of the end. The API has no random mode for folders. - Ceiling: not a full shuffle, neighbours within a page stay together. - OK: viewer at that item, continuing through the same list and pager. ### 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). - Optional app PIN, 4 to 8 digits, typed twice; Remove PIN when one is set. - Header with a back arrow, like the folder grid. ### Splash (PIN lock and indexing wait) - A splash with the logo covers any screen at app start and whenever the app comes back after leaving the screen (an Application lifecycle counter tracks started activities). Home connects only after it. - When a PIN is set it asks for it first. - Then it checks `GET /api/scan-progress`: while `isScanning`, it stays with a spinner, "Library is indexing" and the server's phase message plus the derivative job counter, polling every 5 s; Foldergram is unusably slow while indexing. Idle: it closes at once. - Unreachable at the first check: it lets the user in so Home shows its error with Retry. After a scan was seen, failed polls keep waiting. - A scan that starts while the app is open does not interrupt it. - Back on the lock screen leaves the app. - Stored as salt plus SHA-256, never the PIN. It is a privacy screen, not protection against someone with adb or root on the TV; clearing the app data removes it. ## Architecture | Unit | Job | | --- | --- | | `Api` | `HttpURLConnection` + `org.json`. Folders, folder images, feed, reels, likes, like toggle (`POST`/`DELETE /api/images/:id/like`, the alias older instances also have), 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. | | `FolderActivity` | One folder as a grid, pages fetched lazily in random order. | | `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.