diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 02aaade313..a7bafe3ff3 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -15,6 +15,7 @@
- [3l0w](https://github.com/3l0w)
- [MajMongoose](https://github.com/majmongoose)
- [Olaren15](https://github.com/Olaren15)
+ - [Kinhelm](https://github.com/Kinhelm)
# Emby Contributors
diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt
index cb66204911..70e274c918 100644
--- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt
+++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt
@@ -27,6 +27,7 @@ import org.jellyfin.androidtv.ui.navigation.NavigationAction
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.screensaver.InAppScreensaver
import org.jellyfin.androidtv.ui.startup.StartupActivity
+import org.jellyfin.androidtv.util.WatchTracker
import org.jellyfin.androidtv.util.applyTheme
import org.jellyfin.androidtv.util.isMediaSessionKeyEvent
import org.koin.android.ext.android.inject
@@ -165,6 +166,7 @@ class MainActivity : FragmentActivity() {
onKeyEvent(keyCode, event) || super.onKeyUp(keyCode, event)
override fun onUserInteraction() {
+ WatchTracker.onUserInteraction()
super.onUserInteraction()
screensaverViewModel.notifyInteraction(false)
diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java
index 622d75da05..319a349a58 100644
--- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java
+++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java
@@ -26,6 +26,7 @@
import org.jellyfin.androidtv.ui.livetv.TvManager;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
+import org.jellyfin.androidtv.util.WatchTracker;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.androidtv.util.profile.ExoPlayerProfile;
import org.jellyfin.androidtv.util.sdk.compat.JavaCompat;
@@ -1168,6 +1169,7 @@ public void onError() {
@Override
public void onCompletion() {
+ WatchTracker.INSTANCE.onEpisodeWatched();
Timber.d("On Completion fired");
itemComplete();
}
diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java
index 28e9015264..47b5128c37 100644
--- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java
+++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java
@@ -44,6 +44,7 @@
import org.jellyfin.androidtv.data.compat.StreamInfo;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.preference.constant.ZoomMode;
+import org.jellyfin.androidtv.util.WatchTracker;
import org.jellyfin.sdk.api.client.ApiClient;
import org.jellyfin.sdk.model.api.MediaStream;
import org.jellyfin.sdk.model.api.MediaStreamType;
@@ -162,6 +163,8 @@ public void onTracksChanged(Tracks tracks) {
Timber.d("Tracks changed");
}
});
+
+ WatchTracker.INSTANCE.startWatchTime(activity, this);
}
public void subscribe(@NonNull PlaybackControllerNotifiable notifier) {
@@ -276,16 +279,19 @@ public void start() {
_helper.getFragment().closePlayer();
return;
}
+ WatchTracker.INSTANCE.startWatchTime(this.mActivity, this);
mExoPlayer.setPlayWhenReady(true);
normalWidth = mExoPlayerView.getLayoutParams().width;
normalHeight = mExoPlayerView.getLayoutParams().height;
}
public void play() {
+ WatchTracker.INSTANCE.startWatchTime(this.mActivity, this);
mExoPlayer.setPlayWhenReady(true);
}
public void pause() {
+ WatchTracker.INSTANCE.stopWatchTime();
mExoPlayer.setPlayWhenReady(false);
}
diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/VideoPlayerAdapter.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/VideoPlayerAdapter.java
index 41b39d32ef..33fc7cc023 100644
--- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/VideoPlayerAdapter.java
+++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/VideoPlayerAdapter.java
@@ -7,6 +7,7 @@
import org.jellyfin.androidtv.ui.playback.CustomPlaybackOverlayFragment;
import org.jellyfin.androidtv.ui.playback.PlaybackController;
import org.jellyfin.androidtv.util.Utils;
+import org.jellyfin.androidtv.util.WatchTracker;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import org.jellyfin.sdk.model.api.ChapterInfo;
import org.jellyfin.sdk.model.api.MediaSourceInfo;
diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlayPauseAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlayPauseAction.kt
index c5e23e98c6..896a9642ca 100644
--- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlayPauseAction.kt
+++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlayPauseAction.kt
@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.ui.playback.overlay.action
import android.content.Context
import androidx.leanback.widget.PlaybackControlsRow
import org.jellyfin.androidtv.ui.playback.overlay.VideoPlayerAdapter
+import org.jellyfin.androidtv.util.WatchTracker
class PlayPauseAction(context: Context) : PlaybackControlsRow.PlayPauseAction(context),
AndroidAction {
diff --git a/app/src/main/java/org/jellyfin/androidtv/util/WatchTracker.kt b/app/src/main/java/org/jellyfin/androidtv/util/WatchTracker.kt
new file mode 100644
index 0000000000..4ed44151d0
--- /dev/null
+++ b/app/src/main/java/org/jellyfin/androidtv/util/WatchTracker.kt
@@ -0,0 +1,120 @@
+package org.jellyfin.androidtv.util
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import org.jellyfin.androidtv.R
+import org.jellyfin.androidtv.ui.playback.VideoManager
+import java.util.logging.Logger
+
+interface PromptUserCallback {
+ fun onResult(result: Boolean)
+}
+
+object WatchTracker {
+ private var episodeCount = 0
+ private var watchTime = 0L
+ private var lastUpdateTime= 0L
+ private var lastInteractionTime = System.currentTimeMillis()
+ private val watchTimeHandler = Handler(Looper.getMainLooper())
+ private var isPlaying: Boolean = false
+
+ private class WatchTimeUpdater(
+ private val context: Context,
+ private val videoManager: VideoManager
+ ) : Runnable {
+ override fun run() {
+ if (isPlaying) {
+ val currentTime = System.currentTimeMillis()
+ watchTime += currentTime - lastUpdateTime
+ lastUpdateTime = currentTime
+ checkPrompt(context, videoManager)
+ watchTimeHandler.postDelayed(this, 1000)
+ }
+ }
+ }
+
+ fun onEpisodeWatched() {
+ Logger.getLogger(WatchTracker::class.java.name).info("Watcher onEpisodeWatched")
+ episodeCount++
+ }
+
+ fun onUserInteraction() {
+ Logger.getLogger(WatchTracker::class.java.name).info("Watcher onUserInteraction")
+ resetWatchTime()
+ lastInteractionTime = System.currentTimeMillis()
+ }
+
+ private fun checkPrompt(context: Context, videoManager: VideoManager) {
+ if (episodeCount == 3 || watchTime >= 90 * 60 * 1000) {
+ videoManager.pause()
+ promptUser(context, object : PromptUserCallback {
+ override fun onResult(result: Boolean) {
+ if (result) {
+ videoManager.play()
+ } else {
+ context.getActivity()?.finish()
+ }
+ }
+ })
+ }
+ }
+
+ private fun promptUser(context: Context, callback: PromptUserCallback) {
+ val dialog = AlertDialog.Builder(context)
+ .setTitle(context.getString(R.string.still_watching_title))
+ .setMessage(context.getString(R.string.continue_watching_message))
+ .setPositiveButton(R.string.lbl_yes) { dialog, _ ->
+ dialog.dismiss()
+ callback.onResult(true)
+ }
+ .setNegativeButton(R.string.lbl_no) { dialog, _ ->
+ dialog.dismiss()
+ callback.onResult(false)
+ }
+ .setCancelable(false)
+ .create()
+
+ dialog.show()
+
+ val negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+ val handler = Handler(Looper.getMainLooper())
+ val startTime = System.currentTimeMillis()
+ val countdownTime = 15000L // 15 seconds
+
+ handler.post(object : Runnable {
+ override fun run() {
+ val elapsedTime = System.currentTimeMillis() - startTime
+ val remainingTime = (countdownTime - elapsedTime) / 1000
+ if (remainingTime > 0) {
+ negativeButton.text = context.getString(R.string.no_button_text_with_time, remainingTime)
+ handler.postDelayed(this, 1000)
+ } else {
+ if (dialog.isShowing) {
+ dialog.dismiss()
+ callback.onResult(false)
+ }
+ }
+ }
+ })
+ }
+
+ fun startWatchTime(context: Context, videoManager: VideoManager) {
+ Logger.getLogger(WatchTracker::class.java.name).info("Start tracker")
+ resetWatchTime()
+ lastUpdateTime = System.currentTimeMillis()
+ watchTimeHandler.post(WatchTimeUpdater(context, videoManager))
+ }
+
+ fun stopWatchTime() {
+ isPlaying = false
+ watchTimeHandler.removeCallbacksAndMessages(null)
+ }
+
+ private fun resetWatchTime() {
+ watchTime = 0L
+ episodeCount = 0
+ isPlaying = true
+ }
+}
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0b4224b86f..ca221144f1 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -461,6 +461,9 @@
%1$s gelöscht
Bildschirmschoner
In-App-Bildschirmschoner verwenden
+ Schaust du noch?
+ Möchtest du weiter schauen?
+ Nein (%1$d)
- %1$s Sekunde
- %1$s Sekunden
@@ -566,4 +569,4 @@
Sprungweite vorwärts
%1$dh %2$dm
%1$dm
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index afe90eb79f..1e773727e3 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -463,6 +463,9 @@
Delete item
Recently added in %1$s
Start screensaver after
+ Are you still watching?
+ Do you want to continue watching?
+ No (%1$d)
- %1$s minute
- %1$s minutes
@@ -564,4 +567,4 @@
Ask to skip
Skip forward length
Enable trickplay in video player
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 25c0c49735..c3bf9114af 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -467,6 +467,9 @@
Lancer l\'économiseur d\'écran après
Économiseur d\'écran
Non défini
+ Regardez-vous toujours ?
+ Voulez-vous continuer à regarder ?
+ Non (%1$d)
- %1$s seconde
- %1$s secondes
@@ -576,4 +579,4 @@
Longueur de l\'avance rapide
%1$dh %2$dm
%1$dm
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 14d10e8655..5dd1bf7797 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -464,6 +464,9 @@
Pokaż wygaszacz ekranu Jellyfin podczas uruchomienia aplikacji
Użyj wbudowanego wygaszacza ekranu
Uruchom wygaszacz ekranu po
+ Czy nadal oglądasz?
+ Czy chcesz kontynuować oglądanie?
+ Nie (%1$d)
- %1$s sekunda
- %1$s sekundy
@@ -586,4 +589,4 @@
Długość pomijania do przodu
%1$dh %2$dm
%1$dm
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index a09403f8d9..45fb7ddd89 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -467,6 +467,9 @@
Не задано
Заставка
Использовать заставку приложения
+ Вы все еще смотрите?
+ Вы хотите продолжить просмотр?
+ Нет (%1$d)
- %1$s час
- %1$s часа
@@ -584,4 +587,4 @@
Очистить кэш изображений
Включить trickplay при просмотре видео
Длина перемотки вперед
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 84ab1f7be7..9922f3f7b4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -526,6 +526,9 @@
Unknown segments
Skip forward length
Enable trickplay in video player
+ Are you still watching?
+ Do you want to continue watching?
+ No (%1$d)
- %1$s second
- %1$s seconds