Add: Android auto first attempt
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 37
|
versionCode 38
|
||||||
versionName "0.9.18-beta"
|
versionName "0.9.19-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -36,23 +36,10 @@
|
|||||||
<!--Used by Android Auto-->
|
<!--Used by Android Auto-->
|
||||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||||
android:resource="@drawable/icon" />
|
android:resource="@drawable/icon" />
|
||||||
|
|
||||||
<!-- Android auto rejected the update, removing this so it can be published on the store -->
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.car.application"
|
android:name="com.google.android.gms.car.application"
|
||||||
android:resource="@xml/automotive_app_desc"/>
|
android:resource="@xml/automotive_app_desc"/>
|
||||||
|
|
||||||
<!-- TODO: Can remove in future -->
|
|
||||||
<!-- <provider-->
|
|
||||||
<!-- android:name="androidx.core.content.FileProvider"-->
|
|
||||||
<!-- android:authorities="${applicationId}.fileprovider"-->
|
|
||||||
<!-- android:exported="false"-->
|
|
||||||
<!-- android:grantUriPermissions="true">-->
|
|
||||||
<!-- <meta-data-->
|
|
||||||
<!-- android:name="android.support.FILE_PROVIDER_PATHS"-->
|
|
||||||
<!-- android:resource="@xml/file_paths" />-->
|
|
||||||
<!-- </provider>-->
|
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.net.Uri
|
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
|
import com.getcapacitor.JSObject
|
||||||
|
|
||||||
class Audiobook {
|
class Audiobook {
|
||||||
@@ -16,10 +20,21 @@ class Audiobook {
|
|||||||
var isInvalid:Boolean
|
var isInvalid:Boolean
|
||||||
var path:String
|
var path:String
|
||||||
|
|
||||||
var fallbackCover:Uri
|
var isDownloaded:Boolean = false
|
||||||
var fallbackUri:Uri
|
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()
|
id = jsobj.getString("id", "").toString()
|
||||||
ino = jsobj.getString("ino", "").toString()
|
ino = jsobj.getString("ino", "").toString()
|
||||||
libraryId = jsobj.getString("libraryId", "").toString()
|
libraryId = jsobj.getString("libraryId", "").toString()
|
||||||
@@ -35,11 +50,75 @@ class Audiobook {
|
|||||||
isInvalid = jsobj.getBoolean("isInvalid")
|
isInvalid = jsobj.getBoolean("isInvalid")
|
||||||
path = jsobj.getString("path", "").toString()
|
path = jsobj.getString("path", "").toString()
|
||||||
|
|
||||||
fallbackUri = Uri.parse("http://fallback.com/run.mp3")
|
isDownloaded = jsobj.getBoolean("isDownloaded")
|
||||||
fallbackCover = Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
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 {
|
fun getCover():Uri {
|
||||||
return Uri.parse("$serverUrl/${book.cover}?token=$token")
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.audiobookshelf.app
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
@@ -9,17 +10,22 @@ import android.support.v4.media.MediaDescriptionCompat
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
|
import androidx.media.utils.MediaConstants
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite
|
import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class AudiobookManager {
|
class AudiobookManager {
|
||||||
var tag = "AudiobookManager"
|
var tag = "AudiobookManager"
|
||||||
|
|
||||||
|
interface OnStreamData {
|
||||||
|
fun onStreamReady(asd:AudiobookStreamData)
|
||||||
|
}
|
||||||
|
|
||||||
var hasLoaded = false
|
var hasLoaded = false
|
||||||
|
var isLoading = false
|
||||||
var ctx: Context
|
var ctx: Context
|
||||||
var serverUrl = ""
|
var serverUrl = ""
|
||||||
var token = ""
|
var token = ""
|
||||||
@@ -40,9 +46,157 @@ class AudiobookManager {
|
|||||||
Log.d(tag, "SHARED PREF TOKEN $token")
|
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<MutableList<MediaBrowserCompat.MediaItem>>) {
|
fun fetchAudiobooks(result: MediaBrowserServiceCompat.Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
var url = "$serverUrl/api/library/main/audiobooks"
|
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<MediaBrowserCompat.MediaItem> = 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()
|
val request = Request.Builder()
|
||||||
.url(url).addHeader("Authorization", "Bearer $token")
|
.url(url).addHeader("Authorization", "Bearer $token")
|
||||||
.build()
|
.build()
|
||||||
@@ -55,51 +209,56 @@ class AudiobookManager {
|
|||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
response.use {
|
response.use {
|
||||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
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 bodyString = response.body!!.string()
|
||||||
var json = JSArray(bodyString)
|
var stream = JSObject(bodyString)
|
||||||
var totalBooks = json.length() - 1
|
var startTime = stream.getDouble("startTime")
|
||||||
for (i in 0..totalBooks) {
|
var streamUrl = stream.getString("streamUrl", "").toString()
|
||||||
var abobj = json.get(i)
|
|
||||||
var jsobj = JSObject(abobj.toString())
|
var startTimeLong = (startTime * 1000).toLong()
|
||||||
var audiobook = Audiobook(jsobj)
|
|
||||||
audiobooks.add(audiobook)
|
var abStreamDataObj = JSObject()
|
||||||
Log.d(tag, "Audiobook: ${audiobook.toString()}")
|
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<MediaBrowserCompat.MediaItem> = mutableListOf()
|
Log.d(tag, "Init Player Stream")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun initLocalPlay(audiobook:Audiobook):AudiobookStreamData {
|
||||||
hasLoaded = true
|
|
||||||
|
|
||||||
var db = CapacitorDataStorageSqlite(ctx)
|
var abStreamDataObj = JSObject()
|
||||||
db.openStore("storage", "downloads", false, "no-encryption", 1)
|
abStreamDataObj.put("id", audiobook.id)
|
||||||
Log.d(tag, "CHECK IF DB IS OPEN ${db.isStoreOpen("storage")}")
|
abStreamDataObj.put("contentUrl", audiobook.contentUrl)
|
||||||
var keyvalues = db.keysvalues()
|
abStreamDataObj.put("title", audiobook.book.title)
|
||||||
Log.d(tag, "KEY VALUES $keyvalues")
|
abStreamDataObj.put("author", audiobook.book.authorFL)
|
||||||
keyvalues.forEach { Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}") }
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Audiobook>,
|
||||||
|
val recentMediaId: String? = null
|
||||||
|
) {
|
||||||
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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__"
|
||||||
@@ -12,7 +12,6 @@ import android.support.v4.media.MediaDescriptionCompat
|
|||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -54,9 +53,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onSleepTimerEnded(currentPosition:Long)
|
fun onSleepTimerEnded(currentPosition:Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val tag = "PlayerService"
|
private val tag = "PlayerService"
|
||||||
|
|
||||||
private lateinit var listener:MyCustomObjectListener
|
private var listener:MyCustomObjectListener? = null
|
||||||
|
|
||||||
private lateinit var ctx:Context
|
private lateinit var ctx:Context
|
||||||
private lateinit var mPlayer: SimpleExoPlayer
|
private lateinit var mPlayer: SimpleExoPlayer
|
||||||
private lateinit var mediaSessionConnector: MediaSessionConnector
|
private lateinit var mediaSessionConnector: MediaSessionConnector
|
||||||
@@ -266,51 +267,64 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
|
// val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
|
||||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
// override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||||
Log.d(tag, "ON COMMAND $command")
|
// Log.d(tag, "ON COMMAND $command")
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override fun getSupportedPrepareActions(): Long {
|
// override fun getSupportedPrepareActions(): Long {
|
||||||
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
// return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
||||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
// PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||||
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
|
// PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
|
||||||
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
// PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override fun onPrepare(playWhenReady: Boolean) {
|
// override fun onPrepare(playWhenReady: Boolean) {
|
||||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
// Log.d(tag, "ON PREPARE $playWhenReady")
|
||||||
|
// var audiobook = audiobookManager.audiobooks[0]
|
||||||
var audiobook = audiobookManager.audiobooks[0]
|
// if (audiobook == null) {
|
||||||
if (audiobook == null) {
|
// Log.e(tag, "Audiobook NOT FOUND")
|
||||||
Log.e(tag, "Audiobook NOT FOUND")
|
// return
|
||||||
return
|
// }
|
||||||
}
|
//
|
||||||
listener.onPrepare(audiobook.id, playWhenReady)
|
// var streamListener = object : AudiobookManager.OnStreamData {
|
||||||
}
|
// override fun onStreamReady(asd: AudiobookStreamData) {
|
||||||
|
// Log.d(tag, "Stream Ready ${asd.playlistUrl}")
|
||||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
// initPlayer(asd)
|
||||||
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
// }
|
||||||
var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
|
// }
|
||||||
if (audiobook == null) {
|
// audiobookManager.openStream(audiobook, streamListener)
|
||||||
Log.e(tag, "Audiobook NOT FOUND")
|
// }
|
||||||
return
|
//
|
||||||
}
|
// override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
listener.onPrepare(audiobook.id, playWhenReady)
|
// Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
||||||
}
|
// var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
|
||||||
|
// if (audiobook == null) {
|
||||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
// Log.e(tag, "Audiobook NOT FOUND")
|
||||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
|
// var streamListener = object : AudiobookManager.OnStreamData {
|
||||||
Log.d(tag, "ON PREPARE FROM URI $uri")
|
// 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.setQueueNavigator(queueNavigator)
|
||||||
mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
|
// mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
|
||||||
mediaSessionConnector.setPlayer(mPlayer)
|
mediaSessionConnector.setPlayer(mPlayer)
|
||||||
|
|
||||||
//attach player to playerNotificationManager
|
//attach player to playerNotificationManager
|
||||||
@@ -318,12 +332,124 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS)
|
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS)
|
||||||
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
|
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 {
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
||||||
return handleCallMediaButton(mediaButtonEvent)
|
return handleCallMediaButton(mediaButtonEvent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun handleCallMediaButton(intent: Intent): Boolean {
|
||||||
|
if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) {
|
||||||
|
var keyEvent = intent.getParcelableExtra<KeyEvent>(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) :
|
private inner class DescriptionAdapter(private val controller: MediaControllerCompat) :
|
||||||
PlayerNotificationManager.MediaDescriptionAdapter {
|
PlayerNotificationManager.MediaDescriptionAdapter {
|
||||||
|
|
||||||
@@ -441,7 +567,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else lastPauseTime = System.currentTimeMillis()
|
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
|
User callable methods
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// fun initPlayer(token: String, playlistUri: String, playWhenReady: Boolean, currentTime: Long, title: String, artist: String, albumArt: String) {
|
|
||||||
fun initPlayer(audiobookStreamData: AudiobookStreamData) {
|
fun initPlayer(audiobookStreamData: AudiobookStreamData) {
|
||||||
currentAudiobookStreamData = audiobookStreamData
|
currentAudiobookStreamData = audiobookStreamData
|
||||||
|
|
||||||
@@ -585,14 +710,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
metadata.put("duration", duration)
|
metadata.put("duration", duration)
|
||||||
metadata.put("currentTime", mPlayer.currentPosition)
|
metadata.put("currentTime", mPlayer.currentPosition)
|
||||||
metadata.put("stateName", stateName)
|
metadata.put("stateName", stateName)
|
||||||
if (listener != null) listener.onMetadata(metadata)
|
listener?.onMetadata(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// 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 {
|
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.
|
// No further calls will be made to other media browsing methods.
|
||||||
null
|
null
|
||||||
} else {
|
} 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()
|
val extras = Bundle()
|
||||||
|
extras.putBoolean(
|
||||||
|
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
||||||
extras.putInt(
|
extras.putInt(
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||||
extras.putInt(
|
extras.putInt(
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
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<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
||||||
|
|
||||||
if (!audiobookManager.hasLoaded) {
|
if (!audiobookManager.hasLoaded) {
|
||||||
Log.d(tag, "audiobook manager loading")
|
Log.d(tag, "audiobook manager loading")
|
||||||
result.detach()
|
result.detach()
|
||||||
audiobookManager.load()
|
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<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
return
|
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) {
|
if (audiobookManager.audiobooks.size == 0) {
|
||||||
@@ -636,21 +794,34 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return
|
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 flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
mediaItems.add(newMediaItem)
|
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<MediaBrowserCompat.MediaItem>?)
|
||||||
|
|
||||||
|
// 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:
|
// 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,
|
// build the MediaItem objects for the top level,
|
||||||
// and put them in the mediaItems list
|
// and put them in the mediaItems list
|
||||||
} else {
|
} else {
|
||||||
@@ -658,75 +829,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// examine the passed parentMediaId to see which submenu we're at,
|
// examine the passed parentMediaId to see which submenu we're at,
|
||||||
// and put the children of that menu in the mediaItems list
|
// and put the children of that menu in the mediaItems list
|
||||||
}
|
}
|
||||||
Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Aduiobooks")
|
// Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Aduiobooks")
|
||||||
result.sendResult(mediaItems)
|
// result.sendResult(mediaItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleCallMediaButton(intent: Intent): Boolean {
|
//
|
||||||
if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) {
|
// SLEEP TIMER STUFF
|
||||||
var keyEvent = intent.getParcelableExtra<KeyEvent>(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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSleepTimer(time:Long, isChapterTime:Boolean) : Boolean {
|
fun setSleepTimer(time:Long, isChapterTime:Boolean) : Boolean {
|
||||||
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
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")
|
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
||||||
mPlayer.pause()
|
mPlayer.pause()
|
||||||
|
|
||||||
if (listener != null) listener.onSleepTimerEnded(mPlayer.currentPosition)
|
listener?.onSleepTimerEnded(mPlayer.currentPosition)
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,7 +871,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
Log.d(tag, "Sleep Timer Pausing Player")
|
Log.d(tag, "Sleep Timer Pausing Player")
|
||||||
mPlayer.pause()
|
mPlayer.pause()
|
||||||
}
|
}
|
||||||
if (listener != null) listener.onSleepTimerEnded(mPlayer.currentPosition)
|
listener?.onSleepTimerEnded(mPlayer.currentPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.127451"
|
||||||
|
android:scaleY="1.127451"
|
||||||
|
android:translateX="-1.5294118"
|
||||||
|
android:translateY="-1.5294118">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.127451"
|
||||||
|
android:scaleY="1.127451"
|
||||||
|
android:translateX="-1.5294118"
|
||||||
|
android:translateY="-1.5294118">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20.13,5.41l-1.41,-1.41l-9.19,9.19l-4.25,-4.24l-1.41,1.41l5.66,5.66z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5,18h14v2h-14z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 215 B |
|
After Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 176 B |
|
After Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 448 B |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.18-beta",
|
"version": "v0.9.19-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|||||||