Skip to content

Add still watching feature #4509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 22, 2025
Merged

Conversation

ConnorS1110
Copy link
Contributor

@ConnorS1110 ConnorS1110 commented Mar 9, 2025

Changes
Monitor how many episodes have been watched in a row with no remote interaction. Default behavior is if 3 episodes have been watched, or there was 90 minutes of watch time, with no interaction, a timer starts when watching the 4th episode. If timer runs out with no interaction, "Are you still watching prompt" appears. Another countdown starts and a progress bar fills the "No" button. When countdown ends, and a choice hasn't been selected playback ends. There are a series of user customizable options to configure the conditions the screen will trigger, or you can disable it all together.

Issues
Adds a feature that has been requested for 5 years now
Also requested in #1327

Summary by CodeRabbit

  • New Features

    • Introduced a "Still Watching" screen that prompts users to confirm continued viewing after a configurable period of inactivity or episode count.
    • Added user preference settings to customize the "Still Watching" behavior, including various timeout durations and a disable option.
    • Enhanced playback experience with a new overlay and timer-based confirmation dialog during binge-watching sessions.
  • Improvements

    • Replaced the previous screensaver logic with an improved interaction tracker for more accurate user activity monitoring.
    • Updated preference screens and localization to support the new inactivity timeout options.
  • Bug Fixes

    • Improved focus handling and navigation in the new "Still Watching" overlay to ensure smooth TV remote interactions.

@ConnorS1110 ConnorS1110 marked this pull request as draft March 9, 2025 05:36
@ConnorS1110
Copy link
Contributor Author

I have this like 80% done I would say, but I have hit a roadblock I could use some help with. I have the correct logic in place that will display the are you still watching screen in the correct scenario, but I can't get the actual fragment for this screen to appear. Changing the replace flag on the navigate function makes no difference. Currently just a black screen appears when trying to display the fragment, there are no errors that occur either. I largely followed the pattern used by the NextUpFragment since this is really just another variant of that screen. The playback code in general is very difficult for me to make sense of so I am hoping someone sees something I don't

@Dnkhatri
Copy link

Dnkhatri commented Mar 9, 2025

This feature might not be accepted as I think it would be preferred for this to be in the jellyfin server. So that it can be implemented by all the different client devices

@ConnorS1110
Copy link
Contributor Author

ConnorS1110 commented Mar 9, 2025

What part are you envisioning is at the server level? The logic to decide if the still watching screen should be displayed? There still would need to be changes in each of the consuming clients to display the client specific implementation of the screen and client specific implementations to notify the server if there has been any any device specific inputs to reset the counter.

@Dnkhatri
Copy link

Dnkhatri commented Mar 9, 2025

What part are you envisioning is at the server level? The logic to decide if the still watching screen should be displayed? There still would need to be changes in each of the consuming clients to display the client specific implementation of the screen and client specific implementations to notify the server if there has been any any device specific inputs to reset the counter.

Yes the logic should be decided on the server once that is implemented it can be adopted for different devices/apps to give a consistent system. @nielsvanvelzen would probably be the guy to answer this. But as far as I can tell device specific features are unlikely to get implemented.

@johnpc
Copy link

johnpc commented Mar 13, 2025

@Dnkhatri I don't think that's right - this specific feature resets the timer every time the android tv remote is used, which is not an event the server will ever be aware of. This feature definitely should be implemented on the client.

@ConnorS1110
Copy link
Contributor Author

We have discussed it on element/discord and the consensus is that there is no reason for this to be server side at all, except for user config settings to customize the conditions to trigger the screen. So this will largely stay as is

@johnpc
Copy link

johnpc commented Mar 14, 2025

Have you seen #4389? It seems to be seeking to solve the same problem.

I tried pulling both your solution and the one in #4389 and neither seemed to work right though, and also seem to introduce other buggy behavior.

@ConnorS1110
Copy link
Contributor Author

ConnorS1110 commented Mar 15, 2025

@johnpc I hadn't seen that PR thanks for sharing. It does look like it hasn't been worked on in 2 months though, idk if the contributor is still working on it and just hasn't pushed their changes or what. That PR does look like it is doing what I was hoping to accomplish. I am still not done with this one, I am having issues with fragment management to actually get the screen to appear. From my testing, it does still properly track the condition to display the still watching screen, but displaying the fragment itself is proving to be more troublesome than I thought. If you experienced other issues I would be interested to know what those issues were

