1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
# 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=<random>`,
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.
|