Program root for the native Android rewrite (Compose mobile + Android TV). Every launch-gating work item is a native sub-issue here — the tree and progress bar span this repo and putio-sdk-kotlin. Post-v1 upgrades live under #52 and do not gate this epic. Cross-program console: Frontend project . Sibling programs: Apple · TV web · www .
Current rollout status: 2026-10-01
Everything an agent can finish without an owner call is merged; what's left waits on Altay, hardware or credentials.
Mobile (emulator harness): Auth Tab sign-in, Files, Trash, Transfers with magnet and .torrent intake, Search and History, video and audio playback with resume and MediaSession, downloads, share and deep links, settings, accessibility, inactive-account notice. Missing: Chromecast (Chromecast support in the mobile app (VPN/tunnel-safe) #36 ) and the release lane (Release signing and publishing pipeline in CI (GitHub Environments, SOPS boundary) #49 → Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 ). Still builds as io.put.putio.mobile. Auth Tab live proof (auth: API 37 Auth Tab drops the OAuth callback on Chrome 145 #107 ) needs one human devs-auto sign-in.
TV (emulator harness, devs-auto live proof): device-code sign-in, browse, and Media3 playback with seek, Back stack, resume, tracks, subtitles, MediaSession, conversion and autoplay (feat(tv): play Files media on Media3 with a minimal TV player #217 –feat(tv): publish a media session, type playback errors and show the conversion interstitial #222 , Wave 6). Launcher filter and tv-native session carry-over fixed (fix(tv): add MAIN to the LEANBACK_LAUNCHER filter so TV home lists the app #251 , feat(tv): keep tv-native sessions and playback type on upgrade #252 ). Left: Fire TV remote and oracle 17–31 parity (TV playback parity on Media3 with the Back-button state machine #34 L6) and audio passthrough (Android TV parity: subtitle state, overscan safe area, and audio passthrough #45 ), both on Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 ; release lane TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 .
Audit mobile: tablet layout drops the Share file action #183 –docs: behavior test citations, bootstrap hint, and dead player code #194 : closed except tv: Open in VLC hands the account token to an unverified package #185 (VLC handoff, owner call).
SDK: still a composite build. Maven Central waits on release: publish to Maven Central so consumers pin a version instead of a composite build putio-sdk-kotlin#43 ; then Consume putio-sdk-kotlin from Maven Central instead of the composite build #148 pins io.put:putio-sdk-kotlin (the app still declares io.putdotio).
Owner gates: Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46 release custody, Confirm GT America licensing for Android app distribution (owner: Altay) #48 fonts, Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 devices, release: publish to Maven Central so consumers pin a version instead of a composite build putio-sdk-kotlin#43 Central credentials.
Autonomous batch: 2026-09-29 → 2026-10-01 (done)
Every batch PR merged; none open or abandoned:
W5 audit, CI and harness: chore(github): add PR template and issue forms #195 –fix(share): delete earlier-process exports when a restored session ends #216 (mobile: tablet layout drops the Share file action #183 , auth: mobile secure-storage failure blocks sign-in until app data is cleared #184 , downloads: progress never advances, reconcile outlives close, engine untested #186 –docs: behavior test citations, bootstrap hint, and dead player code #194 , Repo modernization: secret scanning, homepage, PR/issue templates #172 , Make ./gradlew verify the single gate by moving buildSrc to an included build #180 , harness: contract tests depend on host emulator ports, a host-global lock, and bash 4 #197 , tv: fix existing lint errors and add TV lint to verify #204 , share: session-exit cleanup can delete the next session's export #211 , share: sign-out after a restored session leaves earlier-process exports on disk #215 , Measure foreground transfer polling with deep historical lists #121 )
TV playback L1–L5 and fixes: feat(tv): play Files media on Media3 with a minimal TV player #217 , feat(tv): add the player seek bar, D-pad scrubbing and the Back overlay stack #219 –feat(tv): publish a media session, type playback errors and show the conversion interstitial #222 , fix(playback): ask HLS for every subtitle rendition so hidden subtitles stay pickable #227 , fix(tv): keep the choice dialog title outside its scrolling rows #228 , fix(harness): wait on fake emulator state in the Google TV stop contract test #230 , fix(playback): keep subtitles off until account settings load #231 (TV playback parity on Media3 with the Back-button state machine #34 , TV player hides the Subtitles button when the account hides subtitles #223 –harness: Google TV stop contract test races on port 5556 #226 , playback: subtitles show for hide_subtitles accounts before settings load #229 )
Design fetch and refactors: build(design): fetch design assets from a locked @putdotio/design release #218 (Standalone clone: drop sibling-checkout path dependencies #173 design half), docs: fix behaviour test citations, bootstrap hint, and toolchain drift #232 (docs: behavior test citations, bootstrap hint, and dead player code #194 ), refactor: hold session-scoped controllers in one SessionScopedHolder #233 –refactor(mobile): move flat mobile files into feature packages #235 (refactor: one session-scoped holder and split mobile PutioApp.kt #193 )
Wave 6 UX audit: fix(tv): add MAIN to the LEANBACK_LAUNCHER filter so TV home lists the app #251 –feat(files): remember the Move and Copy target folder on the device #277 (playback: start MP4 conversion when the viewer opens a file that needs it #236 –files: remember the Move and Copy target folder #276 ), Dependabot chore: bump the gradle-patch-minor group with 21 updates #279
SDK: feat(files): add getDownloadUrl for API-issued download URLs putio-sdk-kotlin#55 , feat(sharing): add sharing and public-links namespace putio-sdk-kotlin#56 , docs(files): describe NotAvailable conversion as startable, not terminal putio-sdk-kotlin#57 , feat(files): add maxSubtitleCount to HLS stream URLs putio-sdk-kotlin#58 , docs: start MP4 conversion when the viewer opens a NotAvailable file putio-sdk-kotlin#59 , feat(account): expose is_sub_account on AccountInfo putio-sdk-kotlin#61 , feat(files): add multipart upload with a torrent-only option putio-sdk-kotlin#63 , fix(errors): decode put.io's error_message and expose it on PutioApiException putio-sdk-kotlin#64 , feat(sharing): add cloneSharedFiles and getCloneInfo putio-sdk-kotlin#65
Each item: one worker in its own worktree, ./gradlew verify plus both debug assembles, emulator proof attached to the PR, merge after green CI and a clean Codex review (bot, or local slopguard with Codex gpt-6.1-sol when the bot was out of quota).
UX audit: 2026-09-30
Every Android flow compared with app.put.io (putio-web apps/app @ 48f14ef), apps/tv-native / tv-vite, and putio-ios main @ 9c7264f. Findings cite file:line in each issue. References answer two former owner calls:
Wave 6 (agent-ready, TV release blockers first):
tv: launcher filter has no MAIN action, so Android TV home can't list the app #243 TV launcher filter has no MAIN action (confirmed in the merged manifest) — blocks TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 → fix(tv): add MAIN to the LEANBACK_LAUNCHER filter so TV home lists the app #251 26968fa
tv: keep tv-native users signed in and keep their playback type on upgrade #244 Keep tv-native session and playback type on upgrade — blocks TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 → feat(tv): keep tv-native sessions and playback type on upgrade #252 edbdddb
search/history: opening a result should open the file, not its untitled parent #246 Search/History/deep-link opens the file, not an untitled parent → fix: open Search, History and link picks as the item itself #253 63374f9
files: shared-with-me items offer owner actions #245 Shared-with-me items offer owner actions → fix(files): withhold owner actions from shared-with-me items #254 9696c11
search: recent searches record every debounce instead of real searches #247 Recent searches record every debounce; add tv-native's history toggle/clear → fix(search): keep recent searches only on submit and open, add TV history settings #257 043dca9
tv: Back from Search, History or Account goes to Files before exiting #250 TV Back from Search/History/Account → Files; drawer Back pops folders → fix(tv): return Back to Files and keep the drawer off the folder stack #258 4c153f4
tv: "Autoplay next video" toggle does nothing on TV #248 TV "Autoplay next video" does nothing (part of TV playback parity on Media3 with the Back-button state machine #34 ) → fix(tv): play the next video when autoplay is on #259 842e8ab
playback: start MP4 conversion when the viewer opens a file that needs it #236 Auto-start MP4 conversion on open → feat(playback): start MP4 conversion on opening a video that needs it #260 57e75a0, sdk#59 750ecd0
playback: pick the server's default subtitle and hide subtitles entirely with hide_subtitles #237 Server default subtitle; hide subtitles with hide_subtitles → fix(playback): pick the server's default subtitle and hide subtitles entirely with hide_subtitles #261 d227e35
playback: reset resume near the end, resume audio silently, don't force landscape #238 Near-end resume reset, silent audio resume, no forced landscape, TV resume-prompt Back leaves → fix(playback): clear finished positions, resume audio silently, stop forcing landscape #266 85a4575
transfers: show the server's failure reason and retry without a confirm #249 Transfers show the server's error; retry without confirm → fix(transfers): show the server's failure reason and retry without a confirm #264 5bf56ca
account: warn when the account is inactive and files are scheduled for deletion #239 Inactive-account notice with deletion date — Altay: status and date only, no billing link (Play policy) → feat(account): show the inactive-account status and file deletion countdown #265 d8d281d, sdk#61 620c536
copy: align Trash, History, storage and history-event copy with put.io references #241 Copy: Trash 14 days, History-off, storage, history event types → fix(copy): align Trash, History and quota copy with web and iOS #268 c8b814e
files: trash-mode delete without a confirm, and load pages automatically #242 Trash-mode delete without confirm; automatic paging on mobile → fix(files): move to Trash without a confirm and load pages on scroll #269 9e26566
mobile: accept magnet links and .torrent files, and pick a destination folder #240 magnet: / .torrent intake and destination folder (web parity; iOS main has no Transfers) → feat(transfers): accept magnet links and .torrent files, add several links, and pick a destination folder #271 8c2ee80, sdk#63 ebc9dfc
files: offer Make a copy on items shared with you #255 Make a copy on items shared with you (web, iOS main) → feat(files): offer Make a copy on items shared with you #275 8b39d15, sdk#65 e6de13b
files: reveal a target item beyond the loaded pages #262 Reveal a target item beyond the loaded Files pages (from fix: open Search, History and link picks as the item itself #253 , fix(tv): play the next video when autoplay is on #259 ) → fix(files): read on to a revealed item beyond the loaded pages #272 51380a0
playback: final position may be lost behind an in-flight save #263 Final playback position may be lost behind an in-flight save (verify first) → not reproduced (test in fix(playback): clear finished positions, resume audio silently, stop forcing landscape #266 )
errors: show put.io's reason for 4xx failures instead of generic copy #267 Show put.io's reason for 4xx failures instead of generic copy (from fix(transfers): show the server's failure reason and retry without a confirm #264 ) → fix(errors): show put.io's reason for refused requests instead of generic copy #274 c710c32, sdk#64 10fa469
files: explain and offer permanent delete when a folder is too large for Trash #270 Explain and offer permanent delete when a folder is too large for Trash (from fix(files): move to Trash without a confirm and load pages on scroll #269 ) → fix(files): offer permanent delete for a folder too large for Trash #273 b4ccc9e
files: remember the Move and Copy target folder #276 Remember the Move and Copy target folder, stored on the device (Altay, 2026-10-01) → feat(files): remember the Move and Copy target folder on the device #277 4d42152
Scope notes from the audit:
Continue Watching and richer home rails on mobile and TV #38 : TV continue-watching conflicts with putio-design's Android DESIGN.md; narrow Continue Watching and richer home rails on mobile and TV #38 to mobile, move TV Watch Next to Android TV platform integration: voice search provider, home channel, QR pairing #39 .
Android TV platform integration: voice search provider, home channel, QR pairing #39 : tv-native has no voice search, system search, home channel or QR, so none is a regression; QR is UI-only (SDK returns qrCodeUrl, /link?code= works).
Complete Android download queue management and offline polish #53 : add offline resume (start_from ignored offline) and iOS's download concurrency setting; pause has no reference.
Public links and sharing UI #43 : unblocked (sdk#56); web's exclusive-access links with list/revoke set the scope; iOS main has none.
Documents provider (SAF) integration #40 , Tablet power features: keyboard shortcuts, drag-and-drop, multi-window #44 : no reference implements either.
Dismiss playback overlays before exiting on TV Back #9 : Back state machine is done in TV playback parity on Media3 with the Back-button state machine #34 L2; tv-native had no Back handling. Android TV parity: subtitle state, overscan safe area, and audio passthrough #45 : subtitle state (feat(tv): add Language, Subtitles and Speed pickers to the player #221 , fix(playback): keep subtitles off until account settings load #231 , fix(playback): pick the server's default subtitle and hide subtitles entirely with hide_subtitles #261 ) and overscan (fix(tv): pad the shell by the generated overscan safe area #207 ) done; only audio passthrough remains (Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 ).
tv: Open in VLC hands the account token to an unverified package #185 : references are no safer (iOS and tv-native hand VLC a token URL, checked only by canOpenURL); web's folder VLC playlist uses download_token, a third option.
Kept as Android improvements: sign-out revocation with retry (web fires once, iOS never revokes), share-out of file bytes, exact TV focus restore, "Convert again".
Waiting on Altay
tv: Open in VLC hands the account token to an unverified package #185 VLC handoff: IP-bound /files/{id}/url (SDK ready, feat(files): add getDownloadUrl for API-issued download URLs putio-sdk-kotlin#55 ), or web's download_token URL; and which VLC keys to pin (VideoLAN, F-Droid now; Fire TV unverified, Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 ).
Nightly app id vs Play tracks: nightly builds are their own packages (io.put.putio.nightly, io.put.putio.mobile.nightly) while AGENTS.md says internal and closed tracks ship nightly, so those tracks would sit on a separate listing. Rule before Release signing and publishing pipeline in CI (GitHub Environments, SOPS boundary) #49 (Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 , TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 ).
tv-native Diagnostics test streams: port the list onto the player, or drop it and tell support (TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 ).
Privacy copy: "These choices apply to every put.io app" is unverified; no reference exposes the setting. Confirm with backend or soften.
Expand detekt coverage to Android flavor and device-test sources #123 detekt: 116 structural findings at 6a603f7 (re-measure on main): refactor, raise thresholds, or extend test exclusions?
Dismiss playback overlays before exiting on TV Back #9 / TV playback parity on Media3 with the Back-button state machine #34 : accept the emulator D-pad proof as the remote smoke, or keep both open for Fire TV (Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 ).
Brand asset pipelines: Phosphor icons and GT America fonts, locked and drift-gated #21 : file-kind icon weight: fill (design/README.md) or the design's regular.
Mobile live proof (auth: API 37 Auth Tab drops the OAuth callback on Chrome 145 #107 ): sign devs-auto into the phone emulator's Auth Tab once; the classifier denies automated credential entry.
Post-v1 scope: which of Picture-in-Picture for mobile video playback #37 –Tablet power features: keyboard shortcuts, drag-and-drop, multi-window #44 , Complete Android download queue management and offline polish #53 enter a batch; keep or drop Documents provider (SAF) integration #40 and Tablet power features: keyboard shortcuts, drag-and-drop, multi-window #44 (no reference implements either); accept the Continue Watching and richer home rails on mobile and TV #38 narrowing to mobile.
Closures: close Deliver a first-party native Android mobile app #10 and Add touch seek gestures and controls to Android playback #8 (native touch seek shipped in feat(playback): add touch seek controls #112 ; mobile web belongs to putio-web), accept Adopt the next Android tier-2 contract rulings from putio-design#43 #64 , move Investigate Android TV HLS HTTP 400 playback failures #113 to tv-native, decide Define the shared mobile product contract and Android mobile auth spec #19 (mobile v1 shipped ahead of the contract), close Standalone clone: drop sibling-checkout path dependencies #173 into Consume putio-sdk-kotlin from Maven Central instead of the composite build #148 .
Unchanged gates: Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 release identity, Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46 , Confirm GT America licensing for Android app distribution (owner: Altay) #48 , Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 , Evaluate libVLC direct-play: licensing, store compliance, Media3 fallback boundary #42 go/no-go, Prove supported 10-bit playback on Android TV #11 hardware, Consume putio-sdk-kotlin from Maven Central instead of the composite build #148 credentials.
Still open from the batch
Item
Status
Next
#185
needs Altay
URL source and VLC key pins (above)
#123
needs Altay
ruling on structural findings, then extend detekt to every source root
#107
blocked
one human devs-auto Auth Tab sign-in on the API 37 emulator, then record proof
#34 L6
blocked on #51
Fire TV remote smoke and oracle 17–31 parity; closes #9
#45
blocked on #51
audio passthrough only
#173 / #148
blocked on putdotio/putio-sdk-kotlin#43
pin io.put:putio-sdk-kotlin, drop the sibling checkout
#278
agent-ready
check whether put.io refuses a move into a descendant; if not, needs SDK breadcrumbs
Outcome
One putio-android codebase ships two first-party surfaces:
Android mobile (phone + tablet): a new Jetpack Compose app with feature parity against the parallel iOS rewrite's product contract, adapted to Android platform conventions (Material navigation, AndroidX Auth Tab auth, share target, downloads, system media controls)
Android TV / Fire TV: a Compose TV app with behavior parity against the current React Native tv-native Android TV surface, replacing it on the existing Play listing
Both are backed by putio-sdk-kotlin as the only API boundary and themed from @putdotio/design tokens through a generated Compose adapter.
Decisions (settled)
Decision
Choice
Sequencing vs ADR 0005
Android runs in parallel with the Apple loop; ADR 0005's "Apple first" gate is superseded (decision record to be written, tracked below)
feature/android-tv-full-parity branch
Delete — clean slate. Never compiled, 3 months stale, AGP major behind. tv-native source + the 34-capture oracle are the only references
Order inside this repo
Mobile first, then TV (after shared foundation + design adapter)
Play identity
One package, one listing: both form factors ship as io.put.putio on the existing listing; TV versionCode must exceed 91
Offline downloads
Lean core in mobile v1.0 (download, local availability, offline playback); queue-management polish fast-follows — iOS ships the fuller bar, delta recorded in the shared contract (#19 )
Chromecast
In mobile v1 (#36 , after the core player) — iOS v1 ships Cast and it matters more on Android
Appearance
Dark-only on both form factors — the cross-app contract (putio-ios#82 ): product apps are dark-only, only the public web surfaces (www, auth.put.io) may ship light; the token adapter keeps the light palette available for a future flip
Telemetry
Sentry may ship in v1 only behind the approved diagnostics_enabled control and redaction boundary; PostHog/product analytics is out of v1 entirely
i18n
strings.xml discipline from day one, English-only at launch; no putio-i18n commitment
Shared contract
#19 produces a versioned v1 matrix; scope changes update both epics (#14 ↔ putio-ios#123) or neither
Release owner
Altay — Play Console access, signing lineage, production rollout approval
tv-native
Frozen : security/P0-playback-only hotfixes; all other defects route here (e.g. #45 )
Visual source of truth
@putdotio/design DTCG tokens (dist/tokens.dtcg.json, light/dark + tv group). tv-native is a behavior oracle only
API boundary
putio-sdk-kotlin via composite build; no app-local HTTP without a documented SDK gap
Icons / fonts
Phosphor icons and GT America via locked, drift-gated pipelines (mirror the putio-ios scripts); no committed font binaries
Rules of the program
Agent-readiness first. Nothing ships before the harness workstream (W0) makes this repo autonomously workable by agents: environment bootstrap on a MacBook/devbox (cloud-friendly), emulator/device proof, putio CLI auth for live checks, and evidence capture built in.
Definition of done, every child issue: repo verification green (./gradlew verify + flavor assembles), the behavior exercised on the local harness (emulator or device), and visual proof (screenshots or screen recording) uploaded to the PR with gh pr comment <n> --attach <file> (gh 2.99+); never commit proof media.
Screenshot/behavior parity gates for TV come from putio-web apps/tv-native/docs/captures/ (34 Android TV captures) and the TV interface spec (Notion Frontend hub, page TV interface); mobile gates come from the shared mobile product contract.
Completion criteria
This epic closes when all of the following are true:
Post-v1 work in #52 is explicitly outside this completion boundary.
Execution order
Every issue below is a native sub-issue of this epic, so this page tracks live progress across both repos. Steps run left to right; issues inside a step run in parallel. An issue starts when its step is reached and its named blockers are closed.
Step 0 — start today, all parallel
Agent-readiness harness: environment bootstrap, emulator proof, evidence capture #15 agent-readiness harness ← the gate for everything app-side
Clean slate: retire feature/android-tv-full-parity and correct stale repo guidance #17 clean slate, Record program decisions in workspace docs and supersede ADR 0005 sequencing #18 workspace decisions, Define the shared mobile product contract and Android mobile auth spec #19 shared contract + Auth Tab spec (putio-frontend#34)
SDK lane: Device-code auth orchestration: polling state machine with typed expiry and restart putio-sdk-kotlin#21 , Mobile auth: AndroidX Auth Tab, secure token storage, session restore #22 , Mobile shell: Material 3 navigation, dark-only theme, tablet-adaptive layout #23 , Mobile files: browse, sort, paging, file actions, and trash #24
Altay lane: Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46 credentials custody, Create the Android mobile OAuth client id (owner: Altay/backend) #47 mobile OAuth client id, Confirm GT America licensing for Android app distribution (owner: Altay) #48 font licensing
Step 1 — when #15 closes
Step 2 — mobile core (needs #20 + #19 ; auth also needs #47 )
Step 3 — mobile surfaces, all parallel (need #22 + #23 )
Step 4 — mobile media (need #24 ; playback needs sdk#23, downloads need sdk#24)
Mobile playback: video and audio with resume, tracks, background audio, touch seek #27 video playback · Mobile audio playback with background audio and MediaSession #146 audio/MediaSession · Mobile downloads v1: download-to-device with explicit local availability and offline playback #28 downloads (lean core) · Mobile share and open-in: share target, deep links, share out #29 share/open-in · Chromecast support in the mobile app (VPN/tunnel-safe) #36 Chromecast (after Mobile playback: video and audio with resume, tracks, background audio, touch seek #27 ) · Mobile accessibility baseline: TalkBack, font scaling, reduced motion #147 accessibility
Step 5 — mobile ship (needs everything in steps 2–4)
Step 6 — TV, starts once the shared core is stable (~step 3), trails mobile
TV shell and device-code auth on the shared core #32 TV shell/auth (needs sdk#21) → TV browse parity: files, search, history, trash, settings vs the 34-capture oracle #33 browse parity → TV playback parity on Media3 with the Back-button state machine #34 playback parity (+Android TV parity: subtitle state, overscan safe area, and audio passthrough #45 assertions) → TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 TV release (needs Release signing and publishing pipeline in CI (GitHub Environments, SOPS boundary) #49 + Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 )
Post-v1 follow-up
Critical path to mobile v1: #15 → #20 → #23 → #24 → #27 → #31 , with #46 /#47 as the human-gated joins.
Critical path to TV cutover: #15 → #20 → #32 → #33 → #34 → #35 .
Workstreams
Ordering: W0 → W1 unlock everything; W2 runs in parallel from day one; W3 (mobile) leads, W4 (TV) follows behind it reusing the shared core.
W0 — Foundation and agent readiness
Agent-readiness harness: environment bootstrap, emulator proof, evidence capture #15 — agent-readiness harness (env bootstrap, emulator proof, evidence capture) — first, blocks nearly everything
Stand up CI: verify, assemble, lint, and unit-test lanes on GitHub Actions #16 — CI: verify, assemble, lint, unit-test lanes
Clean slate: retire feature/android-tv-full-parity and correct stale repo guidance #17 — clean slate: retire the parity branch, correct stale guidance
Record program decisions in workspace docs and supersede ADR 0005 sequencing #18 — record program decisions in workspace docs, supersede ADR 0005 sequencing
Define the shared mobile product contract and Android mobile auth spec #19 — shared mobile product contract + Android mobile auth spec (coordinate with the iOS epic)
Access, credentials, and release infrastructure (nothing lives only as an inline stop condition anymore):
Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46 — release credentials custody: Play Console, signing lineage, Sentry DSN (owner: Altay ; blocks Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 /TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 )
Create the Android mobile OAuth client id (owner: Altay/backend) #47 — mobile OAuth client id (owner: Altay/backend ; blocks Mobile auth: AndroidX Auth Tab, secure token storage, session restore #22 )
Confirm GT America licensing for Android app distribution (owner: Altay) #48 — GT America licensing confirmation (owner: Altay ; blocks release builds from Brand asset pipelines: Phosphor icons and GT America fonts, locked and drift-gated #21 )
Release signing and publishing pipeline in CI (GitHub Environments, SOPS boundary) #49 — release signing + publishing pipeline in CI (blocked by Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46 , Stand up CI: verify, assemble, lint, and unit-test lanes on GitHub Actions #16 ; feeds Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 /TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 )
Adopt the attach CLI into the harness evidence flow #50 — Attach CLI adoption into the evidence flow
Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 — physical device proof set (needed before TV playback parity on Media3 with the Back-button state machine #34 /TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 /Evaluate libVLC direct-play: licensing, store compliance, Media3 fallback boundary #42 /Prove supported 10-bit playback on Android TV #11 /Android TV parity: subtitle state, overscan safe area, and audio passthrough #45 hardware gates)
W1 — Design system adapter
W2 — SDK hardening (putio-sdk-kotlin)
Device-code auth orchestration: polling state machine with typed expiry and restart putio-sdk-kotlin#21 — device-code auth orchestration
Cursor continuation contracts for files, search, history, trash, and transfers putio-sdk-kotlin#22 — cursor paging helpers
Playback source model: HLS/MP4/original decision, subtitles, start-from, conversion state putio-sdk-kotlin#23 — playback source model + start_from naming resolution
Hardening: AccountApi errors, thread-safe token state, and URL token policy putio-sdk-kotlin#24 — hardening; token atomicity and header auth follow up in Harden token state atomicity and prefer header auth where the API supports it putio-sdk-kotlin#49
Prove Android consumption: minSdk 26, R8, and a live smoke path from putio-android putio-sdk-kotlin#25 — Android consumption proof (minSdk 26, R8, live smoke)
Consume putio-sdk-kotlin from Maven Central instead of the composite build #148 — consume the SDK from Maven Central once release: publish to Maven Central so consumers pin a version instead of a composite build putio-sdk-kotlin#43 tags v0.1.0
W3 — Mobile app v1 (leads)
Mobile auth: AndroidX Auth Tab, secure token storage, session restore #22 — auth: OAuth via AndroidX Auth Tab, secure storage, session restore
auth: harden OAuth foundation before enabling the mobile flow #68 — auth hardening closeout
Mobile shell: Material 3 navigation, dark-only theme, tablet-adaptive layout #23 — shell: Material 3 navigation, dark-only theme, tablet-adaptive
Mobile files: browse, sort, paging, file actions, and trash #24 — files: browse, sort, paging, actions, trash
Mobile search and history #25 — search and history
Mobile transfers: list, add via URL/magnet, progress and actions #26 — transfers: list, add, progress, actions
Mobile playback: video and audio with resume, tracks, background audio, touch seek #27 — video playback: HLS/MP4, subtitles, autoplay, touch seek, resume prompts and throttled position write-back (#158 )
Mobile audio playback with background audio and MediaSession #146 — audio playback, background audio, MediaSession, speed and audio-track selection
Mobile accessibility baseline: TalkBack, font scaling, reduced motion #147 — accessibility baseline: large text in fix(mobile): keep navigation and controls usable with large text #160 , TalkBack and reduced motion in #161
Mobile downloads v1: download-to-device with explicit local availability and offline playback #28 — downloads v1: HLS rendition offline via Media3 (#162 )
Chromecast support in the mobile app (VPN/tunnel-safe) #36 — Chromecast (pulled into v1, after Mobile playback: video and audio with resume, tracks, background audio, touch seek #27 )
Mobile share and open-in: share target, deep links, share out #29 — share and open-in: share-in in #159 , share-out and deep links in #163
Mobile settings and account per the workspace settings contract #30 — settings and account per the settings contract, plus privacy controls and About
Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 — v1 release: one-package Play identity, Actions lane, privacy-gated Sentry (delivers Deliver a first-party native Android mobile app #10 )
W4 — Android TV parity (follows)
TV shell and device-code auth on the shared core #32 — TV shell + device-code auth on the shared core (#165 )
TV browse parity: files, search, history, trash, settings vs the 34-capture oracle #33 — browse parity vs the 34-capture oracle (#166 –#171 ; the conversion-in-progress capture moved to TV playback parity on Media3 with the Back-button state machine #34 )
TV playback parity on Media3 with the Back-button state machine #34 — playback parity on Media3 + Back-button state machine (closes Dismiss playback overlays before exiting on TV Back #9 ); L1–L5 merged, L6 Fire TV and oracle parity wait on Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51
TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 — release: replace tv-native on Play, Fire TV lane, rollout
Android TV parity: subtitle state, overscan safe area, and audio passthrough #45 — shipped-app parity defects: subtitle state, overscan safe area, audio passthrough (asserted across TV shell and device-code auth on the shared core #32 –TV playback parity on Media3 with the Back-button state machine #34 ); only passthrough remains (Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51 )
W5 — Audit hardening (2026-09-29)
mobile: tablet layout drops the Share file action #183 — tablet layout drops the Share file action (P0)
auth: mobile secure-storage failure blocks sign-in until app data is cleared #184 — mobile secure-storage failure blocks sign-in (P0)
tv: Open in VLC hands the account token to an unverified package #185 — TV Open in VLC hands over the account token (P0)
downloads: progress never advances, reconcile outlives close, engine untested #186 — download engine progress, lifecycle and tests
share: bind share-out exports to the session and harden file names #187 — share-out session binding and file names
mobile: parse zone-less timestamps and dedupe History opens like TV #188 — zone-less timestamps and History open dedupe
ci: lint TV, shrink every release variant, stop cancelling main runs #189 — lint TV, shrink every release variant, CI cancellation (after Make ./gradlew verify the single gate by moving buildSrc to an included build #180 )
auth: retry remote token revocation when logout fails #190 — retry remote revocation after failed logout
ci: emulator smoke can pass without proving anything #191 — emulator smoke that proves something
mobile: entering playback rebuilds the navigation host #192 — playback rebuilds the navigation host
refactor: one session-scoped holder and split mobile PutioApp.kt #193 — session-holder dedupe and PutioApp.kt split
docs: behavior test citations, bootstrap hint, and dead player code #194 — docs drift and dead code
harness: contract tests depend on host emulator ports, a host-global lock, and bash 4 #197 — hermetic harness contract tests (host ports, global lock, bash 4)
tv: fix existing lint errors and add TV lint to verify #204 — fix existing TV lint errors and add TV lint to verify
share: session-exit cleanup can delete the next session's export #211 — share cleanup scoped to the departed session (post-merge review of fix(share): bind share-out exports to the session and harden file names #206 )
share: sign-out after a restored session leaves earlier-process exports on disk #215 — sign-out after a restored session clears earlier-process exports
auth: API 37 Auth Tab drops the OAuth callback on Chrome 145 #107 , Expand detekt coverage to Android flavor and device-test sources #123 , Standalone clone: drop sibling-checkout path dependencies #173 — open quality and repo items (Measure foreground transfer polling with deep historical lists #121 , Repo modernization: secret scanning, homepage, PR/issue templates #172 , Make ./gradlew verify the single gate by moving buildSrc to an included build #180 done)
Post-v1 follow-up
Deferred Android and SDK upgrades are native sub-issues of #52 . They remain visible in the Frontend project with Program: Android and Phase: Post-v1; assigning a Wave is the scheduling decision.
Existing issues mapping
Key references
Kickoff assessment: Notion Frontend hub
TV product packet + parity oracle: Notion Frontend hub, pages TV app and TV interface; captures in putio-web apps/tv-native/docs/captures/
Mobile product packet: Notion Frontend hub, page Mobile app
Behavior reference app: putio-web/apps/tv-native (routes, playback architecture, OAuth ids: Android TV 6221, Fire TV 6233)
Design tokens: the @putdotio/design npm release pinned in design/putio-design.lock.json, fetched by scripts/sync-design-assets.sh and generated by :app:generateDesignTokens (build(design): fetch design assets from a locked @putdotio/design release #218 )
SDK: putio-sdk-kotlin (12 namespaces including deviceCodeAuth and sharing; composite build wiring in this repo's settings.gradle.kts)
Risks and stop conditions
Signing lineage / Play listing takeover for io.put.putio is unconfirmed — tracked as Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46 ; Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31 /TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 stop until it closes
GT America licensing for app distribution is unrecorded — tracked as Confirm GT America licensing for Android app distribution (owner: Altay) #48 ; no release build ships fonts before it closes
Play review of adding a mobile form factor to a TV-only listing may surface policy surprises — validate on the internal track early
Privacy controls : production Sentry capture stops until putdotio/support#95 settles the diagnostics control, legal posture, redaction boundary, and kill switch; product analytics remains out of v1
SDK publishing is wired to Maven Central tags; the first release waits on credentials in release: publish to Maven Central so consumers pin a version instead of a composite build putio-sdk-kotlin#43 , after which Android pins a version (Consume putio-sdk-kotlin from Maven Central instead of the composite build #148 )
Verification
./gradlew verify, :app:assembleMobileProductionDebug, :app:assembleTvProductionDebug green in CI
Every mobile slice proven on a phone emulator/device; every TV slice proven on a TV emulator plus Fire TV remote checks where behavior differs
TV screens count as done only with a native capture matching the corresponding oracle screenshot's route/state
Current rollout status: 2026-10-01
Everything an agent can finish without an owner call is merged; what's left waits on Altay, hardware or credentials.
.torrentintake, Search and History, video and audio playback with resume and MediaSession, downloads, share and deep links, settings, accessibility, inactive-account notice. Missing: Chromecast (Chromecast support in the mobile app (VPN/tunnel-safe) #36) and the release lane (Release signing and publishing pipeline in CI (GitHub Environments, SOPS boundary) #49 → Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31). Still builds asio.put.putio.mobile. Auth Tab live proof (auth: API 37 Auth Tab drops the OAuth callback on Chrome 145 #107) needs one human devs-auto sign-in.io.put:putio-sdk-kotlin(the app still declaresio.putdotio).Autonomous batch: 2026-09-29 → 2026-10-01 (done)
Every batch PR merged; none open or abandoned:
Each item: one worker in its own worktree,
./gradlew verifyplus both debug assembles, emulator proof attached to the PR, merge after green CI and a clean Codex review (bot, or local slopguard with Codexgpt-6.1-solwhen the bot was out of quota).UX audit: 2026-09-30
Every Android flow compared with app.put.io (putio-web
apps/app@48f14ef),apps/tv-native/tv-vite, and putio-iosmain@9c7264f. Findings cite file:line in each issue. References answer two former owner calls:hide_subtitles: every reference hides subtitles entirely; the TV player hides the Subtitles button when the account hides subtitles #223 "off but pickable" fix was an invented convention → playback: pick the server's default subtitle and hide subtitles entirely with hide_subtitles #237.Wave 6 (agent-ready, TV release blockers first):
26968faedbdddb63374f99696c11043dca94c153f4842e8ab57e75a0, sdk#59750ecd0hide_subtitles→ fix(playback): pick the server's default subtitle and hide subtitles entirely with hide_subtitles #261d227e3585a45755bf56cad8d281d, sdk#61620c536c8b814e9e26566magnet:/.torrentintake and destination folder (web parity; iOS main has no Transfers) → feat(transfers): accept magnet links and .torrent files, add several links, and pick a destination folder #2718c2ee80, sdk#63ebc9dfc8b39d15, sdk#65e6de13b51380a0c710c32, sdk#6410fa469b4ccc9e4d42152Scope notes from the audit:
qrCodeUrl,/link?code=works).start_fromignored offline) and iOS's download concurrency setting; pause has no reference.canOpenURL); web's folder VLC playlist usesdownload_token, a third option.Waiting on Altay
/files/{id}/url(SDK ready, feat(files): add getDownloadUrl for API-issued download URLs putio-sdk-kotlin#55), or web'sdownload_tokenURL; and which VLC keys to pin (VideoLAN, F-Droid now; Fire TV unverified, Physical device proof set: Android TV, Fire TV, phone, tablet inventory #51).io.put.putio.nightly,io.put.putio.mobile.nightly) while AGENTS.md says internal and closed tracks ship nightly, so those tracks would sit on a separate listing. Rule before Release signing and publishing pipeline in CI (GitHub Environments, SOPS boundary) #49 (Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31, TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35).6a603f7(re-measure on main): refactor, raise thresholds, or extend test exclusions?design/README.md) or the design's regular.Still open from the batch
io.put:putio-sdk-kotlin, drop the sibling checkoutOutcome
One
putio-androidcodebase ships two first-party surfaces:tv-nativeAndroid TV surface, replacing it on the existing Play listingBoth are backed by
putio-sdk-kotlinas the only API boundary and themed from@putdotio/designtokens through a generated Compose adapter.Decisions (settled)
feature/android-tv-full-paritybranchtv-nativesource + the 34-capture oracle are the only referencesio.put.putioon the existing listing; TV versionCode must exceed 91diagnostics_enabledcontrol and redaction boundary; PostHog/product analytics is out of v1 entirelystrings.xmldiscipline from day one, English-only at launch; no putio-i18n commitment@putdotio/designDTCG tokens (dist/tokens.dtcg.json, light/dark +tvgroup).tv-nativeis a behavior oracle onlyputio-sdk-kotlinvia composite build; no app-local HTTP without a documented SDK gapputio-iosscripts); no committed font binariesRules of the program
putioCLI auth for live checks, and evidence capture built in../gradlew verify+ flavor assembles), the behavior exercised on the local harness (emulator or device), and visual proof (screenshots or screen recording) uploaded to the PR withgh pr comment <n> --attach <file>(gh 2.99+); never commit proof media.apps/tv-native/docs/captures/(34 Android TV captures) and the TV interface spec (Notion Frontend hub, page TV interface); mobile gates come from the shared mobile product contract.Completion criteria
This epic closes when all of the following are true:
tv-nativeon Play and has a verified Fire TV distribution pathPost-v1 work in #52 is explicitly outside this completion boundary.
Execution order
Every issue below is a native sub-issue of this epic, so this page tracks live progress across both repos. Steps run left to right; issues inside a step run in parallel. An issue starts when its step is reached and its named blockers are closed.
Step 0 — start today, all parallel
Step 1 — when #15 closes
Step 2 — mobile core (needs #20 + #19; auth also needs #47)
Step 3 — mobile surfaces, all parallel (need #22 + #23)
Step 4 — mobile media (need #24; playback needs sdk#23, downloads need sdk#24)
Step 5 — mobile ship (needs everything in steps 2–4)
Step 6 — TV, starts once the shared core is stable (~step 3), trails mobile
Post-v1 follow-up
Critical path to mobile v1: #15 → #20 → #23 → #24 → #27 → #31, with #46/#47 as the human-gated joins.
Critical path to TV cutover: #15 → #20 → #32 → #33 → #34 → #35.
Workstreams
Ordering: W0 → W1 unlock everything; W2 runs in parallel from day one; W3 (mobile) leads, W4 (TV) follows behind it reusing the shared core.
W0 — Foundation and agent readiness
Access, credentials, and release infrastructure (nothing lives only as an inline stop condition anymore):
W1 — Design system adapter
@putdotio/designwith drift gateW2 — SDK hardening (
putio-sdk-kotlin)start_fromnaming resolutionv0.1.0W3 — Mobile app v1 (leads)
W4 — Android TV parity (follows)
W5 — Audit hardening (2026-09-29)
PutioApp.ktsplitPost-v1 follow-up
Deferred Android and SDK upgrades are native sub-issues of #52. They remain visible in the Frontend project with
Program: AndroidandPhase: Post-v1; assigning a Wave is the scheduling decision.Existing issues mapping
Key references
apps/tv-native/docs/captures/putio-web/apps/tv-native(routes, playback architecture, OAuth ids: Android TV 6221, Fire TV 6233)@putdotio/designnpm release pinned indesign/putio-design.lock.json, fetched byscripts/sync-design-assets.shand generated by:app:generateDesignTokens(build(design): fetch design assets from a locked @putdotio/design release #218)putio-sdk-kotlin(12 namespaces includingdeviceCodeAuthandsharing; composite build wiring in this repo'ssettings.gradle.kts)Risks and stop conditions
io.put.putiois unconfirmed — tracked as Release credentials custody: Play Console access, signing lineage, Sentry DSN (owner: Altay) #46; Mobile v1 release: one-package Play identity, Actions release lane, and Sentry #31/TV release: replace tv-native on Play, Fire TV sideload lane, rollout #35 stop until it closesVerification
./gradlew verify,:app:assembleMobileProductionDebug,:app:assembleTvProductionDebuggreen in CI