@ConnorS1110 ConnorS1110 force-pushed the still-watching branch 2 times, most recently from a288436 to 428aa96 Compare March 18, 2025 03:40
@ConnorS1110 ConnorS1110 marked this pull request as ready for review March 18, 2025 03:40
@ConnorS1110
Copy link
Contributor Author

ConnorS1110 commented Mar 18, 2025

A lot of work was done here. This was based off the work @kinhelm did in PR #4389. If/when this merges, they should also get credit for it as well. I did add 2 temp settings for this feature to make it easier to test. 1 to test the episode count condition, and 1 to test the min watch time condition.

@johnpc
Copy link

johnpc commented Mar 19, 2025

Screenshot 2025-03-18 at 10 00 28 PM

It's beautiful!

Copy link

@johnpc johnpc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature is awesome - I hope it gets included in the next release.

@johnpc
Copy link

johnpc commented Mar 19, 2025

One other thing I noticed - the default focus is on "No". I don't think that makes sense. The default focus should be on "Yes" so if I grab my remote and tap it, the show continues.

Right now if I grab the remote and tap it, it closes the player.

@johnpc
Copy link

johnpc commented Mar 19, 2025

Another thing I noticed - if I choose "Yes" once, it never prompts again. I would expect that after the time elapses again, the popup would reappear.

@ConnorS1110
Copy link
Contributor Author

@johnpc Pushed again and addressed everything to the best of my ability. Now I will explain the logic behind the 2 requirements.

Episode Requirement: We need to ensure we are above the min minute threshold in order to say we have met the episode requirement. Imagine we are watching episodes that are 10 minutes long, Within 30 minutes, we would hit the condition for the default setting to show the screen, even though the min number of minutes is 90. So if we had normal to longer episodes (45 minutes - 1 hr +), this condition will likely get tripped

Time Requirement: There needs to be another condition that handles if you are watching a lot of short episodes. That is what this is for. The logic has been changed so that watchTime must be greater than or equal to the min minute threshold of your user setting AND your episode count must be greater than the number from your user setting. This ensures we capture short media to still display the screen at an appropriate interval.

Hope this makes it make more sense

