Repository navigation
Conversation
Integrate upstream 4b5b90a while preserving TV remote controls. Combine the upstream touch-seeking fix with TV timeline navigation and add position-selection regression tests. Validation: spotlessCheck, app and presentation-core debug unit tests (67 passing), and assembleDebug. Upstream Kotlin metadata/toolchain warnings remain; release and emulator checks were not rerun for this merge.
Merge PR aniyomiorg#2377 with feat/android-tv-pr while preserving both histories. Unify the TV mode preference across remote input and shared UI, retain reader and player navigation fixes, and add mode-policy regression tests. Integration assisted by Codex.
PR Summary by QodoIntegrate Android TV controls, focus navigation, and repository support
AI Description
Diagram
High-Level Assessment
Files changed (92)
|
Code Review by Qodo
1. TV category shortcuts open anime tab
|
| val isTelevision = LocalContext.current.isTvUiEnabled() | ||
| val selectedPage = if (isTelevision) tvPage.value else state.currentPage |
There was a problem hiding this comment.
1. Tv category shortcuts open anime tab 🐞 Bug ≡ Correctness
TabbedScreen derives the rendered TV tab exclusively from tvPage, leaving the supplied PagerState independent and ignoring its updates. Because CategoriesTab does not supply a television page state and showMangaCategory() only calls state.scrollToPage(1), its TV shortcut continues to render the default anime categories instead of manga.
Agent Prompt
## Issue description
TV rendering uses a separate `tvPage` state and ignores programmatic tab updates made through the supplied `PagerState`. Since `CategoriesTab.showMangaCategory()` updates only the pager state, the manga-category shortcut does not display tab 1 in TV mode.
## Fix Focus Areas
- app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt[50-69]
- app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt[144-155]
- app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoriesTab.kt[64-74]
## Recommended Fix
Use one authoritative, externally controllable selected-page state for both television and mobile rendering. Alternatively, synchronize `tvPage` whenever the supplied pager state changes and update `CategoriesTab` to provide and mutate that same television page state when invoking the manga-category shortcut; verify that `CategoriesTab.showMangaCategory()` displays tab 1 in TV mode.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| .extensionList | ||
| ?.extensions | ||
| ?.toProtobufExtensions(repoUrl) | ||
| .orEmpty() |
There was a problem hiding this comment.
4. Unknown protobuf hides legacy index 🐞 Bug ☼ Reliability
decodeMangaExtensionIndex converts a missing protobuf extension-list field into a successful empty list rather than the null fallback signal used by getExtensions. When a repository serves a valid but unsupported protobuf envelope alongside a working JSON index, all of its extensions disappear without attempting the legacy index.
Agent Prompt
## Issue description
A protobuf document without the expected extension-list field is treated as an authoritative empty repository and suppresses legacy JSON fallback.
## Fix Focus Areas
- app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionApi.kt[57-75]
- app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionApi.kt[172-182]
## Recommended Fix
Return `null` or throw a parse exception when the expected protobuf extension-list field is absent, while preserving an explicitly present empty list as a valid empty repository.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| compressed: ByteArray, | ||
| repoUrl: String, | ||
| ): List<MangaExtension.Available> { | ||
| val decoded = GZIPInputStream(ByteArrayInputStream(compressed)).use { it.readBytes() } |
There was a problem hiding this comment.
5. Repository gzip can exhaust app memory 🐞 Bug ⛨ Security
decodeMangaExtensionIndex expands the complete gzip stream with unbounded readBytes() after the compressed response has already been materialized in memory. Refreshing any configured repository can therefore exhaust memory or CPU when its endpoint returns a highly expanding gzip payload.
Agent Prompt
## Issue description
The new repository parser fully expands arbitrary gzip responses without enforcing a decompressed-size limit.
## Fix Focus Areas
- app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionApi.kt[63-69]
- app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionApi.kt[172-178]
## Recommended Fix
Stream decompression through a bounded reader, reject indexes exceeding a conservative maximum compressed or decompressed size, and allow the existing JSON fallback to handle that rejection.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| ): Boolean = isTelevision && | ||
| itemFocused && | ||
| action == KeyEvent.ACTION_DOWN && | ||
| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT |
There was a problem hiding this comment.
2. Right-to-left download navigation breaks 🐞 Bug ≡ Correctness
focusDownloadOnTvRight and downloadFocusTarget hard-code right to enter the download action and left to return to the row. In a right-to-left layout the row places that trailing action on the visual left, so remote movement is forced opposite to the displayed direction for every episode and chapter.
Agent Prompt
## Issue description
Episode and chapter download focus navigation assumes a left-to-right layout even though the application supports right-to-left layouts.
## Fix Focus Areas
- app/src/main/java/eu/kanade/presentation/components/ItemDownloadIndicator.kt[47-63]
- app/src/main/java/eu/kanade/presentation/components/ItemDownloadIndicator.kt[65-106]
## Recommended Fix
Read `LocalLayoutDirection` and map entry and return key directions accordingly, using right-to-enter and left-to-return only for left-to-right layouts and reversing them for right-to-left layouts.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| initialItemFocusRequester: FocusRequester?, | ||
| ) { | ||
| LazyVerticalGrid( | ||
| modifier = modifier.focusGroup(), |
There was a problem hiding this comment.
6. Anime result focus can escape the list 🐞 Bug ≡ Correctness
The anime comfortable grid, compact grid, and list remove their collection-level focusGroup while adding only a requester to the first result item. On TV this allows spatial focus search to select controls outside the result collection before all nearer result children, unlike the equivalent manga result layouts that retain grouping.
Agent Prompt
## Issue description
All anime source result layouts lose collection-level focus grouping while their manga equivalents retain it.
## Fix Focus Areas
- app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceComfortableGrid.kt[35-55]
- app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceCompactGrid.kt[35-55]
- app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt[41-59]
## Recommended Fix
Restore `focusGroup()` on each anime result collection while retaining the new first-item requester used as the filter-chip destination.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| FilterChip( | ||
| modifier = Modifier.focusProperties { down = initialItemFocusRequester }, | ||
| selected = state.listing == Listing.Popular, |
There was a problem hiding this comment.
7. Empty anime results trap remote focus 🐞 Bug ≡ Correctness
Each anime listing chip assigns its Down destination to initialItemFocusRequester, although that requester is attached only to result item index zero. During loading or with an empty result set, pressing Down targets an unattached requester instead of following normal focus traversal; the parallel manga implementation avoids this by requiring TV mode and itemCount > 0.
Agent Prompt
## Issue description
Anime filter chips unconditionally direct D-pad Down to `initialItemFocusRequester`, which is attached only to the first result item. During loading or when there are no results, that destination is unbound and focus cannot reliably leave the filter row through normal traversal.
## Fix Focus Areas
- app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt[134-165]
- app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt[182-205]
- app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceComfortableGrid.kt[47-58]
## Recommended Fix
Collect the anime paging items where the filter chips are composed, then apply the explicit Down focus requester only when TV UI is active and `itemCount > 0`, matching the manga source implementation. Apply this guard consistently to every anime filter chip.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| TvRemoteAction.SeekBackward -> viewModel.handleLeftDoubleTap() | ||
| TvRemoteAction.SeekForward -> viewModel.handleRightDoubleTap() |
There was a problem hiding this comment.
3. Tv remote arrows can change episodes 🐞 Bug ≡ Correctness
PlayerActivity.onKeyDown() maps SeekBackward and SeekForward to handleLeftDoubleTap() and handleRightDoubleTap() instead of the dedicated seek methods. Those handlers honor persisted double-tap gesture preferences, so a TV user whose prior setting is Switch, PlayPause, Custom, or None gets that action when pressing D-pad Left or Right while controls are hidden.
Agent Prompt
## Issue description
TV D-pad Left and Right are classified as seek actions but are dispatched through configurable touch double-tap handlers. Persisted gesture preferences can therefore pause playback, change episodes, run a custom command, or do nothing instead of seeking.
## Fix Focus Areas
- app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt[879-880]
## Recommended Fix
Dispatch `TvRemoteAction.SeekBackward` to `viewModel.leftSeek()` and `TvRemoteAction.SeekForward` to `viewModel.rightSeek()`. Keep the gesture-preference handlers exclusively for touch gesture paths.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| internal fun isTvStorageReady( | ||
| isTvBox: Boolean, | ||
| storagePreferenceSet: Boolean, | ||
| hasExternalStorageManager: Boolean, | ||
| ): Boolean { | ||
| return storagePreferenceSet && (!isTvBox || hasExternalStorageManager) |
There was a problem hiding this comment.
8. Some tv users cannot finish onboarding 🐞 Bug ≡ Correctness
isTvStorageReady() marks TV storage incomplete when storagePref is set but all-files access is absent, while StorageStep hides the sole permission-settings button once that preference is set. Reopening onboarding after the permission is missing or revoked therefore reaches a configured-but-incomplete state with no corrective action.
Agent Prompt
## Issue description
The TV completion predicate requires all-files access even when the storage preference is already configured, but the permission launcher is hidden once that preference is set. If the permission is missing or revoked, reopening onboarding leaves the storage step incomplete without an available corrective action.
## Fix Focus Areas
- app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt[100-122]
- app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt[160-178]
## Recommended Fix
Render the storage recovery action whenever either the preference is unset or required all-files access is missing. Reuse the existing permission flow, request access before validating or creating the configured directory, and re-check completion when the user returns; alternatively, do not mark the step incomplete when an existing storage selection remains valid.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (shouldFocusDownloadOnTvRight( | ||
| isTelevision = isTelevision, | ||
| itemFocused = itemFocused.value, | ||
| keyCode = event.nativeKeyEvent.keyCode, | ||
| action = event.nativeKeyEvent.action, | ||
| ) | ||
| ) { | ||
| downloadFocusRequester.requestFocus() | ||
| true |
There was a problem hiding this comment.
9. Disabled download controls block navigation 🐞 Bug ≡ Correctness
focusDownloadOnTvRight() consumes D-pad Right after requesting the download focus target without checking whether that indicator is enabled or has a download callback. Episode and chapter rows attach this handler even while multi-selection disables downloads, so focus is directed to an unavailable control rather than continuing normal navigation.
Agent Prompt
## Issue description
TV D-pad Right is always intercepted by a focused entry row and sent to its download requester. The download indicator can be disabled during multi-selection or when no download callback is available, so this intercept prevents useful focus movement without exposing a working action.
## Fix Focus Areas
- app/src/main/java/eu/kanade/presentation/components/ItemDownloadIndicator.kt[47-105]
- app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt[133-161]
- app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt[111-115]
## Recommended Fix
Pass download availability into the TV focus modifier and consume Right only when the download indicator is enabled and actionable. Otherwise return false and allow Compose focus navigation to choose the next target.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| .onPreviewKeyEvent { event -> | ||
| if (shouldResetTvControlsAutoHide(isTelevision, event.nativeKeyEvent.action)) { | ||
| resetControls = !resetControls | ||
| } |
There was a problem hiding this comment.
10. Media remotes let controls time out 🐞 Bug ☼ Reliability
The new timeout reset changes local Compose state only from onPreviewKeyEvent, but media-session callbacks invoke the player view model directly without delivering that event. Skip and playback commands received through the active media session therefore leave an already-running visible-controls timeout unchanged.
Agent Prompt
## Issue description
The TV controls timeout is reset only by Compose key events, but media-session controls bypass Compose and call the view model directly. Remote media interaction can therefore continue while the existing auto-hide delay expires.
## Fix Focus Areas
- app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt[140-180]
- app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt[945-1014]
## Recommended Fix
Move the auto-hide reset into a shared view-model event or timeout generation state, and invoke it from both Compose input/button paths and each media-session callback that performs a player action.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Hi. I saw this pr and got interested. Would it be possible for you to upload the .apk for me to try it out? Edit: was able to build it myself but it unfortunately doesnt go past the storage step in the onboarding screen in fire tv. Anikku had fixed this issue so you guys could take a look at how it was done |
|
hey @aoba-seragakii , could you link the commit? Thanks a lot for the feedback! |
https://github.com/komikku-app/anikku/commits/b29fb99e4530a4464ab62731d25febd6ad9bae4f/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt this file history should have all of them Also I wasn't able to grant the install apps permission in fire tv, while im able in anikku. I unfortunately couldn't find the commit though |
|
Guys i switched to anikku as its supported and works with android tv, so i dont think i will work on this. |

Important: fully delegated to Codex
I used OpenAI Codex and delegated all of my implementation, integration, debugging, automated testing and this PR preparation to it because I do not have time to develop this myself. I do not understand the code well enough to independently review or vouch for it.
I have manually tried the app and reported problems while Codex made the changes. The flows I tried now seem to work, but that is not a production-readiness claim or a substitute for an experienced contributor's code review. I am opening this as a draft for review and collaboration, not as something that should be merged without review. This disclosure concerns my contribution; your original work remains credited to you.
Summary
Following your invitation to help with aniyomiorg#2377, this proposes merging my complementary TV work into your
android-tv-dpad-supportbranch, rather than opening a competing upstream TV PR.The integration preserves both histories. Merge
5414735edhas your PR head078836194as its first parent and my TV branch02151998cas its second parent. The proposed head isc15fcfa0a.This is a sizeable integration (92 changed files relative to your current branch), not just the last three fixes. The imported history also includes an earlier upstream synchronization, including the existing torrent null-safety fix from aniyomiorg#2386.
Separately reviewable manga repository change
e01f1fc15adds gzip/protobufindex.pbmanga repository support, legacy JSON fallback, optionalshortNamemetadata and regression tests. This is useful on mobile too and is not required for TV navigation. It is included in the combined test build, but should be reviewed separately and can be split out before any upstream TV merge.Latest TV follow-ups are separate commits:
6769af0b2: refine TV focus outlines.24aeb706b: restore repository/store D-pad navigation.c15fcfa0a: hide the reader touch guide on TV, with regression tests.Validation and limitations
The final local validation passed on 2026-09-08 with JDK 17:
152 unit tests, zero failures/errors/skips.
git diff --checkalso passes. These are debug build/unit results, not a claim of passing release CI; no CI run was present on my published branch when this PR was prepared.Recent direct emulator checks on the combined build (Android TV API 36, x86_64, 1920x1080) included:
I also manually tried earlier TV builds and a phone-emulator build. A complete mobile regression of this final combined branch, all base themes/tablet modes, physical TV hardware, release build/CI, and exhaustive extension/playback/dialog testing are still unverified. Please keep those as review/testing requirements. Codex inspected the code and diffs, but I have not performed an independent human code review.
Images
These are real emulator captures, not mockups. Images are hosted on a separate evidence branch in my fork so they do not add binary files to this code PR.
Device-mode onboarding retained from your TV implementation (reference capture from the emulator review, before the latest follow-up fixes):
Repository delete action reached by remote; Cancel selected in the confirmation after the focus fix:
Please feel free to request changes, keep only the useful parts, or ask for smaller contributions. I would appreciate your review before this is considered for the upstream PR.