diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt index bf84c70a..5c11602e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -70,8 +70,9 @@ class MainActivity : BridgeActivity() { Log.d(tag, "onCreate") -// var ss = SimpleStorage(this) -// ss.requestFullStorageAccess() + // Grant full storage access for testing + // var ss = SimpleStorage(this) + // ss.requestFullStorageAccess() var permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) if (permission != PackageManager.PERMISSION_GRANTED) { 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 12ddc893..5fe635c4 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 @@ -26,7 +26,7 @@ data class LibraryItem( var mediaType:String, var media:MediaType, var libraryFiles:MutableList? -) { +) : LibraryItemWrapper() { @get:JsonIgnore val title get() = media.metadata.title @get:JsonIgnore 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 6de16cd1..2d93a2e1 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 @@ -2,6 +2,8 @@ package com.audiobookshelf.app.data import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo import java.util.* data class ServerConnectionConfig( @@ -18,7 +20,14 @@ data class DeviceData( var serverConnectionConfigs:MutableList, var lastServerConnectionConfigId:String?, var currentLocalPlaybackSession:PlaybackSession? // Stored to open up where left off for local media -) +) { + @JsonIgnore + fun getLastServerConnectionConfig():ServerConnectionConfig? { + return lastServerConnectionConfigId?.let { lsccid -> + return serverConnectionConfigs.find { it.id == lsccid } + } + } +} @JsonIgnoreProperties(ignoreUnknown = true) data class LocalFile( @@ -48,3 +57,10 @@ data class LocalFolder( var storageType:String, var mediaType:String ) + +@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION) +@JsonSubTypes( + JsonSubTypes.Type(LibraryItem::class), + JsonSubTypes.Type(LocalLibraryItem::class) +) +open class LibraryItemWrapper() diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryCategory.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryCategory.kt new file mode 100644 index 00000000..2ab2bc96 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryCategory.kt @@ -0,0 +1,12 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LibraryCategory( + var id:String, + var label:String, + var type:String, + var entities:List, + var isLocal:Boolean +) 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 c515dfb4..f44c908e 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 @@ -1,6 +1,9 @@ package com.audiobookshelf.app.data +import android.net.Uri +import android.support.v4.media.MediaMetadataCompat import android.util.Log +import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties @@ -25,11 +28,19 @@ data class LocalLibraryItem( var serverAddress:String?, var serverUserId:String?, var libraryItemId:String? - ) { - + ) : LibraryItemWrapper() { + @get:JsonIgnore + val title get() = media.metadata.title + @get:JsonIgnore + val authorName get() = media.metadata.getAuthorDisplayName() @get:JsonIgnore val isPodcast get() = mediaType == "podcast" + @JsonIgnore + fun getCoverUri(): Uri { + return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) + } + @JsonIgnore fun getDuration():Double { var total = 0.0 @@ -80,4 +91,18 @@ data class LocalLibraryItem( fun removeLocalFile(localFileId:String) { localFiles.removeIf { it.id == localFileId } } + + @JsonIgnore + fun getMediaMetadata(): MediaMetadataCompat { + return MediaMetadataCompat.Builder().apply { + 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, 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() + } } 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 b9cc9cf0..3744da17 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 @@ -6,7 +6,8 @@ import com.audiobookshelf.app.data.DeviceData import com.audiobookshelf.app.data.ServerConnectionConfig object DeviceManager { - val tag = "DeviceManager" + const val tag = "DeviceManager" + val dbManager:DbManager = DbManager() var deviceData:DeviceData = dbManager.getDeviceData() var serverConnectionConfig: ServerConnectionConfig? = null @@ -15,6 +16,8 @@ object DeviceManager { val serverAddress get() = serverConnectionConfig?.address ?: "" val serverUserId get() = serverConnectionConfig?.userId ?: "" val token get() = serverConnectionConfig?.token ?: "" + val isConnectedToServer get() = serverConnectionConfig != null + val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null init { Log.d(tag, "Device Manager Singleton invoked") 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 3a1edb65..56807052 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 @@ -1,43 +1,150 @@ package com.audiobookshelf.app.media -import com.audiobookshelf.app.data.LibraryItem -import com.audiobookshelf.app.data.PlaybackSession +import android.bluetooth.BluetoothClass +import android.content.Context +import android.util.Log +import com.audiobookshelf.app.data.* +import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.server.ApiHandler import java.util.* +import io.paperdb.Paper + +class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { + val tag = "MediaManager" -class MediaManager(var apiHandler: ApiHandler) { var serverLibraryItems = listOf() + var serverLibraryCategories = listOf() + var serverLibraries = listOf() - fun loadLibraryItems(cb: (List) -> Unit) { + fun initializeAndroidAuto() { + Log.d(tag, "Android Auto started when MainActivity was never started - initializing Paper") + Paper.init(ctx) + } + + fun loadLibraryCategories(libraryId:String, cb: (List) -> Unit) { + if (serverLibraryCategories.isNotEmpty()) { + cb(serverLibraryCategories) + } else { + apiHandler.getLibraryCategories(libraryId) { + serverLibraryCategories = it + cb(it) + } + } + } + + fun loadLibraryItems(libraryId:String, cb: (List) -> Unit) { if (serverLibraryItems.isNotEmpty()) { cb(serverLibraryItems) } else { - apiHandler.getLibraryItems("main") { libraryItems -> + apiHandler.getLibraryItems(libraryId) { libraryItems -> serverLibraryItems = libraryItems cb(libraryItems) } } } - fun getFirstItem() : LibraryItem? { - return if (serverLibraryItems.isNotEmpty()) serverLibraryItems[0] else null + fun loadLibraries(cb: (List) -> Unit) { + if (serverLibraries.isNotEmpty()) { + cb(serverLibraries) + } else { + apiHandler.getLibraries { + serverLibraries = it + cb(it) + } + } } - fun getById(id:String) : LibraryItem? { - return serverLibraryItems.find { it.id == id } + // TODO: Load currently listening category for local items + fun loadLocalCategory():List { + var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") + var localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") + var cats = mutableListOf() + if (localBooks.isNotEmpty()) { + cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true)) + } + if (localPodcasts.isNotEmpty()) { + cats.add(LibraryCategory("local-podcasts", "Local Podcasts", "podcast", localPodcasts, true)) + } + return cats } - fun getFromSearch(query:String?) : LibraryItem? { + fun loadAndroidAutoItems(libraryId:String, cb: (List) -> Unit) { + Log.d(tag, "Load android auto items for library id $libraryId") + var cats = mutableListOf() + + var localCategories = loadLocalCategory() + cats.addAll(localCategories) + + // Connected to server and has internet - load other cats + if (apiHandler.isOnline() && (DeviceManager.isConnectedToServer || DeviceManager.hasLastServerConnectionConfig)) { + if (!DeviceManager.isConnectedToServer) { + DeviceManager.serverConnectionConfig = DeviceManager.deviceData.getLastServerConnectionConfig() + Log.d(tag, "Not connected to server, set last server \"${DeviceManager.serverAddress}\"") + } + + loadLibraries { libraries -> + var library = libraries.find { it.id == libraryId } ?: libraries[0] + Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}") + + loadLibraryCategories(libraryId) { libraryCategories -> + + // Only using book or podcast library categories for now + libraryCategories.forEach { + 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}") + cats.add(it) + } + } + + loadLibraryItems(libraryId) { libraryItems -> + var mainCat = LibraryCategory("library", "Library", library.mediaType, libraryItems, false) + cats.add(mainCat) + + cb(cats) + } + } + } + } else { // Not connected/no internet sent downloaded cats only + cb(cats) + } + } + + fun getFirstItem() : LibraryItemWrapper? { + if (serverLibraryItems.isNotEmpty()) { + return serverLibraryItems[0] + } else { + var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") + return if (localBooks.isNotEmpty()) return localBooks[0] else null + } + } + + fun getById(id:String) : LibraryItemWrapper? { + if (id.startsWith("local")) { + return DeviceManager.dbManager.getLocalLibraryItem(id) + } else { + return serverLibraryItems.find { it.id == id } + } + } + + fun getFromSearch(query:String?) : LibraryItemWrapper? { if (query.isNullOrEmpty()) return getFirstItem() return serverLibraryItems.find { it.title.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault())) } } - fun play(libraryItem:LibraryItem, mediaPlayer:String, cb: (PlaybackSession) -> Unit) { - apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) { - cb(it) - } + fun play(libraryItemWrapper:LibraryItemWrapper, mediaPlayer:String, cb: (PlaybackSession) -> Unit) { + if (libraryItemWrapper is LocalLibraryItem) { + var localLibraryItem = libraryItemWrapper as LocalLibraryItem + cb(localLibraryItem.getPlaybackSession(null)) + } else { + var libraryItem = libraryItemWrapper as LibraryItem + apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) { + cb(it) + } + } } private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { 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 68569ed7..78d9f4ab 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 @@ -7,14 +7,14 @@ import android.support.v4.media.MediaMetadataCompat import android.util.Log import androidx.annotation.AnyRes import com.audiobookshelf.app.R +import com.audiobookshelf.app.data.LibraryCategory import com.audiobookshelf.app.data.LibraryItem +import com.audiobookshelf.app.data.LocalLibraryItem class BrowseTree( val context: Context, - itemsInProgress: List, - itemsMetadata: List, - downloadedMetadata: List + libraryCategories: List ) { private val mediaIdToChildren = mutableMapOf>() @@ -37,17 +37,14 @@ class BrowseTree( val continueReadingMetadata = MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT) - putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Reading") + putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening") putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString()) }.build() val allMetadata = MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT) putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items") - - var resource = getUriToDrawable(context, R.drawable.exo_icon_books).toString() - Log.d("BrowseTree", "RESOURCE $resource") - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource) + putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_books).toString()) }.build() val downloadsMetadata = MediaMetadataCompat.Builder().apply { @@ -56,31 +53,51 @@ class BrowseTree( putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString()) }.build() - if (itemsInProgress.isNotEmpty()) { - rootList += continueReadingMetadata + // Server continue Listening cat + libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory -> + var continueListeningMediaMetadata = continueListeningCategory.entities.map { liw -> + var libraryItem = liw as LibraryItem + libraryItem.getMediaMetadata() + } + if (continueListeningMediaMetadata.isNotEmpty()) { + rootList += continueReadingMetadata + } + continueListeningMediaMetadata.forEach { + val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf() + children += it + mediaIdToChildren[CONTINUE_ROOT] = children + } } + rootList += allMetadata rootList += downloadsMetadata -// rootList += localsMetadata + + // Server library cat + libraryCategories.find { it.id == "library" }?.let { libraryCategory -> + var libraryMediaMetadata = libraryCategory.entities.map { libc -> + var libraryItem = libc as LibraryItem + libraryItem.getMediaMetadata() + } + libraryMediaMetadata.forEach { + val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf() + children += it + mediaIdToChildren[ALL_ROOT] = children + } + } + + libraryCategories.find { it.id == "local-books" }?.let { localBooksCat -> + var localMediaMetadata = localBooksCat.entities.map { libc -> + var libraryItem = libc as LocalLibraryItem + libraryItem.getMediaMetadata() + } + localMediaMetadata.forEach { + val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() + children += it + mediaIdToChildren[DOWNLOADS_ROOT] = children + } + } + mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList - - itemsInProgress.forEach { libraryItem -> - val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf() - children += libraryItem.getMediaMetadata() - mediaIdToChildren[CONTINUE_ROOT] = children - } - - itemsMetadata.forEach { - val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf() - allChildren += it - mediaIdToChildren[ALL_ROOT] = allChildren - } - - downloadedMetadata.forEach { - val allChildren = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() - allChildren += it - mediaIdToChildren[DOWNLOADS_ROOT] = allChildren - } } operator fun get(mediaId: String) = mediaIdToChildren[mediaId] @@ -90,4 +107,3 @@ const val AUTO_BROWSE_ROOT = "/" const val ALL_ROOT = "__ALL__" const val CONTINUE_ROOT = "__CONTINUE__" const val DOWNLOADS_ROOT = "__DOWNLOADS__" -//const val LOCAL_ROOT = "__LOCAL__" 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 1b85e79d..f0ea8f44 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 @@ -10,6 +10,7 @@ import android.support.v4.media.session.MediaSessionCompat import android.util.Log import android.view.KeyEvent import com.audiobookshelf.app.data.LibraryItem +import com.audiobookshelf.app.data.LibraryItemWrapper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -27,7 +28,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT") playerNotificationService.mediaManager.getFirstItem()?.let { li -> playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { - Log.d(tag, "About to prepare player with li ${li.title}") + Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,true) } @@ -49,7 +50,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi Log.d(tag, "ON PLAY FROM SEARCH $query") playerNotificationService.mediaManager.getFromSearch(query)?.let { li -> playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { - Log.d(tag, "About to prepare player with li ${li.title}") + Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,true) } @@ -88,16 +89,16 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId") - var libraryItem: LibraryItem? = null + var libraryItemWrapper: LibraryItemWrapper? = null if (mediaId.isNullOrEmpty()) { - libraryItem = playerNotificationService.mediaManager.getFirstItem() + libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem() } else { - libraryItem = playerNotificationService.mediaManager.getById(mediaId) + libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId) } - libraryItem?.let { li -> + libraryItemWrapper?.let { li -> playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { - Log.d(tag, "About to prepare player with li ${li.title}") + Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,true) } @@ -110,7 +111,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi } fun handleCallMediaButton(intent: Intent): Boolean { - if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) { + if(Intent.ACTION_MEDIA_BUTTON == intent.action) { var keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) if (keyEvent?.getAction() == KeyEvent.ACTION_UP) { when (keyEvent?.getKeyCode()) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt index 8bf3e8af..a5ccd8c0 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt @@ -8,6 +8,7 @@ import android.os.ResultReceiver import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import com.audiobookshelf.app.data.LibraryItem +import com.audiobookshelf.app.data.LibraryItemWrapper import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector @@ -40,10 +41,10 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady") - var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId) - libraryItem?.let { li -> + var libraryItemWrapper: LibraryItemWrapper? = playerNotificationService.mediaManager.getById(mediaId) + libraryItemWrapper?.let { li -> playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { - Log.d(tag, "About to prepare player with li ${li.title}") + Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,playWhenReady) } @@ -55,7 +56,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat Log.d(tag, "ON PREPARE FROM SEARCH $query") playerNotificationService.mediaManager.getFromSearch(query)?.let { li -> playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { - Log.d(tag, "About to prepare player with li ${li.title}") + Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,playWhenReady) } 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 a7581750..00552e02 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.upstream.* +import io.paperdb.Paper import java.util.* import kotlin.concurrent.schedule @@ -196,7 +197,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { initSensor() // Initialize media manager - mediaManager = MediaManager(apiHandler) + mediaManager = MediaManager(apiHandler, ctx) channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel(channelId, channelName) @@ -300,6 +301,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var metadata = playbackSession.getMediaMetadataCompat() mediaSession.setMetadata(metadata) var mediaItems = playbackSession.getMediaItems() + + if (mediaItems.isEmpty()) { + Log.e(tag, "Invalid playback session no media items to play") + currentPlaybackSession = null + return + } + if (mPlayer == currentPlayer) { var mediaSource:MediaSource @@ -561,7 +569,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // No further calls will be made to other media browsing methods. null } else { - // Flag is used to enable syncing progress natively (normally syncing is handled in webview) + if (!isStarted) { + Log.d(tag, "AA Not yet started") + mediaManager.initializeAndroidAuto() + isStarted = true + } + isAndroidAuto = true val extras = Bundle() @@ -584,9 +597,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE result.detach() - mediaManager.loadLibraryItems { libraryItems -> - var itemMediaMetadata:List = libraryItems.map { it.getMediaMetadata() } - browseTree = BrowseTree(this, mutableListOf(), itemMediaMetadata, mutableListOf()) + + mediaManager.loadAndroidAutoItems("main") { libraryCategories -> + browseTree = BrowseTree(this, libraryCategories) val children = browseTree[parentMediaId]?.map { item -> MediaBrowserCompat.MediaItem(item.description, flag) } @@ -605,9 +618,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onSearch(query: String, extras: Bundle?, result: Result>) { result.detach() - mediaManager.loadLibraryItems { libraryItems -> - var itemMediaMetadata:List = libraryItems.map { it.getMediaMetadata() } - browseTree = BrowseTree(this, mutableListOf(), itemMediaMetadata, mutableListOf()) + mediaManager.loadAndroidAutoItems("main") { libraryCategories -> + browseTree = BrowseTree(this, libraryCategories) val children = browseTree[ALL_ROOT]?.map { item -> MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } 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 6d018106..3972d24f 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 @@ -5,11 +5,7 @@ import android.content.SharedPreferences import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.util.Log -import androidx.core.content.ContextCompat.getSystemService -import com.audiobookshelf.app.data.Library -import com.audiobookshelf.app.data.LibraryItem -import com.audiobookshelf.app.data.LocalMediaProgress -import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.MediaProgressSyncData import com.fasterxml.jackson.annotation.JsonIgnoreProperties @@ -21,14 +17,15 @@ import com.getcapacitor.JSObject import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject import java.io.IOException - -class ApiHandler { +class ApiHandler(var ctx:Context) { val tag = "ApiHandler" + private var client = OkHttpClient() var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) - var ctx: Context + var storageSharedPreferences: SharedPreferences? = null data class LocalMediaProgressSyncPayload(val localMediaProgress:List) @@ -36,10 +33,6 @@ class ApiHandler { data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List) data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int) - constructor(_ctx: Context) { - ctx = _ctx - } - fun getRequest(endpoint:String, cb: (JSObject) -> Unit) { val request = Request.Builder() .url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}") @@ -67,19 +60,17 @@ class ApiHandler { fun isOnline(): Boolean { val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - if (connectivityManager != null) { - val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - if (capabilities != null) { - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR") - return true - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { - Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI") - return true - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { - Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET") - return true - } + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (capabilities != null) { + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR") + return true + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI") + return true + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET") + return true } } return false @@ -151,6 +142,27 @@ class ApiHandler { } } + fun getLibraryCategories(libraryId:String, cb: (List) -> Unit) { + getRequest("/api/libraries/$libraryId/personalized") { + val items = mutableListOf() + if (it.has("value")) { + var array = it.getJSONArray("value") + for (i in 0 until array.length()) { + var jsobj = array.get(i) as JSONObject + + var type = jsobj.get("type").toString() + // Only support for podcast and book in android auto + if (type == "podcast" || type == "book") { + jsobj.put("isLocal", false) + val item = jacksonMapper.readValue(jsobj.toString()) + items.add(item) + } + } + } + cb(items) + } + } + fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) { var payload = JSObject() payload.put("mediaPlayer", mediaPlayer)