fun onEpisodeWatched() {
Timber.i("Watcher onEpisodeWatched")
if (!itemWasInterrupted) episodeCount++
itemWasInterrupted = false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this seems to affect whether I touch the remote at all during an episode. So if I touch the remote at the beginning of an episode (to set subtitles or see how long it is or something), it essentially doesn't count toward the still watching feature

val currentEpisodeProgress = videoManager.currentPosition.toFloat() / videoManager.duration.toFloat()
val minMinutesInMs = stillWatchingSetting.minMinutes * MILLIS_PER_MIN

// At episode count, your watch time is above min minute threshold, and you are at least 10% of the way through the next episode
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw your explanation

Episode Requirement: We need to ensure we are above the min minute threshold in order to say we have met the episode requirement. Imagine we are watching episodes that are 10 minutes long, Within 30 minutes, we would hit the condition for the default setting to show the screen, even though the min number of minutes is 90. So if we had normal to longer episodes (45 minutes - 1 hr +), this condition will likely get tripped

The code for this is

		val episodeRequirementMet = episodeCount == stillWatchingSetting.episodeCount - 1 && watchTime >= minMinutesInMs && currentEpisodeProgress >= 0.1

The 10% of currentEpisodeProgress still throws me off. and I don't think the episodeCount == stillWatchingSetting.episodeCount - 1 makes sense either. Can you clarify what we're going for as it related to the code?

@johnpc
Copy link

johnpc commented Mar 19, 2025

Also just a reminder, my suggestions come from an interest in your feature, a love of jellyfin and a desire to build great software, but I am not a maintainer just a guy so that's all they are is suggestions. :D

@ConnorS1110
Copy link
Contributor Author

At this point, I am going to wait for a maintainer to give thoughts on my implementation before working on this more. Hopefully this can get finalized soon and I can start working on making a matching implementation for web

Copy link
Member

@nielsvanvelzen nielsvanvelzen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have interaction tracking for our screensaver so I'd rather add a new "InteractionTracker" that will be used by both the screensaver and this feature.

We will not be adding popups during video playback, that's an anti-pattern. Instead, this feature should be implemented in the next-up screen: when a video item ends and we want to know if a user is still watching it will show a different screen that will behave similarly to the popup you currently have.

All new UI must be written in Compose.

@ConnorS1110
Copy link
Contributor Author

ConnorS1110 commented Apr 3, 2025

@nielsvanvelzen almost done with the changes you requested, but a design question. Thoughts on the screen looking nearly identical to the "Next Up" screen, except the words "Are you still watching?" along with the buttons are in the center of the screen? I thought about still having the next up info in the bottom right corner, just minus the buttons. Or did you envision this screen would look literally EXACTLY the same as the Next Up screen, just with the verbiage describing the next episode removed with just the "Are you still watching?" question with action buttons?

Edit: I did just push up everything. Only thing left is just finalizing the visual design, but the buttons work, the screen displays only at the end of an episode, I combined the viewmodel for the screensaver and this feature, updated all the references for both features, and the user settings still work

Copy link
Member

@nielsvanvelzen nielsvanvelzen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally had some time to look at this PR. Started out with testing it out and here are my findings from that:

  • When the title of the next playing episode is too long the thumbnail is incorrectly positioned
  • The buttons don't have the most intuitive labels, I'd say maybe use "Continue watching" and "Exit" instead of "yes" and "no".
  • The preference title should not use word casing and there is no need for the "settings" suffix. Maybe call it "Show inactivity timeout popup" or something like that
  • While useful for testing, I don't think the 2eps/1min or 5eps/0.5min options should be kept
  • I find the default option should be to disable this behavior

@jellyfin-bot jellyfin-bot added the merge conflict Conflicts prevent merging label Jun 6, 2025
@jellyfin-bot jellyfin-bot removed the merge conflict Conflicts prevent merging label Jun 14, 2025
@ConnorS1110
Copy link
Contributor Author

@nielsvanvelzen had time to go back and address all your comments. Let me know if there's any additional changes you want. ALso tried testing your long title issue and I couldn't repro. If the title is long, it just turns into an ellipsis (the same as the Next Up screen) and the thumbnail looks correct. Maybe you had a weird build or something or maybe its something specific about the title you were seeing?

@jellyfin-bot jellyfin-bot added the merge conflict Conflicts prevent merging label Jun 16, 2025
# Conflicts:
#	app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java
@ConnorS1110
Copy link
Contributor Author

@nielsvanvelzen all comments have been addressed

@nielsvanvelzen
Copy link
Member

@coderabbitai full review

@nielsvanvelzen nielsvanvelzen added this to the v0.19.0 milestone Jun 22, 2025
Copy link

coderabbitai bot commented Jun 22, 2025

✅ Actions performed

Full review triggered.

@nielsvanvelzen nielsvanvelzen added enhancement New feature or request release-highlight Pull request might be suitable for mentioning in the release blog post labels Jun 22, 2025
Copy link

coderabbitai bot commented Jun 22, 2025

📝 Walkthrough

Walkthrough

This update introduces a "Still Watching" feature to the Android TV app, replacing the previous screensaver logic. It adds new ViewModels, UI fragments, data models, and user preferences to manage and display inactivity prompts during media playback. The codebase is refactored to use the new InteractionTrackerViewModel for interaction and screensaver tracking.

Changes

File(s) / Group Change Summary
.../di/AppModule.kt Dependency injection updated: replaced ScreensaverViewModel with singleton InteractionTrackerViewModel; added StillWatchingViewModel provider.
.../preference/UserPreferences.kt Added stillWatchingBehavior enum preference with default DISABLED.
.../preference/constant/StillWatchingBehavior.kt New enum StillWatchingBehavior defines inactivity timeout presets.
.../ui/InteractionTrackerViewModel.kt New ViewModel for tracking user interaction, screensaver, and "still watching" logic.
.../ui/ScreensaverViewModel.kt Removed old screensaver ViewModel and its methods/properties.
.../ui/browsing/MainActivity.kt All references to ScreensaverViewModel replaced with InteractionTrackerViewModel; method calls updated with new signatures.
.../ui/itemdetail/FullDetailsFragment.java Injected InteractionTrackerViewModel; notifies tracker on playback session start.
.../ui/navigation/Destinations.kt Added navigation destination for StillWatchingFragment.
.../ui/picture/PictureViewerFragment.kt Switched from ScreensaverViewModel to InteractionTrackerViewModel for lifecycle lock.
.../ui/playback/CustomPlaybackOverlayFragment.java Cached PlaybackController locally; added showStillWatching(UUID) method for navigation.
.../ui/playback/PlaybackController.java Integrated InteractionTrackerViewModel; updated logic to show "Still Watching" or "Next Up" UI based on settings and tracker state; calls to tracker on playback events.
.../ui/playback/PlaybackOverlayFragmentHelper.kt Switched to InteractionTrackerViewModel for screensaver lock management.
.../ui/playback/rewrite/PlaybackRewriteFragment.kt Replaced ScreensaverViewModel with InteractionTrackerViewModel for lifecycle lock.
.../ui/playback/stillwatching/StillWatchingFragment.kt New fragment and Compose UI for "Still Watching" prompt, including overlay and timer logic.
.../ui/playback/stillwatching/StillWatchingItemData.kt New data class for holding item info for "Still Watching" UI.
.../ui/playback/stillwatching/StillWatchingPresetConfigs.kt New enum for inactivity timeout preset configurations.
.../ui/playback/stillwatching/StillWatchingState.kt New enum for "Still Watching" UI states.
.../ui/playback/stillwatching/StillWatchingStates.kt New data class and companion for mapping presets to state configs.
.../ui/playback/stillwatching/StillWatchingViewModel.kt New ViewModel managing "Still Watching" UI state and data loading.
.../ui/preference/screen/PlaybackPreferencesScreen.kt Added preference UI for selecting "Still Watching" behavior.
.../ui/screensaver/InAppScreensaver.kt Switched to InteractionTrackerViewModel for screensaver visibility and interaction.
.../res/values/strings.xml Added string resources for "Still Watching" feature, durations, and UI labels.

Sequence Diagram(s)

"Still Watching" Flow During Playback

sequenceDiagram
    participant User
    participant MainActivity
    participant InteractionTrackerViewModel
    participant PlaybackController
    participant StillWatchingViewModel
    participant StillWatchingFragment

    User->>MainActivity: Starts playback
    MainActivity->>PlaybackController: Initialize playback
    PlaybackController->>InteractionTrackerViewModel: notifyStart(item)
    loop Watching episodes
        PlaybackController->>InteractionTrackerViewModel: onEpisodeWatched()
        InteractionTrackerViewModel-->>PlaybackController: getShowStillWatching()
        alt Threshold reached and enabled
            PlaybackController->>CustomPlaybackOverlayFragment: showStillWatching(itemId)
            CustomPlaybackOverlayFragment->>StillWatchingFragment: Navigate to StillWatchingFragment(itemId)
            StillWatchingFragment->>StillWatchingViewModel: setItemId(itemId)
            StillWatchingViewModel->>StillWatchingFragment: Provide item data and state
            User->>StillWatchingFragment: Choose "Continue Watching" or "Exit"
            alt Continue Watching
                StillWatchingFragment->>StillWatchingViewModel: stillWatching()
                StillWatchingFragment->>PlaybackController: Resume playback
            else Exit
                StillWatchingFragment->>StillWatchingViewModel: close()
                StillWatchingFragment->>PlaybackController: End playback
            end
        else Continue playback
        end
    end
Loading

Interaction and Screensaver Management

sequenceDiagram
    participant User
    participant MainActivity
    participant InteractionTrackerViewModel
    participant InAppScreensaver

    User->>MainActivity: Key/Touch event
    MainActivity->>InteractionTrackerViewModel: notifyInteraction(canCancel, userInitiated)
    InteractionTrackerViewModel->>InAppScreensaver: Update visible state
    InAppScreensaver->>User: Show/hide screensaver UI
Loading

These diagrams illustrate the main flows for the new "Still Watching" feature and the refactored interaction/screen management logic.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

coderabbitai[bot]
coderabbitai bot previously requested changes Jun 22, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (8)
app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java (1)

1224-1224: Verify session tracking timing.

A past review comment raised a valid concern about calling notifyStartSession from here rather than from the actual video player. This could lead to session tracking inaccuracies if playback fails to start after this point or if the user cancels before the video player launches.

app/src/main/res/values/strings.xml (1)

1-592: Verify usage of test strings mentioned in past reviews.

The past review comments mention unused test strings (lbl_still_watching_test_episode_count and lbl_still_watching_test_min_minutes). Please verify these are actually being used in the implementation or remove them if they're no longer needed.

#!/bin/bash
# Search for usage of the test strings mentioned in past reviews
echo "Searching for test string usage..."
rg -n "lbl_still_watching_test_episode_count|lbl_still_watching_test_min_minutes"
app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java (3)

222-226: Remove controller caching - conflicts with previous review guidance.

This change goes against the explicit guidance from nielsvanvelzen in previous reviews: "We explicitly use a holder-class because the actual playback controller can change. So while it's fine to store the container in a variable you should NOT store the playback controller itself."

Revert to accessing the controller through the container each time:

-        PlaybackController playbackController = playbackControllerContainer.getValue().getPlaybackController();
-
-        if (playbackController != null) {
-            playbackController.init(new VideoManager(requireActivity(), view, helper), this);
+        if (playbackControllerContainer.getValue().getPlaybackController() != null) {
+            playbackControllerContainer.getValue().getPlaybackController().init(new VideoManager(requireActivity(), view, helper), this);
         }

442-461: Remove controller caching - conflicts with previous review guidance.

Same issue as above - this caching approach was explicitly discouraged in previous reviews.

Revert to accessing through the container:

-            PlaybackController playbackController = playbackControllerContainer.getValue().getPlaybackController();
-
-            if (playbackController != null) {
-                if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
-                    playbackController.play(0);
-                    return true;
-                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
-                    playbackController.pause();
-                    return true;
-                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
-                    playbackController.playPause();
-                    return true;
-                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_BUTTON_R1 || keyCode == KeyEvent.KEYCODE_BUTTON_R2) {
-                    playbackController.fastForward();
-                    return true;
-                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND || keyCode == KeyEvent.KEYCODE_BUTTON_L1 || keyCode == KeyEvent.KEYCODE_BUTTON_L2) {
-                    playbackController.rewind();
-                    return true;
-                }
+            if (playbackControllerContainer.getValue().getPlaybackController() != null) {
+                if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+                    playbackControllerContainer.getValue().getPlaybackController().play(0);
+                    return true;
+                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+                    playbackControllerContainer.getValue().getPlaybackController().pause();
+                    return true;
+                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+                    playbackControllerContainer.getValue().getPlaybackController().playPause();
+                    return true;
+                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_BUTTON_R1 || keyCode == KeyEvent.KEYCODE_BUTTON_R2) {
+                    playbackControllerContainer.getValue().getPlaybackController().fastForward();
+                    return true;
+                } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND || keyCode == KeyEvent.KEYCODE_BUTTON_L1 || keyCode == KeyEvent.KEYCODE_BUTTON_L2) {
+                    playbackControllerContainer.getValue().getPlaybackController().rewind();
+                    return true;
+                }
             }

