Skip to content

Add Android TV / D-pad navigation support - #2377

Closed
daviddaco1 wants to merge 4 commits into
aniyomiorg:mainfrom
daviddaco1:android-tv-dpad-support
Closed

daviddaco1 wants to merge 4 commits into
aniyomiorg:mainfrom
daviddaco1:android-tv-dpad-support

Conversation

@daviddaco1

@daviddaco1 daviddaco1 commented Aug 15, 2026 •

Copy link
Copy Markdown

Summary

  • Adapts the existing mobile/tablet UI for Android TV / Google TV remote control navigation (D-pad up/down/left/right, OK, Back, Play/Pause), keeping the current architecture (Voyager navigation, Injekt DI, Compose Foundation) — no new UI frameworks introduced.
  • Single APK strategy: adds an isTvUi() device-mode check (mirrors the existing isTabletUi() pattern) instead of a dedicated flavor/module, since the difference between mobile and TV is layout + focus, not business logic.
  • Central Modifier.tvFocusable() draws the D-pad focus indicator by observing the interactionSource each clickable/selectable/combinedClickable already owns, rather than adding a second focusable() node (Compose skips a focusable ancestor/descendant pair, which was hiding the indicator or making some elements unreachable — this was the core fix threaded through the diff).
  • New: manual TvUiMode preference (Automatic/Always/Never), mirroring the existing TabletUiMode pattern exactly (same UiPreferences shape, same Settings > Appearance row, same "requires restart" toast). Lets users override the automatic hardware detection for TV boxes/hybrid devices where it's unreliable. isTvUi() now combines the preference with hardware detection; AUTOMATIC preserves today's behavior for all existing installs.
    • Swipe-to-mark gestures on chapters/episodes intentionally stay tied to real touch hardware, not the manual override, so forcing "Always" on a touchscreen device doesn't break swipe.
  • New onboarding step (DeviceModeStep, between Theme and Storage) asking Mobile/Touch vs Android TV, persisting Never/Always respectively (Automatic stays the unasked default).
  • Extended focus coverage: Extensions (list + details, install/uninstall/trust actions), Updates list, entry info headers (favorite/tracking buttons, author/artist/source text, tag chips), tracker search results and status selectors, list-preference dialogs, app language list, editor type dropdown, and every onboarding step's buttons.
  • Fixed several LazyColumns across Settings (main menu, Player Settings menu, search, extension stores/repos, script editor, clear-database, tri-state dialog) that were missing Modifier.focusGroup() — without it Compose has no coherent D-pad traversal order between items, so these lists were effectively unnavigable by remote.
  • Fixed a real bug (not just missing focus): extension list items applied combinedClickable twice (once via an explicit modifier, once inside BaseBrowseItem), leaving two overlapping focus/click targets on the same row.
  • Covered surfaces: home navigation rail, library cards (grid + list), chapter/episode rows, browse/source catalog grids, category reordering, settings preference rows and menus, dialogs/bottom sheets (AdaptiveSheet), onboarding, and the video player's on-screen controls.
  • MainActivity nudges initial focus onto each newly pushed Voyager screen (Compose doesn't do this on its own), and PlayerActivity now handles MEDIA_PLAY_PAUSE / DPAD_CENTER / ENTER and stops swallowing DPAD_UP/DOWN once controls are visible, so Compose can move focus between the on-screen buttons.
  • Manifest: optional touchscreen/leanback uses-feature, LEANBACK_LAUNCHER category, placeholder TV banner drawable (not a final design asset).

Resolved from earlier review

The Settings main menu (and several other menus/dialogs) not receiving D-pad navigation was actually a missing focusGroup() on the underlying LazyColumns, not just an initial-focus issue — fixed at the shared ScrollbarLazyColumn/FastScrollLazyColumn/LazyColumnWithAction components plus the handful of screens using a plain LazyColumn directly.

Test plan

  • ./gradlew spotlessCheck
  • ./gradlew assembleDebug
  • ./gradlew testReleaseUnitTest
  • ./gradlew assembleRelease
  • Manual D-pad testing on an Android TV emulator (Television_1080p AVD): home rail navigation, library → browse → source catalog → anime details → episode list → player (show/hide controls, prev/next, play/pause, seek, back) → settings menus and sub-screens → onboarding flow, confirming focus indicator visibility and D-pad traversal with no crashes in logcat across the session
  • Physical Android TV / Google TV device

daviddaco1 and others added 2 commits August 15, 2026 13:41
Adapts the existing UI for Android TV/Google TV remote control navigation
without introducing new frameworks (still Voyager/Injekt/Compose Foundation).

- isTvUi() device-mode detection (mirrors the existing isTabletUi() pattern)
- Modifier.tvFocusable() draws a D-pad focus indicator driven by the
  interactionSource each clickable/selectable already owns, instead of
  stacking a second focusable() node (which Compose skips, hiding the
  indicator or making elements unreachable)
- NavigationRail always visible on TV; library cards, chapter/episode rows,
  settings widgets, and category rows are D-pad focusable and navigable
- Swipe actions on chapters/episodes are disabled on TV (long-press
  selection covers the same actions); category reordering gets explicit
  up/down buttons as a D-pad-reachable alternative to drag handles
- MainActivity nudges initial focus onto each newly pushed Voyager screen,
  since Compose doesn't do this automatically
- PlayerActivity handles MEDIA_PLAY_PAUSE/DPAD_CENTER/ENTER and no longer
  swallows DPAD_UP/DOWN once controls are shown, so Compose focus can
  navigate the on-screen controls; play/pause auto-focuses when controls
  appear
- AdaptiveSheet forces the centered (non-swipe) layout on TV and focuses
  its first element on open
- Manifest: optional touchscreen/leanback uses-feature, LEANBACK_LAUNCHER
  category, placeholder TV banner

Known issue: the Settings main menu list (Appearance/Library/Reader/...)
doesn't yet get initial D-pad focus on its first item; needs the same
tvFocusable treatment as BasePreferenceWidget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add TvUiMode preference (Automatic/Always/Never), mirroring TabletUiMode,
  with a new onboarding step to choose Mobile vs Android TV
- Combine the manual override into isTvUi(), keeping swipe gestures on
  chapters/episodes tied to real touch hardware regardless of the override
- Add tvFocusable focus indicators across Extensions, Updates, Tracker,
  Settings, and onboarding screens that were reachable by D-pad but had no
  visible focus
- Fix missing focusGroup() on several settings LazyColumns that blocked
  D-pad navigation entirely (Settings menus, Player Settings, dialogs)
- Fix duplicate combinedClickable in extension list items
@daviddaco1
daviddaco1 marked this pull request as ready for review August 16, 2026 19:25
@myasn1k

myasn1k commented Sep 7, 2026

Copy link
Copy Markdown

Hi! I was experimenting with Android TV support on my own before I noticed this PR.

Most of the implementation on my branch was done with the help of OpenAI Codex. I tested several workflows manually on both TV and phone emulators, although I’m not confident enough with the codebase to properly review all of the implementation myself.

My branch is here. It includes some fixes for player submenu navigation, remote seeking, manga reader controls, and downloads that might be useful to compare against your work.

If it would help, I’d be happy to test your branch and report any reproducible issues I find. I’d rather contribute to this effort than maintain a separate implementation of the same thing.

@daviddaco1

Copy link
Copy Markdown
Author

@myasn1k, thank you very much for your interest, the branch was born to try to use it in an Android TV Box, I was surprised that I was not prepared for button navigation, my branch implements a TV mode that allows button navigation to be functional and I have even arrived to play videos, I am still correcting things but I would be very grateful for any help.

@jmir1

jmir1 commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Thank you for your work on Aniyomi. This PR won't be merged for now because I can't maintain it right now. If you want, you can submit this PR to Quickdesh/Animiru, which is still actively maintained.

@myasn1k

myasn1k commented Sep 14, 2026

Copy link
Copy Markdown

@jmir1 thanks for the reply!
@daviddaco1 would you like to create the pr to Animiru so i can attach mine to yours?

@daviddaco1

Copy link
Copy Markdown
Author

@jmir1 thanks for the reply! @daviddaco1 would you like to create the pr to Animiru so i can attach mine to yours?

I'll gladly send a pr.

@daviddaco1

Copy link
Copy Markdown
Author

@jmir1 thanks for the reply! @daviddaco1 would you like to create the pr to Animiru so i can attach mine to yours?

I will close this pr, I will bring the changes of that fork and merge them with my changes if everything goes well, I do the pr and I label you.

@daviddaco1

Copy link
Copy Markdown
Author

This PR will be closed to contribute to the fork https://github.com/Quickdesh/Animiru

@daviddaco1 daviddaco1 closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants