diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index 2d4e065a..ffd4a9ac 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -98,7 +98,11 @@ data class DeviceSettings( var disableShakeToResetSleepTimer:Boolean, var shakeSensitivity: ShakeSensitivitySetting, var lockOrientation: LockOrientationSetting, - var hapticFeedback: HapticFeedbackSetting + var hapticFeedback: HapticFeedbackSetting, + var autoSleepTimer: Boolean, + var autoSleepTimerStartTime: String, + var autoSleepTimerEndTime: String, + var sleepTimerLength: Long // Time in milliseconds ) { companion object { // Static method to get default device settings @@ -111,7 +115,11 @@ data class DeviceSettings( disableShakeToResetSleepTimer = false, shakeSensitivity = ShakeSensitivitySetting.MEDIUM, lockOrientation = LockOrientationSetting.NONE, - hapticFeedback = HapticFeedbackSetting.LIGHT + hapticFeedback = HapticFeedbackSetting.LIGHT, + autoSleepTimer = false, + autoSleepTimerStartTime = "22:00", + autoSleepTimerEndTime = "06:00", + sleepTimerLength = 900000L // 15 minutes ) } } @@ -120,6 +128,14 @@ data class DeviceSettings( val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L @get:JsonIgnore val jumpForwardTimeMs get() = jumpForwardTime * 1000L + @get:JsonIgnore + val autoSleepTimerStartHour get() = autoSleepTimerStartTime.split(":")[0].toInt() + @get:JsonIgnore + val autoSleepTimerStartMinute get() = autoSleepTimerStartTime.split(":")[1].toInt() + @get:JsonIgnore + val autoSleepTimerEndHour get() = autoSleepTimerEndTime.split(":")[0].toInt() + @get:JsonIgnore + val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt() @JsonIgnore fun getShakeThresholdGravity() : Float { // Used in ShakeDetector diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt index 28e1d21c..5ffe332f 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -14,6 +14,7 @@ import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.audiobookshelf.app.player.PLAYMETHOD_LOCAL import java.util.* @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index 3722a0ec..7e6b3a82 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -84,6 +84,12 @@ class PlaybackSession( return chapters.find { time >= it.startMs && it.endMs > time} } + @JsonIgnore + fun getCurrentTrackEndTime():Long? { + val currentTrack = audioTracks[this.getCurrentTrackIndex()] + return currentTrack.startOffsetMs + currentTrack.durationMs + } + @JsonIgnore fun getCurrentTrackTimeMs():Long { val currentTrack = audioTracks[this.getCurrentTrackIndex()] diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt index 94f3756a..0948d02b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -26,6 +26,16 @@ object DeviceManager { init { Log.d(tag, "Device Manager Singleton invoked") + + // Initialize new sleep timer settings and shake sensitivity added in v0.9.61 + if (deviceData.deviceSettings?.autoSleepTimerStartTime == null || deviceData.deviceSettings?.autoSleepTimerEndTime == null) { + deviceData.deviceSettings?.autoSleepTimerStartTime = "22:00" + deviceData.deviceSettings?.autoSleepTimerStartTime = "06:00" + deviceData.deviceSettings?.sleepTimerLength = 900000L + } + if (deviceData.deviceSettings?.shakeSensitivity == null) { + deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM + } } fun getBase64Id(id:String):String { 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 fb5da1b4..6b8cd783 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,12 +6,11 @@ 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 -const val SLEEP_EXTENSION_TIME = 900000L // 15m - class SleepTimerManager constructor(private val playerNotificationService: PlayerNotificationService) { private val tag = "SleepTimerManager" @@ -58,7 +57,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean { Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime") sleepTimerTask?.cancel() - sleepTimerRunning = false + sleepTimerRunning = true sleepTimerFinishedAt = 0L sleepTimerElapsed = 0L @@ -88,7 +87,6 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds()) - sleepTimerRunning = true sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) { Handler(Looper.getMainLooper()).post { if (getIsPlaying()) { @@ -120,12 +118,6 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe return true } - // Called when playing audio and only applies to regular timer - fun resetSleepTimer() { - if (!sleepTimerRunning || sleepTimerLength <= 0L) return - setSleepTimer(sleepTimerLength, false) - } - private fun clearSleepTimer() { sleepTimerTask?.cancel() sleepTimerTask = null @@ -168,7 +160,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe } } - fun checkShouldExtendSleepTimer() { + fun checkShouldResetSleepTimer() { if (!sleepTimerRunning) { if (sleepTimerFinishedAt <= 0L) return @@ -180,15 +172,27 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe return } - val newSleepTime = if (sleepTimerLength >= 0) sleepTimerLength else SLEEP_EXTENSION_TIME - vibrate() - setSleepTimer(newSleepTime, false) - play() + // Set sleep timer + // When sleepTimerLength is 0 then use end of chapter/track time + if (sleepTimerLength == 0L) { + val currentChapterEndTimeMs = playerNotificationService.getEndTimeOfChapterOrTrack() + if (currentChapterEndTimeMs == null) { + Log.e(tag, "Checking reset sleep timer to end of chapter/track but there is no current session") + } else { + vibrate() + setSleepTimer(currentChapterEndTimeMs, true) + play() + } + } else { + vibrate() + setSleepTimer(sleepTimerLength, false) + play() + } return } // Does not apply to chapter sleep timers and timer must be running for at least 3 seconds - if (sleepTimerEndTime == 0L && sleepTimerElapsed > 3000L) { + if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) { vibrate() setSleepTimer(sleepTimerLength, false) } @@ -200,7 +204,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe Log.d(tag, "Shake to reset sleep timer is disabled") return } - checkShouldExtendSleepTimer() + checkShouldResetSleepTimer() } } @@ -245,4 +249,53 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe setVolume(1F) playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds()) } + + fun checkAutoSleepTimer() { + if (sleepTimerRunning) { // Sleep timer already running + return + } + DeviceManager.deviceData.deviceSettings?.let { deviceSettings -> + if (!deviceSettings.autoSleepTimer) return // Check auto sleep timer is enabled + + val startCalendar = Calendar.getInstance() + startCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerStartHour) + startCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerStartMinute) + val endCalendar = Calendar.getInstance() + endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour) + endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute) + + // In cases where end time is earlier then start time then we add a day to end time + // e.g. start time 22:00 and end time 06:00. End time will be treated as 6am the next day. + // e.g. start time 08:00 and end time 22:00. Start and end time will be the same day. + if (endCalendar.before(startCalendar)) { + endCalendar.add(Calendar.DAY_OF_MONTH, 1) + } + + val currentCalendar = Calendar.getInstance() + 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") + + // Set sleep timer + // When sleepTimerLength is 0 then use end of chapter/track time + if (deviceSettings.sleepTimerLength == 0L) { + val currentChapterEndTimeMs = playerNotificationService.getEndTimeOfChapterOrTrack() + if (currentChapterEndTimeMs == null) { + Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session") + } else { + setSleepTimer(currentChapterEndTimeMs, true) + } + } else { + setSleepTimer(deviceSettings.sleepTimerLength, false) + } + } else { + Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}") + } + } + } + + fun handleMediaPlayEvent() { + checkShouldResetSleepTimer() + checkAutoSleepTimer() + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt index aaa7c09c..98be44df 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt @@ -68,6 +68,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) { Handler(Looper.getMainLooper()).post() { if (playerNotificationService.currentPlayer.isPlaying) { + // Set auto sleep timer if enabled and within start/end time + playerNotificationService.sleepTimerManager.checkAutoSleepTimer() + // Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index 7fa00026..74f2e1ed 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -169,7 +169,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi Log.d(tag, "handleCallMediaButton: Media Play") if (0 == mediaButtonClickCount) { playerNotificationService.play() - playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer() } handleMediaButtonClickCount() } @@ -201,7 +200,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi } else { if (0 == mediaButtonClickCount) { playerNotificationService.play() - playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer() } handleMediaButtonClickCount() } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt index 6504b060..d7d42f7a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt @@ -92,7 +92,9 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : // Start/stop progress sync interval if (isPlaying) { - playerNotificationService.sleepTimerManager.resetSleepTimer() // Reset sleep timer if running and not a chapter timer + // Handles auto-starting sleep timer and resetting sleep timer + playerNotificationService.sleepTimerManager.handleMediaPlayEvent() + player.volume = 1F // Volume on sleep timer might have decreased this val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 9b3672af..a0ee353c 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -644,6 +644,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return currentPlaybackSession?.getChapterForTime(this.getCurrentTime()) } + fun getEndTimeOfChapterOrTrack():Long? { + return getCurrentBookChapter()?.endMs ?: currentPlaybackSession?.getCurrentTrackEndTime() + } + // Called from PlayerListener play event // check with server if progress has updated since last play and sync progress update fun checkCurrentSessionProgress(seekBackTime:Long):Boolean { diff --git a/components/modals/SleepTimerLengthModal.vue b/components/modals/SleepTimerLengthModal.vue new file mode 100644 index 00000000..9785e10e --- /dev/null +++ b/components/modals/SleepTimerLengthModal.vue @@ -0,0 +1,100 @@ + + + diff --git a/components/modals/SleepTimerModal.vue b/components/modals/SleepTimerModal.vue index 21cb55d5..99bd2b5e 100644 --- a/components/modals/SleepTimerModal.vue +++ b/components/modals/SleepTimerModal.vue @@ -40,7 +40,7 @@
  • - Manual sleep timer + Custom time
  • @@ -96,7 +96,7 @@ export default { }, async clickedOption(timeoutMin) { await this.$hapticsImpact() - var timeout = timeoutMin * 1000 * 60 + const timeout = timeoutMin * 1000 * 60 this.show = false this.manualTimerModal = false this.$nextTick(() => this.$emit('change', { time: timeout, isChapterTime: false })) diff --git a/components/ui/TextInput.vue b/components/ui/TextInput.vue index 7402259e..2d335ad1 100644 --- a/components/ui/TextInput.vue +++ b/components/ui/TextInput.vue @@ -1,12 +1,15 @@ @@ -17,6 +20,7 @@ export default { placeholder: String, type: String, disabled: Boolean, + readonly: Boolean, borderless: Boolean, bg: { type: String, @@ -30,6 +34,10 @@ export default { type: String, default: null }, + appendIcon: { + type: String, + default: null + }, clearable: Boolean }, data() { @@ -74,4 +82,10 @@ export default { }, mounted() {} } - \ No newline at end of file + + + \ No newline at end of file diff --git a/pages/settings.vue b/pages/settings.vue index 47d4df70..fecf87bb 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -1,5 +1,5 @@ @@ -63,6 +94,9 @@ export default { data() { return { deviceData: null, + showMoreMenuDialog: false, + showSleepTimerLengthModal: false, + moreMenuSetting: '', settings: { disableAutoRewind: false, enableAltView: false, @@ -71,15 +105,23 @@ export default { disableShakeToResetSleepTimer: false, shakeSensitivity: 'MEDIUM', lockOrientation: 0, - hapticFeedback: 'LIGHT' + hapticFeedback: 'LIGHT', + autoSleepTimer: false, + autoSleepTimerStartTime: '22:00', + autoSleepTimerEndTime: '06:00', + sleepTimerLength: 900000 // 15 minutes }, + lockCurrentOrientation: false, settingInfo: { disableShakeToResetSleepTimer: { name: 'Disable shake to reset sleep timer', message: 'Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.' + }, + autoSleepTimer: { + name: 'Auto Sleep Timer', + message: 'When playing media between the specified start and end times a sleep timer will automatically start.' } }, - lockCurrentOrientation: false, hapticFeedbackItems: [ { text: 'Off', @@ -145,10 +187,55 @@ export default { currentJumpBackwardsTimeIndex() { var index = this.jumpBackwardsItems.findIndex((jfi) => jfi.value === this.settings.jumpBackwardsTime) return index >= 0 ? index : 1 + }, + shakeSensitivityOption() { + const item = this.shakeSensitivityItems.find((i) => i.value === this.settings.shakeSensitivity) + return item ? item.text : 'Error' + }, + hapticFeedbackOption() { + const item = this.hapticFeedbackItems.find((i) => i.value === this.settings.hapticFeedback) + return item ? item.text : 'Error' + }, + sleepTimerLengthOption() { + if (!this.settings.sleepTimerLength) return 'End of Chapter' + const minutes = Number(this.settings.sleepTimerLength) / 1000 / 60 + return `${minutes} min` + }, + moreMenuItems() { + if (this.moreMenuSetting === 'shakeSensitivity') return this.shakeSensitivityItems + else if (this.moreMenuSetting === 'hapticFeedback') return this.hapticFeedbackItems + return [] } }, methods: { - sensitivityUpdated(val) { + sleepTimerLengthModalSelection(value) { + this.settings.sleepTimerLength = value + this.saveSettings() + }, + showSleepTimerOptions() { + this.showSleepTimerLengthModal = true + }, + showHapticFeedbackOptions() { + this.moreMenuSetting = 'hapticFeedback' + this.showMoreMenuDialog = true + }, + showShakeSensitivityOptions() { + this.moreMenuSetting = 'shakeSensitivity' + this.showMoreMenuDialog = true + }, + clickMenuAction(action) { + this.showMoreMenuDialog = false + if (this.moreMenuSetting === 'shakeSensitivity') { + this.settings.shakeSensitivity = action + this.saveSettings() + } else if (this.moreMenuSetting === 'hapticFeedback') { + this.settings.hapticFeedback = action + this.hapticFeedbackUpdated(action) + } + }, + autoSleepTimerTimeUpdated(val) { + console.log('[settings] Auto sleep timer time=', val) + if (!val) return // invalid times return falsy this.saveSettings() }, hapticFeedbackUpdated(val) { @@ -163,6 +250,10 @@ export default { }) } }, + toggleAutoSleepTimer() { + this.settings.autoSleepTimer = !this.settings.autoSleepTimer + this.saveSettings() + }, toggleDisableShakeToResetSleepTimer() { this.settings.disableShakeToResetSleepTimer = !this.settings.disableShakeToResetSleepTimer this.saveSettings() @@ -220,11 +311,16 @@ export default { this.settings.enableAltView = !!deviceSettings.enableAltView this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10 this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10 - this.settings.disableShakeToResetSleepTimer = !!deviceSettings.disableShakeToResetSleepTimer - this.settings.shakeSensitivity = deviceSettings.shakeSensitivity || 'MEDIUM' this.settings.lockOrientation = deviceSettings.lockOrientation || 'NONE' this.lockCurrentOrientation = this.settings.lockOrientation !== 'NONE' this.settings.hapticFeedback = deviceSettings.hapticFeedback || 'LIGHT' + + this.settings.disableShakeToResetSleepTimer = !!deviceSettings.disableShakeToResetSleepTimer + this.settings.shakeSensitivity = deviceSettings.shakeSensitivity || 'MEDIUM' + this.settings.autoSleepTimer = !!deviceSettings.autoSleepTimer + this.settings.autoSleepTimerStartTime = deviceSettings.autoSleepTimerStartTime || '22:00' + this.settings.autoSleepTimerEndTime = deviceSettings.autoSleepTimerEndTime || '06:00' + this.settings.sleepTimerLength = !isNaN(deviceSettings.sleepTimerLength) ? deviceSettings.sleepTimerLength : 900000 // 15 minutes } }, mounted() {