666-666: Remove controller caching.

Another instance of the same caching issue.

-        if (playbackControllerContainer.getValue().getPlaybackController() == null || !playbackControllerContainer.getValue().getPlaybackController().hasFragment()) {
+        PlaybackController controller = playbackControllerContainer.getValue().getPlaybackController();
+        if (controller == null || !controller.hasFragment()) {
app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java (1)

1182-1182: Proper episode completion tracking

The onEpisodeWatched call correctly tracks episode completion before processing the next item, ensuring accurate counting for the still watching feature.

app/src/main/java/org/jellyfin/androidtv/ui/InteractionTrackerViewModel.kt (1)

27-28: Add empty line after group comment

Group name comments should have an empty line following them for better readability.

 	// Screensaver vars
+
 	private var timer: Job? = null
app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingStates.kt (1)

5-13: Consider moving getSetting logic to the enum class

Based on past review feedback, consider moving this mapping logic to the StillWatchingPresetConfigs enum itself as a method, which would eliminate the need for the when statement and make the code more object-oriented.

This would allow StillWatchingPresetConfigs.DEFAULT.toStates() instead of StillWatchingStates.getSetting(StillWatchingPresetConfigs.DEFAULT).

🧹 Nitpick comments (4)
app/src/main/java/org/jellyfin/androidtv/ui/navigation/Destinations.kt (1)

3-3: Remove unused import.

The bundleOf import appears to be unused in this file and should be removed to keep the imports clean.

-import androidx.core.os.bundleOf
app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingStates.kt (1)

3-3: Use a more specific type for minMinutes.

The minMinutes property uses the Number type, which is abstract and not commonly used for such values. Consider using Int or Long for better type safety and clarity.

-data class StillWatchingStates(val enabled: Boolean, val episodeCount: Int, val minMinutes: Number) {
+data class StillWatchingStates(val enabled: Boolean, val episodeCount: Int, val minMinutes: Int) {
app/src/main/java/org/jellyfin/androidtv/ui/InteractionTrackerViewModel.kt (1)

125-127: Simplify complex condition for better readability

The condition is complex with 4 parts. Consider extracting it to a well-named method to improve readability and maintainability.

+	private fun shouldHideScreensaver(canCancel: Boolean): Boolean {
+		return _screensaverVisible.value && (canCancel || !inAppEnabled || activityPaused)
+	}
+
 	fun notifyInteraction(canCancel: Boolean, userInitiated: Boolean) {
 		// ... existing code ...
 
 		// Hide screensaver when interacted with allowed cancellation or when disabled
-		if (_screensaverVisible.value && (canCancel || !inAppEnabled || activityPaused)) {
+		if (shouldHideScreensaver(canCancel)) {
 			_screensaverVisible.value = false
 		}
app/src/main/java/org/jellyfin/androidtv/preference/constant/StillWatchingBehavior.kt (1)

15-15: Fix indentation formatting

There's a formatting inconsistency with the leading whitespace on this line.

- 	*/
+	*/
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 006f171 and 3456ceb.

📒 Files selected for processing (22)
  • app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt (3 hunks)
  • app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt (2 hunks)
  • app/src/main/java/org/jellyfin/androidtv/preference/constant/StillWatchingBehavior.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/InteractionTrackerViewModel.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/ScreensaverViewModel.kt (0 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt (11 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java (3 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/navigation/Destinations.kt (3 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerFragment.kt (3 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java (4 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java (9 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackOverlayFragmentHelper.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/PlaybackRewriteFragment.kt (3 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingFragment.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingItemData.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingPresetConfigs.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingState.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingStates.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingViewModel.kt (1 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt (2 hunks)
  • app/src/main/java/org/jellyfin/androidtv/ui/screensaver/InAppScreensaver.kt (2 hunks)
  • app/src/main/res/values/strings.xml (4 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/java/org/jellyfin/androidtv/ui/ScreensaverViewModel.kt
🧰 Additional context used
🪛 GitHub Check: detekt
app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingFragment.kt

[warning] 130-130: One method should have one responsibility. Long methods tend to handle many things at once. Prefer smaller methods to make them easier to understand.
The function StillWatchingOverlay is too long (93). The maximum length is 60.

app/src/main/java/org/jellyfin/androidtv/ui/InteractionTrackerViewModel.kt

[warning] 94-94: The caught exception is swallowed. The original exception could be lost.
The caught exception is swallowed. The original exception could be lost.


[warning] 118-118: Complex conditions should be simplified and extracted into well-named methods if necessary.
This condition is too complex (4). Defined complexity threshold for conditions is set to '4'

🪛 GitHub Check: Android Lint
app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingViewModel.kt

[warning] 22-22: Static Field Leaks
This field leaks a context object

🪛 detekt (1.23.8)
app/src/main/java/org/jellyfin/androidtv/ui/InteractionTrackerViewModel.kt

[warning] 94-94: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (36)
app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt (1)

67-71: Well-structured preference integration following established patterns.

The implementation correctly follows the existing code patterns for enum preferences, including proper binding and conditional visibility based on media queuing being enabled.

app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingPresetConfigs.kt (1)

3-9: Clean enum implementation with appropriate preset values.

The enum provides a good range of preset configurations from SHORT to DISABLED, enabling flexible user customization of the still watching behavior.

app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingState.kt (1)

3-8: Well-defined state enum for UI state management.

The enum values logically represent the different states of the still watching UI flow, providing clear state transitions for the feature.

app/src/main/java/org/jellyfin/androidtv/ui/navigation/Destinations.kt (1)

146-148: Navigation destination correctly implemented.

The reuse of NextUpFragment.ARGUMENT_ITEM_ID is appropriate since both features work with the same item data type, maintaining consistency in the navigation pattern.

app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java (1)

154-154: Dependency injection correctly implemented.

The lazy injection of InteractionTrackerViewModel follows the established pattern used throughout the class.

app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt (1)

13-13: LGTM! Preference implementation follows established patterns.

The new stillWatchingBehavior preference correctly follows the naming convention (no pref_ prefix as mentioned in past reviews) and uses an appropriate default value of DISABLED. The implementation is consistent with other enum preferences in the file.

Also applies to: 91-94

app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerFragment.kt (1)

23-23: LGTM! Clean migration from ScreensaverViewModel to InteractionTrackerViewModel.

The changes correctly migrate from the old ScreensaverViewModel to the new InteractionTrackerViewModel while preserving the existing lifecycle lock functionality. The renaming is consistent and follows the pattern used across other files in this PR.

Also applies to: 48-48, 80-80

app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt (1)

29-29: LGTM! Dependency injection setup is correctly implemented.

The new ViewModels are properly registered with appropriate scopes:

  • InteractionTrackerViewModel as a singleton (suitable for tracking user interactions across the app)
  • StillWatchingViewModel as a view model (suitable for UI state management)

Both ViewModels have the correct dependency injection parameters and follow established Koin patterns.

Also applies to: 36-36, 110-110, 125-125

app/src/main/res/values/strings.xml (1)

44-44: LGTM! String resources appropriately support the still watching feature.

The new string resources provide comprehensive text support for the still watching feature including preference titles, timeout options, and UI labels. All strings appear relevant and well-named.

Also applies to: 261-261, 365-368, 550-550

app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackOverlayFragmentHelper.kt (1)

3-3: LGTM! Consistent ViewModel migration maintains existing functionality.

The migration from ScreensaverViewModel to InteractionTrackerViewModel is correctly implemented, preserving the existing screensaver lock mechanism while following the consistent pattern established across other files in this PR.

Also applies to: 9-9, 14-14

app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/PlaybackRewriteFragment.kt (4)

15-15: LGTM: Import statement updated correctly.

The import change from ScreensaverViewModel to InteractionTrackerViewModel is consistent with the broader refactoring across the app.


40-40: LGTM: Property declaration updated correctly.

The property rename and type change are consistent with the ViewModel refactoring while maintaining the same injection pattern.


70-70: ```shell
#!/bin/bash

Search for InteractionTrackerViewModel class definition

rg --color never -n 'class InteractionTrackerViewModel' -C3

Search for addLifecycleLock method signature

rg --color never -n 'fun addLifecycleLock' -C3


---

`15-15`: **LGTM: Clean migration to InteractionTrackerViewModel**

The refactoring from `ScreensaverViewModel` to `InteractionTrackerViewModel` is implemented correctly with proper import updates, property renaming, and preserved lifecycle lock management logic.




Also applies to: 40-40, 70-70

</details>
<details>
<summary>app/src/main/java/org/jellyfin/androidtv/ui/screensaver/InAppScreensaver.kt (4)</summary>

`19-19`: **LGTM: Import statement updated consistently.**

The import change aligns with the broader ViewModel refactoring across the codebase.

---

`24-25`: **LGTM: ViewModel usage updated correctly.**

The ViewModel instantiation and state collection have been updated consistently with the refactoring while maintaining the same usage patterns.

---

`40-40`: To locate the method definition, let’s search for its declaration across the repo:


```shell
#!/bin/bash
# Find the notifyInteraction method signature
rg "fun notifyInteraction" -n

19-19: LGTM: Successful migration with enhanced interaction tracking

The transition to InteractionTrackerViewModel maintains the existing screensaver functionality while introducing more granular interaction tracking through the notifyInteraction method's canCancel and userInitiated parameters.

Also applies to: 24-25, 40-40

app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingItemData.kt (2)

7-13: LGTM: Well-structured data class.

The StillWatchingItemData class is well-designed with appropriate property types:

  • BaseItemDto for the core item data
  • UUID for unique identification
  • Optional JellyfinImage properties for UI display
  • Clean, focused data structure without unnecessary complexity

7-13: LGTM: Well-designed data class for still watching feature

The StillWatchingItemData class provides a clean, well-typed container for the still watching prompt with appropriate nullable image properties and comprehensive item information.

app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingStates.kt (1)

5-13: Well-designed configuration mapping that addresses previous concerns.

The getSetting method effectively addresses past review concerns:

  1. Long episodes concern: The dual-threshold approach (episode count OR minutes) ensures the prompt triggers appropriately for both short and long content. For example, a single 150+ minute movie would trigger the LONG preset's time threshold even without multiple episodes.

  2. Enum usage: The method properly leverages the StillWatchingPresetConfigs enum with exhaustive when-expression mapping.

The configuration values appear well-balanced for different viewing patterns, from SHORT (2 episodes/60min) to VERY_LONG (8 episodes/240min).

app/src/main/java/org/jellyfin/androidtv/preference/constant/StillWatchingBehavior.kt (2)

6-29: Excellent enum implementation with clear documentation.

The StillWatchingBehavior enum is well-structured and addresses previous feedback:

  1. Clear behavior description: Each enum value documents the exact thresholds (episodes and minutes) rather than referencing external services, addressing the past comment about describing actual functionality.

  2. Comprehensive options: Provides a good range from SHORT (2 episodes/60min) to VERY_LONG (8 episodes/240min), plus a DISABLED option for users who prefer not to use the feature.

  3. Proper implementation: Correctly implements PreferenceEnum with appropriate string resource references.

The documentation is factual, specific, and user-friendly.


6-29: LGTM: Well-documented enum with clear behavior descriptions

The enum provides a comprehensive range of still watching behaviors with clear documentation that describes the actual functionality rather than external references. The implementation properly follows the PreferenceEnum pattern with appropriate string resource usage.

app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java (1)

1325-1330: LGTM: Well-implemented navigation method.

The showStillWatching method follows the same pattern as showNextUp with proper navigation guard using the navigating flag.

app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingViewModel.kt (1)

26-66: LGTM: Well-structured ViewModel implementation.

The ViewModel properly uses StateFlow for reactive UI updates, handles coroutines correctly with viewModelScope, and follows good practices with IO dispatcher for network calls. The use of itemImages extension properties aligns with recent framework changes.

app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt (5)

25-25: LGTM: Correct import update for ViewModel replacement.

The import has been properly updated to use the new InteractionTrackerViewModel.


41-41: LGTM: ViewModel injection updated correctly.

The ViewModel injection has been properly updated to use InteractionTrackerViewModel.


59-74: LGTM: Method calls updated with appropriate parameters.

The method calls have been correctly updated to match the new API. The parameter usage is appropriate:

  • keepScreenOn flow access is correct
  • notifyInteraction(canCancel = false, userInitiated = false) for navigation actions is logical

89-90: LGTM: Activity lifecycle handling preserved.

The activity pause/resume handling has been correctly updated to use the new ViewModel while preserving the same logic.

Also applies to: 106-107


121-121: LGTM: Interaction tracking calls properly updated.

All interaction tracking calls have been consistently updated with appropriate parameter values:

  • User-initiated interactions correctly set userInitiated = true
  • System interactions correctly set userInitiated = false
  • canCancel parameter is appropriately set based on the event type

Also applies to: 140-142, 161-161, 167-169, 179-181, 190-192

app/src/main/java/org/jellyfin/androidtv/ui/playback/stillwatching/StillWatchingFragment.kt (3)

61-61: LGTM: Proper use of Duration API.

Good use of Duration.Companion.seconds instead of hardcoded milliseconds, addressing previous feedback about using proper duration types.


63-128: LGTM: Well-structured Compose screen implementation.

The screen properly manages state, handles navigation actions, and follows good Compose patterns with proper focus management and background handling.


242-259: LGTM: Clean fragment implementation.

The fragment properly handles argument extraction and integrates well with the Compose UI system.

app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java (3)

802-804: Good formatting improvement

Adding braces to the if statement improves code consistency and prevents potential bugs.


1096-1111: Well-implemented still watching integration

The logic correctly integrates the still watching feature with the existing next up functionality. The precedence given to "Still Watching" over "Next Up" when both are enabled is a reasonable design choice that prioritizes user engagement checks.


1132-1132: Correct placement of interaction tracking

The notifyStart call is properly placed after the playback state is set to PLAYING, ensuring accurate tracking of playback sessions.

Copy link
Member

@nielsvanvelzen nielsvanvelzen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could probably find more nitpicks but overall these changes are good and the functionality works as expected. I've made a few final small changes in the last commits.

Thanks for your work on this!

@nielsvanvelzen nielsvanvelzen merged commit 1743ab6 into jellyfin:master Jun 22, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request release-highlight Pull request might be suitable for mentioning in the release blog post
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants