diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7becd87d..d7252823 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -13,8 +13,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 37
- versionName "0.9.18-beta"
+ versionCode 38
+ versionName "0.9.19-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f67b82b5..a5c482ba 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,23 +36,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt b/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt
index 0c9b75cf..0832b189 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt
@@ -1,6 +1,10 @@
package com.audiobookshelf.app
import android.net.Uri
+import android.os.Bundle
+import android.support.v4.media.MediaBrowserCompat
+import android.support.v4.media.MediaDescriptionCompat
+import android.support.v4.media.MediaMetadataCompat
import com.getcapacitor.JSObject
class Audiobook {
@@ -16,10 +20,21 @@ class Audiobook {
var isInvalid:Boolean
var path:String
- var fallbackCover:Uri
- var fallbackUri:Uri
+ var isDownloaded:Boolean = false
+ var downloadFolderUrl:String = ""
+ var folderUrl:String = ""
+ var contentUrl:String = ""
+ var filename:String = ""
+ var localCoverUrl:String = ""
+ var localCover:String = ""
+
+ var serverUrl:String = ""
+ var token:String = ""
+
+ constructor(jsobj: JSObject, serverUrl:String, token:String) {
+ this.serverUrl = serverUrl
+ this.token = token
- constructor(jsobj: JSObject) {
id = jsobj.getString("id", "").toString()
ino = jsobj.getString("ino", "").toString()
libraryId = jsobj.getString("libraryId", "").toString()
@@ -35,11 +50,75 @@ class Audiobook {
isInvalid = jsobj.getBoolean("isInvalid")
path = jsobj.getString("path", "").toString()
- fallbackUri = Uri.parse("http://fallback.com/run.mp3")
- fallbackCover = Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
+ isDownloaded = jsobj.getBoolean("isDownloaded")
+ if (isDownloaded) {
+ downloadFolderUrl = jsobj.getString("downloadFolderUrl", "").toString()
+ folderUrl = jsobj.getString("folderUrl", "").toString()
+ contentUrl = jsobj.getString("contentUrl", "").toString()
+ filename = jsobj.getString("filename", "").toString()
+ localCover = jsobj.getString("localCover", "").toString()
+ localCoverUrl = jsobj.getString("localCoverUrl", "").toString()
+ }
}
- fun getCover(serverUrl:String, token:String):Uri {
- return Uri.parse("$serverUrl/${book.cover}?token=$token")
+ fun getCover():Uri {
+ if (isDownloaded) {
+// return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
+ return Uri.parse(localCoverUrl)
+ }
+ if (book.cover == "") return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
+ return Uri.parse("$serverUrl${book.cover}?token=$token&ts=${book.lastUpdate}")
+ }
+
+ fun getDurationLong():Long {
+ return duration.toLong() * 1000L
+ }
+
+ fun toMediaItem():MediaBrowserCompat.MediaItem {
+ var builder = MediaDescriptionCompat.Builder()
+ .setMediaId(id)
+ .setTitle(book.title)
+ .setSubtitle(book.authorFL)
+ .setMediaUri(null)
+ .setIconUri(getCover())
+
+ val extras = Bundle()
+ if (isDownloaded) {
+ extras.putLong(
+ MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
+ MediaDescriptionCompat.STATUS_DOWNLOADED)
+ }
+// extras.putInt(
+// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
+ builder.setExtras(extras)
+
+ var mediaDescription = builder.build()
+ return MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
+ }
+
+ fun toMediaMetadata():MediaMetadataCompat {
+ return MediaMetadataCompat.Builder().apply {
+ putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
+ putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, book.title)
+ putString(MediaMetadataCompat.METADATA_KEY_TITLE, book.title)
+ putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, book.authorFL)
+ putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCover().toString())
+ putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCover().toString())
+ putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCover().toString())
+ putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, book.authorFL)
+// val extras = Bundle()
+// if (isDownloaded) {
+// extras.putLong(
+// MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
+// MediaDescriptionCompat.STATUS_DOWNLOADED)
+// }
+// extras.putInt(
+// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
+
+// putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, RESOURCE_ROOT_URI +
+// context.resources.getResourceEntryName(R.drawable.notification_bg_low_normal))
+ }.build()
}
}
diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt
index 9793d134..ca0e8c56 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt
@@ -2,6 +2,7 @@ package com.audiobookshelf.app
import android.app.Activity
import android.content.Context
+import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat
@@ -9,17 +10,22 @@ import android.support.v4.media.MediaDescriptionCompat
import android.util.Log
import androidx.media.MediaBrowserServiceCompat
+import androidx.media.utils.MediaConstants
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite
import okhttp3.*
import java.io.IOException
-import java.net.URL
class AudiobookManager {
var tag = "AudiobookManager"
+ interface OnStreamData {
+ fun onStreamReady(asd:AudiobookStreamData)
+ }
+
var hasLoaded = false
+ var isLoading = false
var ctx: Context
var serverUrl = ""
var token = ""
@@ -40,9 +46,157 @@ class AudiobookManager {
Log.d(tag, "SHARED PREF TOKEN $token")
}
+ fun loadAudiobooks(cb: (() -> Unit)) {
+ if (serverUrl == "" || token == "") {
+ Log.d(tag, "No Server or Token set")
+ cb()
+ return
+ }
+
+ var url = "$serverUrl/api/library/main/audiobooks"
+ val request = Request.Builder()
+ .url(url).addHeader("Authorization", "Bearer $token")
+ .build()
+
+ client.newCall(request).enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ Log.d(tag, "FAILURE TO CONNECT")
+ e.printStackTrace()
+ cb()
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ response.use {
+ if (!response.isSuccessful) throw IOException("Unexpected code $response")
+
+ var bodyString = response.body!!.string()
+ var json = JSArray(bodyString)
+ var totalBooks = json.length() - 1
+ for (i in 0..totalBooks) {
+ var abobj = json.get(i)
+ var jsobj = JSObject(abobj.toString())
+ jsobj.put("isDownloaded", false)
+ var audiobook = Audiobook(jsobj, serverUrl, token)
+
+ if (audiobook.isMissing || audiobook.isInvalid) {
+ Log.d(tag, "Audiobook ${audiobook.book.title} is missing or invalid")
+ } else if (audiobook.numTracks <= 0) {
+ Log.d(tag, "Audiobook ${audiobook.book.title} has audio tracks")
+ } else {
+ var audiobookExists = audiobooks.find { it.id == audiobook.id }
+ if (audiobookExists == null) {
+ audiobooks.add(audiobook)
+ } else {
+ Log.d(tag, "Audiobook already there from downloaded")
+ }
+ }
+ }
+ Log.d(tag, "${audiobooks.size} Audiobooks Loaded")
+ cb()
+ }
+ }
+ })
+ }
+
fun fetchAudiobooks(result: MediaBrowserServiceCompat.Result>) {
var url = "$serverUrl/api/library/main/audiobooks"
- Log.d(tag, "RUNNING SAMPLER $url")
+ val request = Request.Builder()
+ .url(url).addHeader("Authorization", "Bearer $token")
+ .build()
+
+ client.newCall(request).enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ Log.d(tag, "FAILURE TO CONNECT")
+ e.printStackTrace()
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ response.use {
+ if (!response.isSuccessful) throw IOException("Unexpected code $response")
+
+ var bodyString = response.body!!.string()
+ var json = JSArray(bodyString)
+ var totalBooks = json.length() - 1
+ for (i in 0..totalBooks) {
+ var abobj = json.get(i)
+ var jsobj = JSObject(abobj.toString())
+ jsobj.put("isDownloaded", false)
+ var audiobook = Audiobook(jsobj, serverUrl, token)
+
+ if (audiobook.isMissing || audiobook.isInvalid) {
+ Log.d(tag, "Audiobook ${audiobook.book.title} is missing or invalid")
+ } else if (audiobook.numTracks <= 0) {
+ Log.d(tag, "Audiobook ${audiobook.book.title} has audio tracks")
+ } else {
+ var audiobookExists = audiobooks.find { it.id == audiobook.id }
+ if (audiobookExists == null) {
+ audiobooks.add(audiobook)
+ } else {
+ Log.d(tag, "Audiobook already there from downloaded")
+ }
+ }
+ }
+ Log.d(tag, "${audiobooks.size} Audiobooks Loaded")
+
+ val mediaItems: MutableList = mutableListOf()
+ audiobooks.forEach {
+ var builder = MediaDescriptionCompat.Builder()
+ .setMediaId(it.id)
+ .setTitle(it.book.title)
+ .setSubtitle(it.book.authorFL)
+ .setMediaUri(null)
+ .setIconUri(it.getCover())
+
+ val extras = Bundle()
+ if (it.isDownloaded) {
+ extras.putLong(
+ MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
+ MediaDescriptionCompat.STATUS_DOWNLOADED)
+ }
+// extras.putInt(
+// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
+ builder.setExtras(extras)
+
+ var mediaDescription = builder.build()
+ var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
+ mediaItems.add(newMediaItem)
+
+ }
+ Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Audiobooks")
+ result.sendResult(mediaItems)
+ }
+ }
+ })
+ }
+
+ fun load() {
+ isLoading = true
+ hasLoaded = true
+
+ var db = CapacitorDataStorageSqlite(ctx)
+ db.openStore("storage", "downloads", false, "no-encryption", 1)
+ var keyvalues = db.keysvalues()
+ keyvalues.forEach {
+ Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}")
+
+ var dlobj = JSObject(it.getString("value"))
+ var abobj = dlobj.getJSObject("audiobook")!!
+ abobj.put("isDownloaded", true)
+ abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString())
+ abobj.put("filename", dlobj.getString("filename", "").toString())
+ abobj.put("folderUrl", dlobj.getString("folderUrl", "").toString())
+ abobj.put("downloadFolderUrl", dlobj.getString("downloadFolderUrl", "").toString())
+ abobj.put("localCoverUrl", dlobj.getString("coverUrl", "").toString())
+ abobj.put("localCover", dlobj.getString("cover", "").toString())
+
+ var audiobook = Audiobook(abobj, serverUrl, token)
+ audiobooks.add(audiobook)
+ }
+ }
+
+ fun openStream(audiobook:Audiobook, streamListener:OnStreamData) {
+ var url = "$serverUrl/api/audiobook/${audiobook.id}/stream"
val request = Request.Builder()
.url(url).addHeader("Authorization", "Bearer $token")
.build()
@@ -55,51 +209,56 @@ class AudiobookManager {
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
-// for ((name, value) in response.headers) {
-// Log.d(tag, "HEADER $name: $value")
-// }
var bodyString = response.body!!.string()
- var json = JSArray(bodyString)
- var totalBooks = json.length() - 1
- for (i in 0..totalBooks) {
- var abobj = json.get(i)
- var jsobj = JSObject(abobj.toString())
- var audiobook = Audiobook(jsobj)
- audiobooks.add(audiobook)
- Log.d(tag, "Audiobook: ${audiobook.toString()}")
+ var stream = JSObject(bodyString)
+ var startTime = stream.getDouble("startTime")
+ var streamUrl = stream.getString("streamUrl", "").toString()
+
+ var startTimeLong = (startTime * 1000).toLong()
+
+ var abStreamDataObj = JSObject()
+ abStreamDataObj.put("id", audiobook.id)
+ abStreamDataObj.put("playlistUrl", "$serverUrl$streamUrl")
+ abStreamDataObj.put("title", audiobook.book.title)
+ abStreamDataObj.put("author", audiobook.book.authorFL)
+ abStreamDataObj.put("token", token)
+ abStreamDataObj.put("cover", audiobook.getCover())
+ abStreamDataObj.put("duration", audiobook.getDurationLong())
+ abStreamDataObj.put("startTime", startTimeLong)
+ abStreamDataObj.put("playbackSpeed", 1)
+ abStreamDataObj.put("playWhenReady", true)
+ abStreamDataObj.put("isLocal", false)
+
+ var audiobookStreamData = AudiobookStreamData(abStreamDataObj)
+
+ Handler(Looper.getMainLooper()).post() {
+ Log.d(tag, "Stream Ready on Main Looper")
+ streamListener.onStreamReady(audiobookStreamData)
}
- Log.d(tag, "Audiobooks Loaded")
- val mediaItems: MutableList = mutableListOf()
- audiobooks.forEach {
- var builder = MediaDescriptionCompat.Builder()
- .setMediaId(it.id)
- .setTitle(it.book.title)
- .setSubtitle(it.book.authorFL)
- .setMediaUri(it.fallbackUri)
- .setIconUri(it.fallbackCover)
-
- var mediaDescription = builder.build()
- var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
- mediaItems.add(newMediaItem)
-
- }
- Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Aduiobooks")
- result.sendResult(mediaItems)
+ Log.d(tag, "Init Player Stream")
}
}
})
}
- fun load() {
- hasLoaded = true
+ fun initLocalPlay(audiobook:Audiobook):AudiobookStreamData {
- var db = CapacitorDataStorageSqlite(ctx)
- db.openStore("storage", "downloads", false, "no-encryption", 1)
- Log.d(tag, "CHECK IF DB IS OPEN ${db.isStoreOpen("storage")}")
- var keyvalues = db.keysvalues()
- Log.d(tag, "KEY VALUES $keyvalues")
- keyvalues.forEach { Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}") }
+ var abStreamDataObj = JSObject()
+ abStreamDataObj.put("id", audiobook.id)
+ abStreamDataObj.put("contentUrl", audiobook.contentUrl)
+ abStreamDataObj.put("title", audiobook.book.title)
+ abStreamDataObj.put("author", audiobook.book.authorFL)
+ abStreamDataObj.put("token", null)
+ abStreamDataObj.put("cover", audiobook.getCover())
+ abStreamDataObj.put("duration", audiobook.getDurationLong())
+ abStreamDataObj.put("startTime", 0)
+ abStreamDataObj.put("playbackSpeed", 1)
+ abStreamDataObj.put("playWhenReady", true)
+ abStreamDataObj.put("isLocal", true)
+
+ var audiobookStreamData = AudiobookStreamData(abStreamDataObj)
+ return audiobookStreamData
}
}
diff --git a/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt b/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt
new file mode 100644
index 00000000..ebec88fc
--- /dev/null
+++ b/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt
@@ -0,0 +1,71 @@
+package com.audiobookshelf.app
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import android.support.v4.media.MediaMetadataCompat
+import android.util.Log
+import androidx.annotation.AnyRes
+
+
+class BrowseTree(
+ val context: Context,
+ val audiobooks: List,
+ val recentMediaId: String? = null
+) {
+ private val mediaIdToChildren = mutableMapOf>()
+
+ /**
+ * get uri to drawable or any other resource type if u wish
+ * @param context - context
+ * @param drawableId - drawable res id
+ * @return - uri
+ */
+ fun getUriToDrawable(context: Context,
+ @AnyRes drawableId: Int): Uri {
+ return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ + "://" + context.resources.getResourcePackageName(drawableId)
+ + '/' + context.resources.getResourceTypeName(drawableId)
+ + '/' + context.resources.getResourceEntryName(drawableId))
+ }
+
+ init {
+ val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
+
+ val allMetadata = MediaMetadataCompat.Builder().apply {
+ putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
+ putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Audiobooks")
+
+ var resource = getUriToDrawable(context, R.drawable.exo_icon_books).toString()
+ Log.d("BrowseTree", "RESOURCE $resource")
+ putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource)
+ }.build()
+
+ val albumsMetadata = MediaMetadataCompat.Builder().apply {
+ putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
+ putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
+ putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
+ }.build()
+
+ rootList += allMetadata
+ rootList += albumsMetadata
+ mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
+
+ audiobooks.forEach { audiobook ->
+ if (audiobook.isDownloaded) {
+ val downloadsChildren = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
+ downloadsChildren += audiobook.toMediaMetadata()
+ mediaIdToChildren[DOWNLOADS_ROOT] = downloadsChildren
+ }
+ val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
+ allChildren += audiobook.toMediaMetadata()
+ mediaIdToChildren[ALL_ROOT] = allChildren
+ }
+ }
+
+ operator fun get(mediaId: String) = mediaIdToChildren[mediaId]
+}
+
+const val AUTO_BROWSE_ROOT = "/"
+const val ALL_ROOT = "__ALL__"
+const val DOWNLOADS_ROOT = "__DOWNLOADS__"
diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
index e6589223..15fb4024 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
@@ -12,7 +12,6 @@ import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
@@ -54,9 +53,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onSleepTimerEnded(currentPosition:Long)
}
+
private val tag = "PlayerService"
- private lateinit var listener:MyCustomObjectListener
+ private var listener:MyCustomObjectListener? = null
+
private lateinit var ctx:Context
private lateinit var mPlayer: SimpleExoPlayer
private lateinit var mediaSessionConnector: MediaSessionConnector
@@ -266,51 +267,64 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
- val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
- override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
- Log.d(tag, "ON COMMAND $command")
- return false
- }
-
- override fun getSupportedPrepareActions(): Long {
- return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
- PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
- PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
- PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
- }
-
- override fun onPrepare(playWhenReady: Boolean) {
- Log.d(tag, "ON PREPARE $playWhenReady")
-
- var audiobook = audiobookManager.audiobooks[0]
- if (audiobook == null) {
- Log.e(tag, "Audiobook NOT FOUND")
- return
- }
- listener.onPrepare(audiobook.id, playWhenReady)
- }
-
- override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
- Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
- var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
- if (audiobook == null) {
- Log.e(tag, "Audiobook NOT FOUND")
- return
- }
- listener.onPrepare(audiobook.id, playWhenReady)
- }
-
- override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
- Log.d(tag, "ON PREPARE FROM SEARCH $query")
- }
-
- override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
- Log.d(tag, "ON PREPARE FROM URI $uri")
- }
-
- }
+// val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
+// override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
+// Log.d(tag, "ON COMMAND $command")
+// return false
+// }
+//
+// override fun getSupportedPrepareActions(): Long {
+// return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
+// PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
+// PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
+// PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
+// }
+//
+// override fun onPrepare(playWhenReady: Boolean) {
+// Log.d(tag, "ON PREPARE $playWhenReady")
+// var audiobook = audiobookManager.audiobooks[0]
+// if (audiobook == null) {
+// Log.e(tag, "Audiobook NOT FOUND")
+// return
+// }
+//
+// var streamListener = object : AudiobookManager.OnStreamData {
+// override fun onStreamReady(asd: AudiobookStreamData) {
+// Log.d(tag, "Stream Ready ${asd.playlistUrl}")
+// initPlayer(asd)
+// }
+// }
+// audiobookManager.openStream(audiobook, streamListener)
+// }
+//
+// override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
+// Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
+// var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
+// if (audiobook == null) {
+// Log.e(tag, "Audiobook NOT FOUND")
+// return
+// }
+//
+// var streamListener = object : AudiobookManager.OnStreamData {
+// override fun onStreamReady(asd: AudiobookStreamData) {
+// Log.d(tag, "Stream Ready ${asd.playlistUrl}")
+// initPlayer(asd)
+// }
+// }
+// audiobookManager.openStream(audiobook, streamListener)
+// }
+//
+// override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
+// Log.d(tag, "ON PREPARE FROM SEARCH $query")
+// }
+//
+// override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
+// Log.d(tag, "ON PREPARE FROM URI $uri")
+// }
+//
+// }
mediaSessionConnector.setQueueNavigator(queueNavigator)
- mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
+// mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
mediaSessionConnector.setPlayer(mPlayer)
//attach player to playerNotificationManager
@@ -318,12 +332,124 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS)
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
+ override fun onPrepare() {
+ Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
+ super.onPrepare()
+ }
+ override fun onPlay() {
+ Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
+ play()
+ }
+ override fun onPause() {
+ Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
+ pause()
+ }
+ override fun onStop() {
+ pause()
+ }
+ override fun onSkipToPrevious() {
+ seekBackward(seekAmount)
+ }
+ override fun onSkipToNext() {
+ seekForward(seekAmount)
+ }
+ override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
+ Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
+
+ var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
+ if (audiobook == null) {
+ Log.e(tag, "Audiobook NOT FOUND")
+ return
+ }
+
+ if (!audiobook.isDownloaded) {
+
+ var streamListener = object : AudiobookManager.OnStreamData {
+ override fun onStreamReady(asd: AudiobookStreamData) {
+ Log.d(tag, "Stream Ready ${asd.playlistUrl}")
+ initPlayer(asd)
+ }
+ }
+ audiobookManager.openStream(audiobook, streamListener)
+ } else {
+ var asd = audiobookManager.initLocalPlay(audiobook)
+ initPlayer(asd)
+ }
+ }
+
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return handleCallMediaButton(mediaButtonEvent)
}
})
}
+
+ fun handleCallMediaButton(intent: Intent): Boolean {
+ if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) {
+ var keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
+ if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
+ when (keyEvent?.getKeyCode()) {
+ KeyEvent.KEYCODE_HEADSETHOOK -> {
+ if(0 == mediaButtonClickCount) {
+ if (mPlayer.isPlaying)
+ pause()
+ else
+ play()
+ }
+ handleMediaButtonClickCount()
+ }
+ KeyEvent.KEYCODE_MEDIA_PLAY -> {
+ if(0 == mediaButtonClickCount) play()
+ handleMediaButtonClickCount()
+ }
+ KeyEvent.KEYCODE_MEDIA_PAUSE -> {
+ if(0 == mediaButtonClickCount) pause()
+ handleMediaButtonClickCount()
+ }
+ KeyEvent.KEYCODE_MEDIA_NEXT -> {
+ seekForward(seekAmount)
+ }
+ KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
+ seekBackward(seekAmount)
+ }
+ KeyEvent.KEYCODE_MEDIA_STOP -> {
+ terminateStream()
+ }
+ else -> {
+ Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
+ return false
+ }
+ }
+ }
+ }
+ return true
+ }
+
+ fun handleMediaButtonClickCount() {
+ mediaButtonClickCount++
+ if (1 == mediaButtonClickCount) {
+ Timer().schedule(mediaButtonClickTimeout) {
+ mediaBtnHandler.sendEmptyMessage(mediaButtonClickCount)
+ mediaButtonClickCount = 0
+ }
+ }
+ }
+
+ private val mediaBtnHandler : Handler = @SuppressLint("HandlerLeak")
+ object : Handler(){
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ if (2 == msg.what) {
+ seekBackward(seekAmount)
+ play()
+ }
+ else if (msg.what >= 3) {
+ seekForward(seekAmount)
+ play()
+ }
+ }
+ }
+
private inner class DescriptionAdapter(private val controller: MediaControllerCompat) :
PlayerNotificationManager.MediaDescriptionAdapter {
@@ -441,7 +567,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
else lastPauseTime = System.currentTimeMillis()
- if (listener != null) listener.onPlayingUpdate(player.isPlaying)
+ listener?.onPlayingUpdate(player.isPlaying)
}
}
})
@@ -452,7 +578,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
User callable methods
*/
-// fun initPlayer(token: String, playlistUri: String, playWhenReady: Boolean, currentTime: Long, title: String, artist: String, albumArt: String) {
fun initPlayer(audiobookStreamData: AudiobookStreamData) {
currentAudiobookStreamData = audiobookStreamData
@@ -585,14 +710,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
metadata.put("duration", duration)
metadata.put("currentTime", mPlayer.currentPosition)
metadata.put("stateName", stateName)
- if (listener != null) listener.onMetadata(metadata)
+ listener?.onMetadata(metadata)
}
//
// MEDIA BROWSER STUFF (ANDROID AUTO)
//
- private val MY_MEDIA_ROOT_ID = "audiobookshelf"
+ private val AUTO_MEDIA_ROOT = "/"
+ private lateinit var browseTree:BrowseTree
private fun isValid(packageName:String, uid:Int) : Boolean {
@@ -608,26 +734,58 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// No further calls will be made to other media browsing methods.
null
} else {
+
+ val maximumRootChildLimit = rootHints?.getInt(
+ MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
+ /* defaultValue= */ 4)
+// val supportedRootChildFlags = rootHints.getInt(
+// MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
+// /* defaultValue= */ android.media.browse.MediaBrowser.MediaItem.FLAG_BROWSABLE)
+
+
+
val extras = Bundle()
+ extras.putBoolean(
+ MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
- MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, extras)
+
+ BrowserRoot(AUTO_MEDIA_ROOT, extras)
}
}
override fun onLoadChildren(parentMediaId: String, result: Result>) {
val mediaItems: MutableList = mutableListOf()
+ Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
if (!audiobookManager.hasLoaded) {
Log.d(tag, "audiobook manager loading")
result.detach()
audiobookManager.load()
- audiobookManager.fetchAudiobooks(result)
+ audiobookManager.loadAudiobooks() {
+ audiobookManager.isLoading = false
+
+ Log.d(tag, "LOADED AUDIOBOOKS")
+ browseTree = BrowseTree(this, audiobookManager.audiobooks, null)
+ val children = browseTree[parentMediaId]?.map { item ->
+ MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
+ }
+ if (children != null) {
+ Log.d(tag, "BROWSE TREE CHILDREN ${children.size}")
+ }
+ result.sendResult(children as MutableList?)
+ }
return
+ } else if (audiobookManager.isLoading) {
+ Log.d(tag, "AUDIOBOOKS LOADING")
+ result.detach()
+ return
+ } else {
+ Log.d(tag, "ABs are loaded")
}
if (audiobookManager.audiobooks.size == 0) {
@@ -636,21 +794,34 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return
}
- audiobookManager.audiobooks.forEach {
- var builder = MediaDescriptionCompat.Builder()
- .setMediaId(it.id)
- .setTitle(it.book.title)
- .setSubtitle(it.book.authorFL)
- .setMediaUri(it.fallbackUri)
- .setIconUri(it.getCover(audiobookManager.serverUrl, audiobookManager.token))
- var mediaDescription = builder.build()
- var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
- mediaItems.add(newMediaItem)
+ var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
+
+ val children = browseTree[parentMediaId]?.map { item ->
+ MediaBrowserCompat.MediaItem(item.description, flag)
}
+ if (children != null) {
+ Log.d(tag, "BROWSE TREE $parentMediaId CHILDREN ${children.size}")
+ }
+ result.sendResult(children as MutableList?)
+
+// audiobookManager.audiobooks.forEach {
+// var builder = MediaDescriptionCompat.Builder()
+// .setMediaId(it.id)
+// .setTitle(it.book.title)
+// .setSubtitle(it.book.authorFL)
+// .setMediaUri(null)
+// .setIconUri(it.getCover(audiobookManager.serverUrl, audiobookManager.token))
+//
+//
+//
+// var mediaDescription = builder.build()
+// var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
+// mediaItems.add(newMediaItem)
+// }
// Check if this is the root menu:
- if (MY_MEDIA_ROOT_ID == parentMediaId) {
+ if (AUTO_MEDIA_ROOT == parentMediaId) {
// build the MediaItem objects for the top level,
// and put them in the mediaItems list
} else {
@@ -658,75 +829,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// examine the passed parentMediaId to see which submenu we're at,
// and put the children of that menu in the mediaItems list
}
- Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Aduiobooks")
- result.sendResult(mediaItems)
+// Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Aduiobooks")
+// result.sendResult(mediaItems)
}
- fun handleCallMediaButton(intent: Intent): Boolean {
- if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) {
- var keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
- if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
- when (keyEvent?.getKeyCode()) {
- KeyEvent.KEYCODE_HEADSETHOOK -> {
- if(0 == mediaButtonClickCount) {
- if (mPlayer.isPlaying)
- pause()
- else
- play()
- }
- handleMediaButtonClickCount()
- }
- KeyEvent.KEYCODE_MEDIA_PLAY -> {
- if(0 == mediaButtonClickCount) play()
- handleMediaButtonClickCount()
- }
- KeyEvent.KEYCODE_MEDIA_PAUSE -> {
- if(0 == mediaButtonClickCount) pause()
- handleMediaButtonClickCount()
- }
- KeyEvent.KEYCODE_MEDIA_NEXT -> {
- seekForward(seekAmount)
- }
- KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
- seekBackward(seekAmount)
- }
- KeyEvent.KEYCODE_MEDIA_STOP -> {
- terminateStream()
- }
- else -> {
- Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
- return false
- }
- }
- }
- }
- return true
- }
-
- fun handleMediaButtonClickCount() {
- mediaButtonClickCount++
- if (1 == mediaButtonClickCount) {
- Timer().schedule(mediaButtonClickTimeout) {
- handler.sendEmptyMessage(mediaButtonClickCount)
- mediaButtonClickCount = 0
- }
- }
- }
-
- private val handler : Handler = @SuppressLint("HandlerLeak")
- object : Handler(){
- override fun handleMessage(msg: Message) {
- super.handleMessage(msg)
- if (2 == msg.what) {
- seekBackward(seekAmount)
- play()
- }
- else if (msg.what >= 3) {
- seekForward(seekAmount)
- play()
- }
- }
- }
+ //
+ // SLEEP TIMER STUFF
+ //
fun setSleepTimer(time:Long, isChapterTime:Boolean) : Boolean {
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
@@ -749,7 +858,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
mPlayer.pause()
- if (listener != null) listener.onSleepTimerEnded(mPlayer.currentPosition)
+ listener?.onSleepTimerEnded(mPlayer.currentPosition)
sleepTimerTask?.cancel()
}
}
@@ -762,7 +871,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "Sleep Timer Pausing Player")
mPlayer.pause()
}
- if (listener != null) listener.onSleepTimerEnded(mPlayer.currentPosition)
+ listener?.onSleepTimerEnded(mPlayer.currentPosition)
}
}
}
diff --git a/android/app/src/main/res/drawable-anydpi-v24/exo_icon_books.xml b/android/app/src/main/res/drawable-anydpi-v24/exo_icon_books.xml
new file mode 100644
index 00000000..0072c6e5
--- /dev/null
+++ b/android/app/src/main/res/drawable-anydpi-v24/exo_icon_books.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable-anydpi-v24/exo_icon_downloaddone.xml b/android/app/src/main/res/drawable-anydpi-v24/exo_icon_downloaddone.xml
new file mode 100644
index 00000000..0c3acf89
--- /dev/null
+++ b/android/app/src/main/res/drawable-anydpi-v24/exo_icon_downloaddone.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable-hdpi/exo_icon_books.png b/android/app/src/main/res/drawable-hdpi/exo_icon_books.png
new file mode 100644
index 00000000..5e15a4a9
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/exo_icon_books.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/exo_icon_downloaddone.png b/android/app/src/main/res/drawable-hdpi/exo_icon_downloaddone.png
new file mode 100644
index 00000000..f7b92c79
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/exo_icon_downloaddone.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/exo_icon_books.png b/android/app/src/main/res/drawable-mdpi/exo_icon_books.png
new file mode 100644
index 00000000..3dbd2199
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/exo_icon_books.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/exo_icon_downloaddone.png b/android/app/src/main/res/drawable-mdpi/exo_icon_downloaddone.png
new file mode 100644
index 00000000..b1242a00
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/exo_icon_downloaddone.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/exo_icon_books.png b/android/app/src/main/res/drawable-xhdpi/exo_icon_books.png
new file mode 100644
index 00000000..f432c030
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/exo_icon_books.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/exo_icon_downloaddone.png b/android/app/src/main/res/drawable-xhdpi/exo_icon_downloaddone.png
new file mode 100644
index 00000000..a89dc73f
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/exo_icon_downloaddone.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/exo_icon_books.png b/android/app/src/main/res/drawable-xxhdpi/exo_icon_books.png
new file mode 100644
index 00000000..213a80e8
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/exo_icon_books.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/exo_icon_downloaddone.png b/android/app/src/main/res/drawable-xxhdpi/exo_icon_downloaddone.png
new file mode 100644
index 00000000..5499dfae
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/exo_icon_downloaddone.png differ
diff --git a/package.json b/package.json
index f5427f10..2f2e5666 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
- "version": "v0.9.18-beta",
+ "version": "v0.9.19-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname localhost --port 1337",