From d81e47204cbf0b08da91562e84603432000fdfbb Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 24 Jan 2025 19:55:03 -0700 Subject: [PATCH 1/6] Auto formatted code and added function comments --- .../app/managers/SleepTimerManager.kt | 285 ++++++++++++------ 1 file changed, 198 insertions(+), 87 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt index d53a548c..b2fb6b35 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt @@ -1,7 +1,6 @@ package com.audiobookshelf.app.managers import android.content.Context -import android.media.metrics.PlaybackSession import android.os.* import android.util.Log import com.audiobookshelf.app.device.DeviceManager @@ -12,44 +11,67 @@ import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt -class SleepTimerManager constructor(private val playerNotificationService: PlayerNotificationService) { +class SleepTimerManager +constructor(private val playerNotificationService: PlayerNotificationService) { private val tag = "SleepTimerManager" - private var sleepTimerTask:TimerTask? = null - private var sleepTimerRunning:Boolean = false - private var sleepTimerEndTime:Long = 0L - private var sleepTimerLength:Long = 0L - private var sleepTimerElapsed:Long = 0L - private var sleepTimerFinishedAt:Long = 0L - private var isAutoSleepTimer:Boolean = false // When timer was auto-set + private var sleepTimerTask: TimerTask? = null + private var sleepTimerRunning: Boolean = false + private var sleepTimerEndTime: Long = 0L + private var sleepTimerLength: Long = 0L + private var sleepTimerElapsed: Long = 0L + private var sleepTimerFinishedAt: Long = 0L + private var isAutoSleepTimer: Boolean = false // When timer was auto-set private var isFirstAutoSleepTimer: Boolean = true - private var sleepTimerSessionId:String = "" + private var sleepTimerSessionId: String = "" - private fun getCurrentTime():Long { + /** + * Gets the current time from the player notification service. + * @return Long - the current time in milliseconds. + */ + private fun getCurrentTime(): Long { return playerNotificationService.getCurrentTime() } - private fun getDuration():Long { + /** + * Gets the duration of the current playback. + * @return Long - the duration in milliseconds. + */ + private fun getDuration(): Long { return playerNotificationService.getDuration() } - private fun getIsPlaying():Boolean { + /** + * Checks if the player is currently playing. + * @return Boolean - true if the player is playing, false otherwise. + */ + private fun getIsPlaying(): Boolean { return playerNotificationService.currentPlayer.isPlaying } - private fun setVolume(volume:Float) { + /** + * Sets the volume of the player. + * @param volume Float - the volume level to set. + */ + private fun setVolume(volume: Float) { playerNotificationService.currentPlayer.volume = volume } + /** Pauses the player. */ private fun pause() { playerNotificationService.currentPlayer.pause() } + /** Plays the player. */ private fun play() { playerNotificationService.currentPlayer.play() } - private fun getSleepTimerTimeRemainingSeconds():Int { + /** + * Gets the remaining time of the sleep timer in seconds. + * @return Int - the remaining time in seconds. + */ + private fun getSleepTimerTimeRemainingSeconds(): Int { if (sleepTimerEndTime == 0L && sleepTimerLength > 0) { // For regular timer return ((sleepTimerLength - sleepTimerElapsed) / 1000).toDouble().roundToInt() } @@ -58,7 +80,13 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt() } - private fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean { + /** + * Sets the sleep timer. + * @param time Long - the time to set the sleep timer for. + * @param isChapterTime Boolean - true if the time is for the end of a chapter, false otherwise. + * @return Boolean - true if the sleep timer was set successfully, false otherwise. + */ + private fun setSleepTimer(time: Long, isChapterTime: Boolean): Boolean { Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime") sleepTimerTask?.cancel() sleepTimerRunning = true @@ -86,48 +114,75 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe sleepTimerEndTime = 0L } - playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer) - - sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) { - Handler(Looper.getMainLooper()).post { - if (getIsPlaying()) { - sleepTimerElapsed += 1000L - - val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds() - Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s") - - if (sleepTimeSecondsRemaining > 0) { - playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining, isAutoSleepTimer) - } - - if (sleepTimeSecondsRemaining <= 0) { - Log.d(tag, "Sleep Timer Pausing Player on Chapter") - pause() - - playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime()) - clearSleepTimer() - sleepTimerFinishedAt = System.currentTimeMillis() - } else if (sleepTimeSecondsRemaining <= 60 && DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut != true) { - // Start fading out audio down to 10% volume - val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F) - val volume = 1f - (percentToReduce * 0.9f) - Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining") - setVolume(volume) - } else { - setVolume(1f) - } - } - } - } + playerNotificationService.clientEventEmitter?.onSleepTimerSet( + getSleepTimerTimeRemainingSeconds(), + isAutoSleepTimer + ) + + sleepTimerTask = + Timer("SleepTimer", false).schedule(0L, 1000L) { + Handler(Looper.getMainLooper()).post { + if (getIsPlaying()) { + sleepTimerElapsed += 1000L + + val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds() + Log.d( + tag, + "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s" + ) + + if (sleepTimeSecondsRemaining > 0) { + playerNotificationService.clientEventEmitter?.onSleepTimerSet( + sleepTimeSecondsRemaining, + isAutoSleepTimer + ) + } + + if (sleepTimeSecondsRemaining <= 0) { + Log.d(tag, "Sleep Timer Pausing Player on Chapter") + pause() + + playerNotificationService.clientEventEmitter?.onSleepTimerEnded( + getCurrentTime() + ) + clearSleepTimer() + sleepTimerFinishedAt = System.currentTimeMillis() + } else if (sleepTimeSecondsRemaining <= 60 && + DeviceManager.deviceData + .deviceSettings + ?.disableSleepTimerFadeOut != true + ) { + // Start fading out audio down to 10% volume + val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F) + val volume = 1f - (percentToReduce * 0.9f) + Log.d( + tag, + "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining" + ) + setVolume(volume) + } else { + setVolume(1f) + } + } + } + } return true } - fun setManualSleepTimer(playbackSessionId:String, time: Long, isChapterTime:Boolean):Boolean { + /** + * Sets a manual sleep timer. + * @param playbackSessionId String - the playback session ID. + * @param time Long - the time to set the sleep timer for. + * @param isChapterTime Boolean - true if the time is for the end of a chapter, false otherwise. + * @return Boolean - true if the sleep timer was set successfully, false otherwise. + */ + fun setManualSleepTimer(playbackSessionId: String, time: Long, isChapterTime: Boolean): Boolean { sleepTimerSessionId = playbackSessionId isAutoSleepTimer = false return setSleepTimer(time, isChapterTime) } + /** Clears the sleep timer. */ private fun clearSleepTimer() { sleepTimerTask?.cancel() sleepTimerTask = null @@ -138,10 +193,15 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe setVolume(1f) } - fun getSleepTimerTime():Long { + /** + * Gets the sleep timer end time. + * @return Long - the sleep timer end time in milliseconds. + */ + fun getSleepTimerTime(): Long { return sleepTimerEndTime } + /** Cancels the sleep timer. */ fun cancelSleepTimer() { Log.d(tag, "Canceling Sleep Timer") @@ -155,15 +215,15 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false) } - // Vibrate when resetting sleep timer + /** Provides vibration feedback when resetting the sleep timer. */ private fun vibrateFeedback() { if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerResetFeedback == true) return val context = playerNotificationService.getContext() - val vibrator:Vibrator + val vibrator: Vibrator if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = - context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager vibrator = vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") @@ -172,18 +232,20 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe vibrator.let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150),-1) + val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150), -1) it.vibrate(vibrationEffect) } else { - @Suppress("DEPRECATION") - it.vibrate(10) + @Suppress("DEPRECATION") it.vibrate(10) } } } - // Get the chapter end time for use in End of Chapter timers - // if less than 2s remain in chapter then use the next chapter - private fun getChapterEndTime():Long? { + /** + * Gets the chapter end time for use in End of Chapter timers. If less than 2 seconds remain in + * the chapter, then use the next chapter. + * @return Long? - the chapter end time in milliseconds, or null if there is no current session. + */ + private fun getChapterEndTime(): Long? { val currentChapterEndTimeMs = playerNotificationService.getEndTimeOfChapterOrTrack() if (currentChapterEndTimeMs == null) { Log.e(tag, "Getting chapter sleep timer end of chapter/track but there is no current session") @@ -195,7 +257,10 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe Log.i(tag, "Getting chapter sleep timer time and current chapter has less than 2s remaining") val nextChapterEndTimeMs = playerNotificationService.getEndTimeOfNextChapterOrTrack() if (nextChapterEndTimeMs == null || currentChapterEndTimeMs == nextChapterEndTimeMs) { - Log.e(tag, "Invalid next chapter time. No current session or equal to current chapter. $nextChapterEndTimeMs") + Log.e( + tag, + "Invalid next chapter time. No current session or equal to current chapter. $nextChapterEndTimeMs" + ) null } else { nextChapterEndTimeMs @@ -205,6 +270,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe } } + /** Resets the chapter timer. */ private fun resetChapterTimer() { this.getChapterEndTime()?.let { chapterEndTime -> Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime") @@ -214,6 +280,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe } } + /** Checks if the sleep timer should be reset. */ private fun checkShouldResetSleepTimer() { if (!sleepTimerRunning) { if (sleepTimerFinishedAt <= 0L) return @@ -230,7 +297,10 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe if (isAutoSleepTimer) { DeviceManager.deviceData.deviceSettings?.let { deviceSettings -> if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) { - Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms") + Log.i( + tag, + "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms" + ) playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime) } isFirstAutoSleepTimer = false @@ -260,7 +330,10 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe // When navigating to previous chapters make sure this is still the end of the current chapter this.getChapterEndTime()?.let { chapterEndTime -> if (chapterEndTime != sleepTimerEndTime) { - Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime") + Log.d( + tag, + "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime" + ) vibrateFeedback() setSleepTimer(chapterEndTime, true) play() @@ -269,6 +342,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe } } + /** Handles the shake event to reset the sleep timer. */ fun handleShake() { if (sleepTimerRunning || sleepTimerFinishedAt > 0L) { if (DeviceManager.deviceData.deviceSettings?.disableShakeToResetSleepTimer == true) { @@ -279,48 +353,65 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe } } + /** + * Increases the sleep timer time. + * @param time Long - the time to increase the sleep timer by. + */ fun increaseSleepTime(time: Long) { Log.d(tag, "Increase Sleep time $time") if (!sleepTimerRunning) return if (sleepTimerEndTime == 0L) { sleepTimerLength += time - if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime() + if (sleepTimerLength + getCurrentTime() > getDuration()) + sleepTimerLength = getDuration() - getCurrentTime() } else { val newSleepEndTime = sleepTimerEndTime + time - sleepTimerEndTime = if (newSleepEndTime >= getDuration()) { - getDuration() - } else { - newSleepEndTime - } + sleepTimerEndTime = + if (newSleepEndTime >= getDuration()) { + getDuration() + } else { + newSleepEndTime + } } setVolume(1F) - playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer) + playerNotificationService.clientEventEmitter?.onSleepTimerSet( + getSleepTimerTimeRemainingSeconds(), + isAutoSleepTimer + ) } + /** + * Decreases the sleep timer time. + * @param time Long - the time to decrease the sleep timer by. + */ fun decreaseSleepTime(time: Long) { Log.d(tag, "Decrease Sleep time $time") if (!sleepTimerRunning) return - if (sleepTimerEndTime == 0L) { sleepTimerLength -= time if (sleepTimerLength <= 0) sleepTimerLength = 1000L } else { val newSleepEndTime = sleepTimerEndTime - time - sleepTimerEndTime = if (newSleepEndTime <= 1000) { - // End sleep timer in 1 second - getCurrentTime() + 1000 - } else { - newSleepEndTime - } + sleepTimerEndTime = + if (newSleepEndTime <= 1000) { + // End sleep timer in 1 second + getCurrentTime() + 1000 + } else { + newSleepEndTime + } } setVolume(1F) - playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer) + playerNotificationService.clientEventEmitter?.onSleepTimerSet( + getSleepTimerTimeRemainingSeconds(), + isAutoSleepTimer + ) } + /** Checks if the auto sleep timer should be set. */ fun checkAutoSleepTimer() { if (sleepTimerRunning) { // Sleep timer already running return @@ -337,10 +428,13 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe val currentCalendar = Calendar.getInstance() - // In cases where end time is before start time then we shift the time window forward or backward based on the current time. + // In cases where end time is before start time then we shift the time window forward or + // backward based on the current time. // e.g. start time 22:00 and end time 06:00. - // If current time is less than start time (e.g. 00:30) then start time will be the previous day. - // If current time is greater than start time (e.g. 23:00) then end time will be the next day. + // If current time is less than start time (e.g. 00:30) then start time will be the + // previous day. + // If current time is greater than start time (e.g. 23:00) then end time will be the + // next day. if (endCalendar.before(startCalendar)) { if (currentCalendar.before(startCalendar)) { // Shift start back a day startCalendar.add(Calendar.DAY_OF_MONTH, -1) @@ -351,11 +445,17 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time) if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) { - Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer") + Log.i( + tag, + "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer" + ) // Automatically Rewind in the book if settings is enabled if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) { - Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms") + Log.i( + tag, + "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms" + ) playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime) } isFirstAutoSleepTimer = false @@ -365,7 +465,10 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe if (deviceSettings.sleepTimerLength == 0L) { val chapterEndTimeMs = this.getChapterEndTime() if (chapterEndTimeMs == null) { - Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session") + Log.e( + tag, + "Setting auto sleep timer to end of chapter/track but there is no current session" + ) } else { isAutoSleepTimer = true setSleepTimer(chapterEndTimeMs, true) @@ -376,15 +479,23 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe } } else { isFirstAutoSleepTimer = true - Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}") + Log.d( + tag, + "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}" + ) } } } - fun handleMediaPlayEvent(playbackSessionId:String) { + /** + * Handles the media play event and checks if the sleep timer should be reset or set. + * @param playbackSessionId String - the playback session ID. + */ + fun handleMediaPlayEvent(playbackSessionId: String) { // Check if the playback session has changed // If it hasn't changed OR the sleep timer is running then check reset the timer - // e.g. You set a manual sleep timer for 10 mins, then decide to change books, the sleep timer will stay on and reset to 10 mins + // e.g. You set a manual sleep timer for 10 mins, then decide to change books, the sleep timer + // will stay on and reset to 10 mins if (sleepTimerSessionId == playbackSessionId || sleepTimerRunning) { checkShouldResetSleepTimer() } else { From 16472e1de885142e5a1a8aab845db77d18f619d5 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 24 Jan 2025 20:05:43 -0700 Subject: [PATCH 2/6] Simplify increase/decrease sleep timer --- .../app/managers/SleepTimerManager.kt | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt index b2fb6b35..50f2d876 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt @@ -361,18 +361,13 @@ constructor(private val playerNotificationService: PlayerNotificationService) { Log.d(tag, "Increase Sleep time $time") if (!sleepTimerRunning) return + // Increase the sleep timer time (if using fixed length) or end time (if using chapter end time) + // and ensure it doesn't go over the duration of the current playback item if (sleepTimerEndTime == 0L) { sleepTimerLength += time - if (sleepTimerLength + getCurrentTime() > getDuration()) - sleepTimerLength = getDuration() - getCurrentTime() + sleepTimerLength = minOf(sleepTimerLength, getDuration() - getCurrentTime()) } else { - val newSleepEndTime = sleepTimerEndTime + time - sleepTimerEndTime = - if (newSleepEndTime >= getDuration()) { - getDuration() - } else { - newSleepEndTime - } + sleepTimerEndTime = minOf(sleepTimerEndTime + time, getDuration()) } setVolume(1F) @@ -390,18 +385,12 @@ constructor(private val playerNotificationService: PlayerNotificationService) { Log.d(tag, "Decrease Sleep time $time") if (!sleepTimerRunning) return + // Decrease the sleep timer time (if using fixed length) or end time (if using chapter end time) + // and ensure it doesn't go below 1 second if (sleepTimerEndTime == 0L) { - sleepTimerLength -= time - if (sleepTimerLength <= 0) sleepTimerLength = 1000L + sleepTimerLength = maxOf(sleepTimerLength - time, 1000L) } else { - val newSleepEndTime = sleepTimerEndTime - time - sleepTimerEndTime = - if (newSleepEndTime <= 1000) { - // End sleep timer in 1 second - getCurrentTime() + 1000 - } else { - newSleepEndTime - } + sleepTimerEndTime = maxOf(sleepTimerEndTime - time, getCurrentTime() + 1000) } setVolume(1F) From 161614f6c99e26ea7138d196dcc80274b1113bf6 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 24 Jan 2025 22:18:03 -0700 Subject: [PATCH 3/6] Fix: scale end of chapter time by playback speed --- .../app/managers/SleepTimerManager.kt | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt index 50f2d876..bb5e3667 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt @@ -49,6 +49,14 @@ constructor(private val playerNotificationService: PlayerNotificationService) { return playerNotificationService.currentPlayer.isPlaying } + /** + * Gets the playback speed of the player. + * @return Float - the playback speed. + */ + private fun getPlaybackSpeed(): Float { + return playerNotificationService.currentPlayer.playbackParameters.speed + } + /** * Sets the volume of the player. * @param volume Float - the volume level to set. @@ -69,15 +77,16 @@ constructor(private val playerNotificationService: PlayerNotificationService) { /** * Gets the remaining time of the sleep timer in seconds. + * @param speed Float - the playback speed of the player, default value is 1. * @return Int - the remaining time in seconds. */ - private fun getSleepTimerTimeRemainingSeconds(): Int { + private fun getSleepTimerTimeRemainingSeconds(speed: Float = 1f): Int { if (sleepTimerEndTime == 0L && sleepTimerLength > 0) { // For regular timer return ((sleepTimerLength - sleepTimerElapsed) / 1000).toDouble().roundToInt() } // For chapter end timer if (sleepTimerEndTime <= 0) return 0 - return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt() + return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble() / speed).roundToInt() } /** @@ -115,7 +124,7 @@ constructor(private val playerNotificationService: PlayerNotificationService) { } playerNotificationService.clientEventEmitter?.onSleepTimerSet( - getSleepTimerTimeRemainingSeconds(), + getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()), isAutoSleepTimer ) @@ -125,7 +134,8 @@ constructor(private val playerNotificationService: PlayerNotificationService) { if (getIsPlaying()) { sleepTimerElapsed += 1000L - val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds() + val sleepTimeSecondsRemaining = + getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()) Log.d( tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s" @@ -364,15 +374,18 @@ constructor(private val playerNotificationService: PlayerNotificationService) { // Increase the sleep timer time (if using fixed length) or end time (if using chapter end time) // and ensure it doesn't go over the duration of the current playback item if (sleepTimerEndTime == 0L) { + // Fixed length sleepTimerLength += time sleepTimerLength = minOf(sleepTimerLength, getDuration() - getCurrentTime()) } else { - sleepTimerEndTime = minOf(sleepTimerEndTime + time, getDuration()) + // Chapter end time + sleepTimerEndTime = + minOf(sleepTimerEndTime + (time * getPlaybackSpeed()).roundToInt(), getDuration()) } setVolume(1F) playerNotificationService.clientEventEmitter?.onSleepTimerSet( - getSleepTimerTimeRemainingSeconds(), + getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()), isAutoSleepTimer ) } @@ -388,14 +401,20 @@ constructor(private val playerNotificationService: PlayerNotificationService) { // Decrease the sleep timer time (if using fixed length) or end time (if using chapter end time) // and ensure it doesn't go below 1 second if (sleepTimerEndTime == 0L) { + // Fixed length sleepTimerLength = maxOf(sleepTimerLength - time, 1000L) } else { - sleepTimerEndTime = maxOf(sleepTimerEndTime - time, getCurrentTime() + 1000) + // Chapter end time + sleepTimerEndTime = + maxOf( + sleepTimerEndTime - (time * getPlaybackSpeed()).roundToInt(), + getCurrentTime() + 1000 + ) } setVolume(1F) playerNotificationService.clientEventEmitter?.onSleepTimerSet( - getSleepTimerTimeRemainingSeconds(), + getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()), isAutoSleepTimer ) } From 13b020732f71af6e9198d1fe91455a5c155d1cee Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 25 Jan 2025 19:00:43 -0700 Subject: [PATCH 4/6] General cleanup, only disable auto-sleep temporarily Auto sleep timer is only disabled until the end of the current time period (e.g. when the sleep timer would be disabled automatically). --- .../app/managers/SleepTimerManager.kt | 199 ++++++++---------- components/modals/SleepTimerModal.vue | 21 +- strings/en-us.json | 2 + 3 files changed, 107 insertions(+), 115 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt index bb5e3667..43ab6980 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt @@ -6,7 +6,6 @@ import android.util.Log import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.SLEEP_TIMER_WAKE_UP_EXPIRATION -import java.text.SimpleDateFormat import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt @@ -22,7 +21,7 @@ constructor(private val playerNotificationService: PlayerNotificationService) { private var sleepTimerElapsed: Long = 0L private var sleepTimerFinishedAt: Long = 0L private var isAutoSleepTimer: Boolean = false // When timer was auto-set - private var isFirstAutoSleepTimer: Boolean = true + private var autoTimerDisabled: Boolean = false // Disable until out of auto timer period private var sleepTimerSessionId: String = "" /** @@ -91,38 +90,47 @@ constructor(private val playerNotificationService: PlayerNotificationService) { /** * Sets the sleep timer. - * @param time Long - the time to set the sleep timer for. - * @param isChapterTime Boolean - true if the time is for the end of a chapter, false otherwise. + * @param time Long - the time to set the sleep timer for. When 0L, use end of chapter/track time. * @return Boolean - true if the sleep timer was set successfully, false otherwise. */ - private fun setSleepTimer(time: Long, isChapterTime: Boolean): Boolean { - Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime") + private fun setSleepTimer(time: Long): Boolean { + Log.d(tag, "Setting Sleep Timer for $time") sleepTimerTask?.cancel() sleepTimerRunning = true sleepTimerFinishedAt = 0L sleepTimerElapsed = 0L setVolume(1f) - // Register shake sensor - playerNotificationService.registerSensor() + if (time == 0L) { + // Get the current chapter time and set the sleep timer to the end of the chapter + val chapterEndTime = this.getChapterEndTime() - val currentTime = getCurrentTime() - if (isChapterTime) { - if (currentTime > time) { - Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time") + if (chapterEndTime == null) { + Log.e(tag, "Setting sleep timer to end of chapter/track but there is no current session") + return false + } + + val currentTime = getCurrentTime() + if (currentTime > chapterEndTime) { + Log.d(tag, "Invalid sleep timer - time is already past chapter time $chapterEndTime") return false } - sleepTimerEndTime = time - sleepTimerLength = 0 + + sleepTimerEndTime = chapterEndTime if (sleepTimerEndTime > getDuration()) { sleepTimerEndTime = getDuration() } } else { - sleepTimerLength = time sleepTimerEndTime = 0L } + // Set sleep timer length. Will be 0L if using chapter end time + sleepTimerLength = time + + // Register shake sensor + playerNotificationService.registerSensor() + playerNotificationService.clientEventEmitter?.onSleepTimerSet( getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()), isAutoSleepTimer @@ -189,7 +197,13 @@ constructor(private val playerNotificationService: PlayerNotificationService) { fun setManualSleepTimer(playbackSessionId: String, time: Long, isChapterTime: Boolean): Boolean { sleepTimerSessionId = playbackSessionId isAutoSleepTimer = false - return setSleepTimer(time, isChapterTime) + if (isChapterTime) { + Log.d(tag, "Setting manual sleep timer for end of chapter") + return setSleepTimer(0L) + } else { + Log.d(tag, "Setting manual sleep timer for $time") + return setSleepTimer(time) + } } /** Clears the sleep timer. */ @@ -216,9 +230,8 @@ constructor(private val playerNotificationService: PlayerNotificationService) { Log.d(tag, "Canceling Sleep Timer") if (isAutoSleepTimer) { - Log.i(tag, "Disabling auto sleep timer") - DeviceManager.deviceData.deviceSettings?.autoSleepTimer = false - DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData) + Log.i(tag, "Disabling auto sleep timer for this time period") + autoTimerDisabled = true } clearSleepTimer() @@ -280,19 +293,35 @@ constructor(private val playerNotificationService: PlayerNotificationService) { } } - /** Resets the chapter timer. */ - private fun resetChapterTimer() { - this.getChapterEndTime()?.let { chapterEndTime -> - Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime") - vibrateFeedback() - setSleepTimer(chapterEndTime, true) - play() + /** + * Rewind auto sleep timer if setting enabled. To ensure the first rewind of the time period does + * not take place, make sure to set `isAutoSleepTimer` after calling this function. + */ + private fun tryRewindAutoSleepTimer() { + DeviceManager.deviceData.deviceSettings?.let { deviceSettings -> + if (isAutoSleepTimer && deviceSettings.autoSleepTimerAutoRewind) { + Log.i( + tag, + "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms" + ) + playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime) + } } } /** Checks if the sleep timer should be reset. */ private fun checkShouldResetSleepTimer() { - if (!sleepTimerRunning) { + if (sleepTimerRunning) { + // Reset the sleep timer if it has been running for at least 3 seconds or it is an end of + // chapter/track timer + if (sleepTimerLength == 0L || sleepTimerElapsed > 3000L) { + Log.d(tag, "Resetting running sleep timer") + vibrateFeedback() + setSleepTimer(sleepTimerLength) + play() + } + } else { + if (sleepTimerFinishedAt <= 0L) return val finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt @@ -303,52 +332,14 @@ constructor(private val playerNotificationService: PlayerNotificationService) { return } - // Automatically Rewind in the book if settings is enabled - if (isAutoSleepTimer) { - DeviceManager.deviceData.deviceSettings?.let { deviceSettings -> - if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) { - Log.i( - tag, - "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms" - ) - playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime) - } - isFirstAutoSleepTimer = false - } - } + // Automatically rewind in the book if settings are enabled + tryRewindAutoSleepTimer() // Set sleep timer - // When sleepTimerLength is 0 then use end of chapter/track time - if (sleepTimerLength == 0L) { - Log.d(tag, "Resetting stopped chapter sleep timer") - resetChapterTimer() - } else { - Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength") - vibrateFeedback() - setSleepTimer(sleepTimerLength, false) - play() - } - return - } - - // Does not apply to chapter sleep timers and timer must be running for at least 3 seconds - if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) { - Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength") + Log.d(tag, "Resetting stopped sleep timer") vibrateFeedback() - setSleepTimer(sleepTimerLength, false) - } else if (sleepTimerLength == 0L) { - // When navigating to previous chapters make sure this is still the end of the current chapter - this.getChapterEndTime()?.let { chapterEndTime -> - if (chapterEndTime != sleepTimerEndTime) { - Log.d( - tag, - "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime" - ) - vibrateFeedback() - setSleepTimer(chapterEndTime, true) - play() - } - } + setSleepTimer(sleepTimerLength) + play() } } @@ -419,7 +410,7 @@ constructor(private val playerNotificationService: PlayerNotificationService) { ) } - /** Checks if the auto sleep timer should be set. */ + /** Checks whether the auto sleep timer should be set, and set up auto sleep timer if so. */ fun checkAutoSleepTimer() { if (sleepTimerRunning) { // Sleep timer already running return @@ -451,46 +442,36 @@ constructor(private val playerNotificationService: PlayerNotificationService) { } } - val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time) - if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) { - Log.i( - tag, - "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer" - ) + val isDuringAutoTime = + currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar) - // Automatically Rewind in the book if settings is enabled - if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) { - Log.i( - tag, - "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms" - ) - playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime) - } - isFirstAutoSleepTimer = false - - // Set sleep timer - // When sleepTimerLength is 0 then use end of chapter/track time - if (deviceSettings.sleepTimerLength == 0L) { - val chapterEndTimeMs = this.getChapterEndTime() - if (chapterEndTimeMs == null) { - Log.e( - tag, - "Setting auto sleep timer to end of chapter/track but there is no current session" - ) - } else { - isAutoSleepTimer = true - setSleepTimer(chapterEndTimeMs, true) - } + // Determine whether to set the auto sleep timer or not + if (autoTimerDisabled) { + if (!isDuringAutoTime) { + // Check if sleep timer was disabled during the previous period and enable again + Log.i(tag, "Leaving disabled auto sleep time period, enabling for next time period") + autoTimerDisabled = false } else { - isAutoSleepTimer = true - setSleepTimer(deviceSettings.sleepTimerLength, false) + // Auto time is disabled, do not set sleep timer + Log.i(tag, "Auto sleep timer is disabled for this time period") } } else { - isFirstAutoSleepTimer = true - Log.d( - tag, - "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}" - ) + if (isDuringAutoTime) { + // Start an auto sleep timer + val currentHour = currentCalendar.get(Calendar.HOUR_OF_DAY) + val currentMin = currentCalendar.get(Calendar.MINUTE) + Log.i(tag, "Starting sleep timer at $currentHour:$currentMin") + + // Automatically rewind in the book if settings is enabled + tryRewindAutoSleepTimer() + + // Set `isAutoSleepTimer` to true to indicate that the timer was set automatically + // and to not cause the timer to rewind + isAutoSleepTimer = true + setSleepTimer(deviceSettings.sleepTimerLength) + } else { + Log.d(tag, "Not in auto sleep time period") + } } } } @@ -506,9 +487,7 @@ constructor(private val playerNotificationService: PlayerNotificationService) { // will stay on and reset to 10 mins if (sleepTimerSessionId == playbackSessionId || sleepTimerRunning) { checkShouldResetSleepTimer() - } else { - isFirstAutoSleepTimer = true - } + } else {} sleepTimerSessionId = playbackSessionId checkAutoSleepTimer() diff --git a/components/modals/SleepTimerModal.vue b/components/modals/SleepTimerModal.vue index 79f03f89..b210fddc 100644 --- a/components/modals/SleepTimerModal.vue +++ b/components/modals/SleepTimerModal.vue @@ -86,6 +86,9 @@ export default { }, timeRemainingPretty() { return this.$secondsToTimestamp(this.currentTime) + }, + isIos() { + return this.$platform === 'ios' } }, methods: { @@ -103,11 +106,19 @@ export default { }, async cancelSleepTimer() { if (this.isAuto) { - const { value } = await Dialog.confirm({ - title: 'Confirm', - message: 'Are you sure you want to disable the auto sleep timer? You will need to enable this again in settings.' - }) - if (!value) return + if (this.$platform === 'ios') { + const { value } = await Dialog.confirm({ + title: 'Confirm', + message: this.$strings.MessageConfirmDisableAutoTimerIos + }) + if (!value) return + } else { + const { value } = await Dialog.confirm({ + title: 'Confirm', + message: this.$strings.MessageConfirmDisableAutoTimerAndroid + }) + if (!value) return + } } await this.$hapticsImpact() diff --git a/strings/en-us.json b/strings/en-us.json index 8b9b00ef..d2e09640 100644 --- a/strings/en-us.json +++ b/strings/en-us.json @@ -278,6 +278,8 @@ "MessageBookshelfEmpty": "Bookshelf empty", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", + "MessageConfirmDisableAutoTimerAndroid": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.", + "MessageConfirmDisableAutoTimerIos": "Are you sure you want to disable the auto sleep timer? You will need to enable this again in settings.", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", From b6ab7dc8a72e44dd9d6537e87cb365c102065c8c Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Jan 2025 12:36:11 -0600 Subject: [PATCH 5/6] Update sleep timer modal to not show negative time remaining --- components/modals/SleepTimerModal.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/components/modals/SleepTimerModal.vue b/components/modals/SleepTimerModal.vue index b210fddc..bdae5a32 100644 --- a/components/modals/SleepTimerModal.vue +++ b/components/modals/SleepTimerModal.vue @@ -85,6 +85,7 @@ export default { return [5, 10, 15, 30, 45, 60, 90] }, timeRemainingPretty() { + if (this.currentTime <= 0) return '0:00' return this.$secondsToTimestamp(this.currentTime) }, isIos() { From 0509d7105e6b006d4f1402696a36b3a1376eda0f Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Jan 2025 12:40:39 -0600 Subject: [PATCH 6/6] Update confirm disable auto timer message --- components/modals/SleepTimerModal.vue | 18 +++++------------- strings/en-us.json | 3 +-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/components/modals/SleepTimerModal.vue b/components/modals/SleepTimerModal.vue index bdae5a32..dbf2c43d 100644 --- a/components/modals/SleepTimerModal.vue +++ b/components/modals/SleepTimerModal.vue @@ -107,19 +107,11 @@ export default { }, async cancelSleepTimer() { if (this.isAuto) { - if (this.$platform === 'ios') { - const { value } = await Dialog.confirm({ - title: 'Confirm', - message: this.$strings.MessageConfirmDisableAutoTimerIos - }) - if (!value) return - } else { - const { value } = await Dialog.confirm({ - title: 'Confirm', - message: this.$strings.MessageConfirmDisableAutoTimerAndroid - }) - if (!value) return - } + const { value } = await Dialog.confirm({ + title: 'Confirm', + message: this.$strings.MessageConfirmDisableAutoTimer + }) + if (!value) return } await this.$hapticsImpact() diff --git a/strings/en-us.json b/strings/en-us.json index d2e09640..b00c90cf 100644 --- a/strings/en-us.json +++ b/strings/en-us.json @@ -278,8 +278,7 @@ "MessageBookshelfEmpty": "Bookshelf empty", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", - "MessageConfirmDisableAutoTimerAndroid": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.", - "MessageConfirmDisableAutoTimerIos": "Are you sure you want to disable the auto sleep timer? You will need to enable this again in settings.", + "MessageConfirmDisableAutoTimer": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",