diff --git a/android/app/build.gradle b/android/app/build.gradle index 7f5e8ab6..e35667d0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,8 +33,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 86 - versionName "0.9.55-beta" + versionCode 87 + versionName "0.9.56-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. @@ -121,7 +121,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.2' // Jackson for JSON - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2' // FFMPEG-Kit implementation 'com.arthenica:ffmpeg-kit-min:4.5.1' diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 0e926874..9e4997f5 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -59,8 +59,6 @@ data class LibraryItem( putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString()) - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString()) - putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString()) putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) }.build() } @@ -309,7 +307,6 @@ data class PodcastEpisode( putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName()) putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName()) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) @@ -407,18 +404,25 @@ data class BookChapter( } @JsonIgnoreProperties(ignoreUnknown = true) -data class MediaProgress( +class MediaProgress( var id:String, var libraryItemId:String, var episodeId:String?, var duration:Double, // seconds - var progress:Double, // 0 to 1 + progress:Double, // 0 to 1 var currentTime:Double, - var isFinished:Boolean, + isFinished:Boolean, var lastUpdate:Long, var startedAt:Long, var finishedAt:Long? +) : MediaProgressWrapper(isFinished, progress) + +@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class) +@JsonSubTypes( + JsonSubTypes.Type(MediaProgress::class), + JsonSubTypes.Type(LocalMediaProgress::class) ) +open class MediaProgressWrapper(var isFinished:Boolean, var progress:Double) // Helper class data class LibraryItemWithEpisode( 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 6b658011..714098fd 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 @@ -20,7 +20,8 @@ data class DeviceSettings( var disableAutoRewind:Boolean, var enableAltView:Boolean, var jumpBackwardsTime:Int, - var jumpForwardTime:Int + var jumpForwardTime:Int, + var disableShakeToResetSleepTimer:Boolean ) { companion object { // Static method to get default device settings @@ -29,7 +30,8 @@ data class DeviceSettings( disableAutoRewind = false, enableAltView = false, jumpBackwardsTime = 10, - jumpForwardTime = 10 + jumpForwardTime = 10, + disableShakeToResetSleepTimer = false ) } } 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 1536ac4f..c1b2e2ce 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 @@ -2,8 +2,11 @@ package com.audiobookshelf.app.data import android.content.Context import android.net.Uri +import android.os.Bundle +import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaMetadataCompat import android.util.Log +import androidx.media.utils.MediaConstants import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.fasterxml.jackson.annotation.JsonIgnore @@ -95,7 +98,7 @@ data class LocalLibraryItem( } @JsonIgnore - fun getMediaMetadata(ctx: Context): MediaMetadataCompat { + fun getMediaMetadata(): MediaMetadataCompat { val coverUri = getCoverUri() return MediaMetadataCompat.Builder().apply { @@ -104,8 +107,6 @@ data class LocalLibraryItem( putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString()) - putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString()) putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) }.build() } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt index 3a5d7462..e6658fcc 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt @@ -5,14 +5,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlin.math.roundToInt @JsonIgnoreProperties(ignoreUnknown = true) -data class LocalMediaProgress( +class LocalMediaProgress( var id:String, var localLibraryItemId:String, var localEpisodeId:String?, var duration:Double, - var progress:Double, // 0 to 1 + progress:Double, // 0 to 1 var currentTime:Double, - var isFinished:Boolean, + isFinished:Boolean, var lastUpdate:Long, var startedAt:Long, var finishedAt:Long?, @@ -22,7 +22,7 @@ data class LocalMediaProgress( var serverUserId:String?, var libraryItemId:String?, var episodeId:String? -) { +) : MediaProgressWrapper(isFinished, progress) { @get:JsonIgnore val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt() 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 eb38c9d1..0a9c0065 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 @@ -91,6 +91,7 @@ class PlaybackSession( @JsonIgnore fun getTrackStartOffsetMs(index:Int):Long { + if (index < 0 || index >= audioTracks.size) return 0L val currentTrack = audioTracks[index] return (currentTrack.startOffset * 1000L).toLong() } @@ -123,8 +124,6 @@ class PlaybackSession( .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor) .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series") .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) return metadataBuilder.build() } diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index 28c9b7cc..02e8850b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -14,6 +14,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.getcapacitor.JSObject +import org.json.JSONException class FolderScanner(var ctx: Context) { private val tag = "FolderScanner" @@ -465,11 +466,17 @@ class FolderScanner(var ctx: Context) { fun probeAudioFile(absolutePath:String):AudioProbeResult? { val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") - Log.d(tag, "FFprobe output ${JSObject(session.output)}") - val probeObject = JSObject(session.output) - if (!probeObject.has("streams")) { // Check if output is empty - Log.d(tag, "probeAudioFile Probe audio file $absolutePath is empty") + var probeObject:JSObject? = null + try { + probeObject = JSObject(session.output) + } catch(error:JSONException) { + Log.e(tag, "Failed to parse probe result $error") + } + + Log.d(tag, "FFprobe output $probeObject") + if (probeObject == null || !probeObject.has("streams")) { // Check if output is empty + Log.d(tag, "probeAudioFile Probe audio file $absolutePath failed or invalid") return null } else { val audioProbeResult = jacksonMapper.readValue(session.output) diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index 945ab001..0ef698bb 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -2,8 +2,12 @@ package com.audiobookshelf.app.media import android.app.Activity import android.content.Context +import android.os.Bundle import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.MediaDescriptionCompat +import android.support.v4.media.MediaMetadataCompat import android.util.Log +import androidx.media.utils.MediaConstants import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.server.ApiHandler @@ -18,7 +22,8 @@ import kotlin.coroutines.suspendCoroutine class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { val tag = "MediaManager" - var serverLibraryItems = mutableListOf() + var serverLibraryItems = mutableListOf() // Store all items here + var selectedLibraryItems = mutableListOf() var selectedLibraryId = "" var selectedLibraryItemWrapper:LibraryItemWrapper? = null @@ -28,6 +33,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { var serverLibraryCategories = listOf() var serverLibraries = listOf() var serverConfigIdUsed:String? = null + var serverConfigLastPing:Long = 0L + var serverUserMediaProgress:MutableList = mutableListOf() var userSettingsPlaybackRate:Float? = null @@ -68,6 +75,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { serverLibraryCategories = listOf() serverLibraries = listOf() serverLibraryItems = mutableListOf() + selectedLibraryItems = mutableListOf() selectedLibraryId = "" } } @@ -84,14 +92,18 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } fun loadLibraryItemsWithAudio(libraryId:String, cb: (List) -> Unit) { - if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) { - cb(serverLibraryItems) + if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) { + cb(selectedLibraryItems) } else { apiHandler.getLibraryItems(libraryId) { libraryItems -> val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() } - if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId + if (libraryItemsWithAudio.isNotEmpty()) { + selectedLibraryId = libraryId + } + selectedLibraryItems = mutableListOf() libraryItemsWithAudio.forEach { libraryItem -> + selectedLibraryItems.add(libraryItem) if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) { serverLibraryItems.add(libraryItem) } @@ -132,7 +144,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { val children = podcast.episodes?.map { podcastEpisode -> Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}") - MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + + val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper) + val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}") + val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress) + MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) } @@ -147,7 +163,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { selectedPodcast = podcast val children = podcast.episodes?.map { podcastEpisode -> - MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + + val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper) + val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id } + val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress) + MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) } @@ -179,16 +199,49 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { return successfulPing } - fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking { - if (!apiHandler.isOnline()) cb(false) - else { - coroutineScope { - var hasValidConn = false + suspend fun authorize(config:ServerConnectionConfig) : MutableList { + var mediaProgress:MutableList = mutableListOf() + suspendCoroutine> { cont -> + apiHandler.authorize(config) { + Log.d(tag, "authorize: Authorized server config ${config.address} result = $it") + if (!it.isNullOrEmpty()) { + mediaProgress = it + } + cont.resume(mediaProgress) + } + } + return mediaProgress + } - // First check if the current selected config is pingable - DeviceManager.serverConnectionConfig?.let { - hasValidConn = checkServerConnection(it) - Log.d(tag, "checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn") + fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking { + Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed") + + coroutineScope { + if (!apiHandler.isOnline()) { + serverUserMediaProgress = mutableListOf() + cb(false) + } else { + + var hasValidConn = false + var lookupMediaProgress = true + + if (!serverConfigIdUsed.isNullOrEmpty() && serverConfigLastPing > 0L && System.currentTimeMillis() - serverConfigLastPing < 5000) { + Log.d(tag, "checkSetValidServerConnectionConfig last ping less than a 5 seconds ago") + hasValidConn = true + lookupMediaProgress = false + } else { + serverUserMediaProgress = mutableListOf() + } + + if (!hasValidConn) { + // First check if the current selected config is pingable + DeviceManager.serverConnectionConfig?.let { + hasValidConn = checkServerConnection(it) + Log.d( + tag, + "checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn" + ) + } } if (!hasValidConn) { @@ -205,9 +258,21 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } + if (hasValidConn) { + serverConfigLastPing = System.currentTimeMillis() + + if (lookupMediaProgress) { + Log.d(tag, "Has valid conn now get user media progress") + DeviceManager.serverConnectionConfig?.let { + serverUserMediaProgress = authorize(it) + } + } + } + cb(hasValidConn) } } + } // TODO: Load currently listening category for local items @@ -259,6 +324,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } + // Log.d(tag, "Found library category ${it.label} with type ${it.type}") if (it.type == library.mediaType) { // Log.d(tag, "Using library category ${it.id}") @@ -328,6 +394,41 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } + fun getMediaDescriptionFromMediaMetadata(item: MediaMetadataCompat, progress:MediaProgressWrapper?): MediaDescriptionCompat { + + val extras = Bundle() + if (progress != null) { + Log.d(tag, "Has media progress for ${item.description.title} | ${progress}") + if (progress.isFinished) { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } else { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + extras.putDouble( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress + ) + } + } else { + Log.d(tag, "No media progress for ${item.description.title} | ${item.description.mediaId}") + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } + + return MediaDescriptionCompat.Builder() + .setMediaId(item.description.mediaId) + .setTitle(item.description.title) + .setIconUri(item.description.iconUri) + .setSubtitle(item.description.subtitle) + .setExtras(extras).build() + } + private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { val lhsLength = lhs.length + 1 val rhsLength = rhs.length + 1 diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt index 115f6b3d..cf519f8e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt @@ -6,10 +6,7 @@ import android.net.Uri import android.support.v4.media.MediaMetadataCompat import androidx.annotation.AnyRes import com.audiobookshelf.app.R -import com.audiobookshelf.app.data.Library -import com.audiobookshelf.app.data.LibraryCategory -import com.audiobookshelf.app.data.LibraryItem -import com.audiobookshelf.app.data.LocalLibraryItem +import com.audiobookshelf.app.data.* class BrowseTree( val context: Context, @@ -85,7 +82,7 @@ class BrowseTree( localBooksCat.entities.forEach { libc -> val libraryItem = libc as LocalLibraryItem val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() - children += libraryItem.getMediaMetadata(context) + children += libraryItem.getMediaMetadata() mediaIdToChildren[DOWNLOADS_ROOT] = children } } @@ -94,7 +91,7 @@ class BrowseTree( localPodcastsCat.entities.forEach { libc -> val libraryItem = libc as LocalLibraryItem val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() - children += libraryItem.getMediaMetadata(context) + children += libraryItem.getMediaMetadata() mediaIdToChildren[DOWNLOADS_ROOT] = children } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt index c8803565..99ee91b2 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt @@ -74,14 +74,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic } } - fun stop(cb: () -> Unit) { + fun stop(shouldSync:Boolean? = true, cb: () -> Unit) { if (!listeningTimerRunning) return listeningTimerTask?.cancel() listeningTimerTask = null listeningTimerRunning = false Log.d(tag, "stop: Stopping listening for $currentDisplayTitle") - val currentTime = playerNotificationService.getCurrentTimeSeconds() + val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0 if (currentTime > 0) { // Current time should always be > 0 on stop sync(true, currentTime) { reset() diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationListener.kt index ef58fbed..3fa291c4 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationListener.kt @@ -29,9 +29,14 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio Log.d(tag, "onNotificationCancelled not dismissed by user") // When stop button is pressed on the notification I guess it isn't considered "dismissedByUser" so we need to close playback ourselves - if (!PlayerNotificationService.isClosed) { + if (!PlayerNotificationService.isClosed && !PlayerNotificationService.isSwitchingPlayer) { Log.d(tag, "PNS is not closed - closing it now") playerNotificationService.closePlayback() + } else if (PlayerNotificationService.isSwitchingPlayer) { + // When switching from cast player to exo player and vice versa the notification is cancelled and posted again + // so we don't want to cancel the playback during this switch + Log.d(tag, "PNS is switching player") + PlayerNotificationService.isSwitchingPlayer = false } } } 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 86822961..eb313b6c 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 @@ -50,6 +50,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var isStarted = false var isClosed = false var isUnmeteredNetwork = false + var isSwitchingPlayer = false // Used when switching between cast player and exoplayer } interface ClientEventEmitter { @@ -147,6 +148,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // detach player override fun onDestroy() { + try { + val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager + connectivityManager.unregisterNetworkCallback(networkCallback) + } catch(error:Exception) { + Log.e(tag, "Error unregistering network listening callback $error") + } + + Log.d(tag, "onDestroy") playerNotificationManager.setPlayer(null) mPlayer.release() castPlayer?.release() @@ -255,11 +264,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // Fix for local images crashing on Android 11 for specific devices // https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958 - ctx.grantUriPermission( - "com.android.systemui", - coverUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) + try { + ctx.grantUriPermission( + "com.android.systemui", + coverUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch(error:Exception) { + Log.e(tag, "Grant uri permission error $error") + } return MediaDescriptionCompat.Builder() .setMediaId(currentPlaybackSession!!.id) @@ -309,8 +322,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val seekBackTime = DeviceManager.deviceData.deviceSettings?.jumpBackwardsTimeMs ?: 10000 val seekForwardTime = DeviceManager.deviceData.deviceSettings?.jumpForwardTimeMs ?: 10000 - Log.d(tag, "Seek Back Time $seekBackTime") - Log.d(tag, "Seek Forward Time $seekForwardTime") mPlayer = ExoPlayer.Builder(this) .setLoadControl(customLoadControl) @@ -413,7 +424,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.setPlaybackSpeed(playbackRateToUse) currentPlayer.prepare() - } else if (castPlayer != null) { val currentTrackIndex = playbackSession.getCurrentTrackIndex() val currentTrackTime = playbackSession.getCurrentTrackTimeMs() @@ -436,7 +446,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) { if (it == null) { // Play request failed clientEventEmitter?.onPlaybackFailed(errorMessage) - closePlayback() + closePlayback(true) } else { Handler(Looper.getMainLooper()).post { preparePlayer(it, true, null) @@ -445,7 +455,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } else { clientEventEmitter?.onPlaybackFailed(errorMessage) - closePlayback() + closePlayback(true) } } } @@ -489,6 +499,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } + if (currentPlaybackSession == null) { + Log.e(tag, "switchToPlayer: No Current playback session") + } else { + isSwitchingPlayer = true + } + + // Playback session in progress syncer is a copy that is up-to-date so replace current here with that + // TODO: bad design here implemented to prevent the session in MediaProgressSyncer from changing while syncing + if (mediaProgressSyncer.currentPlaybackSession != null) { + currentPlaybackSession = mediaProgressSyncer.currentPlaybackSession?.clone() + } + currentPlayer = if (useCastPlayer) { Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo) mediaSessionConnector.setPlayer(castPlayer) @@ -503,15 +525,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer()) - if (currentPlaybackSession == null) { - Log.d(tag, "switchToPlayer: No Current playback session") - } - currentPlaybackSession?.let { - Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}") + Log.d(tag, "switchToPlayer: Starting new playback session ${it.displayTitle}") if (wasPlaying) { // media is paused when switching players clientEventEmitter?.onPlayingUpdate(false) } + + // TODO: Start a new playback session here instead of using the existing preparePlayer(it, false, null) } } @@ -548,10 +568,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return currentPlaybackSession?.totalDurationMs ?: 0L } - fun getCurrentBookTitle() : String? { - return currentPlaybackSession?.displayTitle - } - fun getCurrentPlaybackSessionCopy() :PlaybackSession? { return currentPlaybackSession?.clone() } @@ -656,10 +672,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return } currentPlayer.volume = 1F - if (currentPlayer == castPlayer) { - Log.d(tag, "CAST Player set on play ${currentPlayer.isLoading} || ${currentPlayer.duration} | ${currentPlayer.currentPosition}") - } - currentPlayer.play() } @@ -701,12 +713,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.setPlaybackSpeed(speed) } - fun closePlayback() { + fun closePlayback(calledOnError:Boolean? = false) { Log.d(tag, "closePlayback") if (mediaProgressSyncer.listeningTimerRunning) { Log.i(tag, "About to close playback so stopping media progress syncer first") - mediaProgressSyncer.stop { - Log.d(tag, "Media Progress syncer stopped and synced") + mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception) + Log.d(tag, "Media Progress syncer stopped") } } @@ -816,7 +828,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onLoadChildren(parentMediaId: String, result: Result>) { Log.d(tag, "ON LOAD CHILDREN $parentMediaId") - var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + val flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE result.detach() @@ -832,10 +844,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val libraryItemMediaMetadata = libraryItem.getMediaMetadata() if (libraryItem.mediaType == "podcast") { // Podcasts are browseable - flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) + } else { + val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemMediaMetadata.description.mediaId } + val description = mediaManager.getMediaDescriptionFromMediaMetadata(libraryItemMediaMetadata, progress) + MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } - - MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag) } result.sendResult(children as MutableList?) } @@ -846,26 +860,34 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val localBrowseItems:MutableList = mutableListOf() localBooks.forEach { localLibraryItem -> - val mediaMetadata = localLibraryItem.getMediaMetadata(ctx) - localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + val mediaMetadata = localLibraryItem.getMediaMetadata() + val progress = DeviceManager.dbManager.getLocalMediaProgress(mediaMetadata.description.mediaId ?: "") + val description = mediaManager.getMediaDescriptionFromMediaMetadata(mediaMetadata, progress) + + localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } localPodcasts.forEach { localLibraryItem -> - val mediaMetadata = localLibraryItem.getMediaMetadata(ctx) + val mediaMetadata = localLibraryItem.getMediaMetadata() localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) } result.sendResult(localBrowseItems) } else { // Load categories - - mediaManager.loadAndroidAutoItems() { libraryCategories -> + mediaManager.loadAndroidAutoItems { libraryCategories -> browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) val children = browseTree[parentMediaId]?.map { item -> Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag") - MediaBrowserCompat.MediaItem(item.description, flag) + if (flag == MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) { + val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == item.description.mediaId } + val description = mediaManager.getMediaDescriptionFromMediaMetadata(item, progress) + MediaBrowserCompat.MediaItem(description, flag) + } else { + MediaBrowserCompat.MediaItem(item.description, flag) + } } result.sendResult(children as MutableList?) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/player/SleepTimerManager.kt index 30efac35..cc174af1 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/SleepTimerManager.kt @@ -3,6 +3,7 @@ package com.audiobookshelf.app.player import android.content.Context import android.os.* import android.util.Log +import com.audiobookshelf.app.device.DeviceManager import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt @@ -203,8 +204,13 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica } fun handleShake() { - Log.d(tag, "HANDLE SHAKE HERE") - if (sleepTimerRunning || sleepTimerFinishedAt > 0L) checkShouldExtendSleepTimer() + if (sleepTimerRunning || sleepTimerFinishedAt > 0L) { + if (DeviceManager.deviceData.deviceSettings?.disableShakeToResetSleepTimer == true) { + Log.d(tag, "Shake to reset sleep timer is disabled") + return + } + checkShouldExtendSleepTimer() + } } fun increaseSleepTime(time: Long) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index b55910f5..3893b558 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -16,6 +16,7 @@ import com.getcapacitor.JSObject import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.io.IOException @@ -43,11 +44,13 @@ class ApiHandler(var ctx:Context) { makeRequest(request, httpClient, cb) } - fun postRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) { + fun postRequest(endpoint:String, payload: JSObject, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) { + val address = config?.address ?: DeviceManager.serverAddress + val token = config?.token ?: DeviceManager.token val mediaType = "application/json; charset=utf-8".toMediaType() val requestBody = payload.toString().toRequestBody(mediaType) val request = Request.Builder().post(requestBody) - .url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}") + .url("${address}$endpoint").addHeader("Authorization", "Bearer ${token}") .build() makeRequest(request, null, cb) } @@ -211,7 +214,7 @@ class ApiHandler(var ctx:Context) { val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload)) val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId" - postRequest(endpoint, payload) { + postRequest(endpoint, payload, null) { if (it.has("error")) { Log.e(tag, it.getString("error") ?: "Play Library Item Failed") cb(null) @@ -227,7 +230,7 @@ class ApiHandler(var ctx:Context) { fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) { val payload = JSObject(jacksonMapper.writeValueAsString(syncData)) - postRequest("/api/session/$sessionId/sync", payload) { + postRequest("/api/session/$sessionId/sync", payload, null) { if (!it.getString("error").isNullOrEmpty()) { cb(false) } else { @@ -239,7 +242,7 @@ class ApiHandler(var ctx:Context) { fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) { val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession)) - postRequest("/api/session/local", payload) { + postRequest("/api/session/local", payload, null) { if (!it.getString("error").isNullOrEmpty()) { cb(false) } else { @@ -265,7 +268,7 @@ class ApiHandler(var ctx:Context) { if (localMediaProgress.isNotEmpty()) { Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items") val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress))) - postRequest("/api/me/sync-local-progress", payload) { + postRequest("/api/me/sync-local-progress", payload, null) { Log.d(tag, "Media Progress Sync payload $payload - response ${it}") if (it.toString() == "{}") { @@ -343,4 +346,25 @@ class ApiHandler(var ctx:Context) { } } } + + fun authorize(config:ServerConnectionConfig, cb: (MutableList?) -> Unit) { + Log.d(tag, "authorize: Authorizing ${config.address}") + postRequest("/api/authorize", JSObject(), config) { + val error = it.getString("error") + if (!error.isNullOrEmpty()) { + Log.d(tag, "authorize: Authorize ${config.address} Failed: $error") + cb(null) + } else { + val mediaProgressList:MutableList = mutableListOf() + val user = it.getJSObject("user") + val mediaProgress = user?.getJSONArray("mediaProgress") ?: JSONArray() + for (i in 0 until mediaProgress.length()) { + val mediaProg = jacksonMapper.readValue(mediaProgress.getJSONObject(i).toString()) + mediaProgressList.add(mediaProg) + } + Log.d(tag, "authorize: Authorize ${config.address} Successful") + cb(mediaProgressList) + } + } + } } diff --git a/android/app/src/main/res/drawable-land-hdpi/screen.png b/android/app/src/main/res/drawable-land-hdpi/screen.png deleted file mode 100644 index db9f3288..00000000 Binary files a/android/app/src/main/res/drawable-land-hdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-mdpi/screen.png b/android/app/src/main/res/drawable-land-mdpi/screen.png deleted file mode 100644 index 85796f95..00000000 Binary files a/android/app/src/main/res/drawable-land-mdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/screen.png b/android/app/src/main/res/drawable-land-xhdpi/screen.png deleted file mode 100644 index 8610f113..00000000 Binary files a/android/app/src/main/res/drawable-land-xhdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/screen.png b/android/app/src/main/res/drawable-land-xxhdpi/screen.png deleted file mode 100644 index 52f5f3a4..00000000 Binary files a/android/app/src/main/res/drawable-land-xxhdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/screen.png b/android/app/src/main/res/drawable-land-xxxhdpi/screen.png deleted file mode 100644 index 1d36e7b4..00000000 Binary files a/android/app/src/main/res/drawable-land-xxxhdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-hdpi/screen.png b/android/app/src/main/res/drawable-port-hdpi/screen.png deleted file mode 100644 index 046f49da..00000000 Binary files a/android/app/src/main/res/drawable-port-hdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-mdpi/screen.png b/android/app/src/main/res/drawable-port-mdpi/screen.png deleted file mode 100644 index 5dec3f2f..00000000 Binary files a/android/app/src/main/res/drawable-port-mdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/screen.png b/android/app/src/main/res/drawable-port-xhdpi/screen.png deleted file mode 100644 index e53e2dbd..00000000 Binary files a/android/app/src/main/res/drawable-port-xhdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/screen.png b/android/app/src/main/res/drawable-port-xxhdpi/screen.png deleted file mode 100644 index d066a875..00000000 Binary files a/android/app/src/main/res/drawable-port-xxhdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/screen.png b/android/app/src/main/res/drawable-port-xxxhdpi/screen.png deleted file mode 100644 index 4dfea4a7..00000000 Binary files a/android/app/src/main/res/drawable-port-xxxhdpi/screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21db..00000000 --- a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc53..00000000 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/screen.png b/android/app/src/main/res/drawable/screen.png deleted file mode 100644 index 046f49da..00000000 Binary files a/android/app/src/main/res/drawable/screen.png and /dev/null differ diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml index 80d515ff..4c10bf86 100644 --- a/android/app/src/main/res/values-v21/styles.xml +++ b/android/app/src/main/res/values-v21/styles.xml @@ -1,4 +1,23 @@ + + + + + + + +