mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-29 22:29:29 +02:00
Add:Native progress sync for android auto streaming #41, Change:Android auto only show samples when no audiobooks
This commit is contained in:
parent
1a7f90c93b
commit
f59e6521fc
12 changed files with 371 additions and 124 deletions
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
|
@ -105,13 +106,13 @@ class AudiobookManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAudiobooks(cb: (() -> Unit)) {
|
fun loadAudiobooks(cb: (() -> Unit)) {
|
||||||
Log.d(tag, "LOAD AUDIBOOOSK $serverUrl | $token")
|
Log.d(tag, "Load Audiobooks: $serverUrl | $token")
|
||||||
if (serverUrl == "" || token == "") {
|
if (serverUrl == "" || token == "") {
|
||||||
Log.d(tag, "No Server or Token set")
|
Log.d(tag, "Load Audiobooks: No Server or Token set")
|
||||||
cb()
|
cb()
|
||||||
return
|
return
|
||||||
} else if (!serverUrl.startsWith("http")) {
|
} else if (!serverUrl.startsWith("http")) {
|
||||||
Log.e(tag, "Invalid server url $serverUrl")
|
Log.e(tag, "Load Audiobooks: Invalid server url $serverUrl")
|
||||||
cb()
|
cb()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -119,14 +120,14 @@ class AudiobookManager {
|
||||||
// First load currently reading
|
// First load currently reading
|
||||||
loadCategories() {
|
loadCategories() {
|
||||||
// Then load all
|
// Then load all
|
||||||
var url = "$serverUrl/api/libraries/main/books/all"
|
var url = "$serverUrl/api/libraries/main/books/all?sort=book.title"
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url).addHeader("Authorization", "Bearer $token")
|
.url(url).addHeader("Authorization", "Bearer $token")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Log.d(tag, "FAILURE TO CONNECT")
|
Log.d(tag, "Load Audiobooks: FAILURE TO CONNECT")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
@ -174,6 +175,7 @@ class AudiobookManager {
|
||||||
|
|
||||||
localMediaManager.loadLocalAudio()
|
localMediaManager.loadLocalAudio()
|
||||||
|
|
||||||
|
// Load downloads from sql db
|
||||||
var db = CapacitorDataStorageSqlite(ctx)
|
var db = CapacitorDataStorageSqlite(ctx)
|
||||||
db.openStore("storage", "downloads", false, "no-encryption", 1)
|
db.openStore("storage", "downloads", false, "no-encryption", 1)
|
||||||
var keyvalues = db.keysvalues()
|
var keyvalues = db.keysvalues()
|
||||||
|
@ -214,13 +216,15 @@ class AudiobookManager {
|
||||||
|
|
||||||
var bodyString = response.body!!.string()
|
var bodyString = response.body!!.string()
|
||||||
var stream = JSObject(bodyString)
|
var stream = JSObject(bodyString)
|
||||||
|
var streamId = stream.getString("streamId", "").toString()
|
||||||
var startTime = stream.getDouble("startTime")
|
var startTime = stream.getDouble("startTime")
|
||||||
var streamUrl = stream.getString("streamUrl", "").toString()
|
var streamUrl = stream.getString("streamUrl", "").toString()
|
||||||
|
|
||||||
var startTimeLong = (startTime * 1000).toLong()
|
var startTimeLong = (startTime * 1000).toLong()
|
||||||
|
|
||||||
var abStreamDataObj = JSObject()
|
var abStreamDataObj = JSObject()
|
||||||
abStreamDataObj.put("id", audiobook.id)
|
abStreamDataObj.put("id", streamId)
|
||||||
|
abStreamDataObj.put("audiobookId", audiobook.id)
|
||||||
abStreamDataObj.put("playlistUrl", "$serverUrl$streamUrl")
|
abStreamDataObj.put("playlistUrl", "$serverUrl$streamUrl")
|
||||||
abStreamDataObj.put("title", audiobook.book.title)
|
abStreamDataObj.put("title", audiobook.book.title)
|
||||||
abStreamDataObj.put("author", audiobook.book.authorFL)
|
abStreamDataObj.put("author", audiobook.book.authorFL)
|
||||||
|
@ -247,7 +251,8 @@ class AudiobookManager {
|
||||||
|
|
||||||
fun initDownloadPlay(audiobook:Audiobook):AudiobookStreamData {
|
fun initDownloadPlay(audiobook:Audiobook):AudiobookStreamData {
|
||||||
var abStreamDataObj = JSObject()
|
var abStreamDataObj = JSObject()
|
||||||
abStreamDataObj.put("id", audiobook.id)
|
abStreamDataObj.put("id", "download")
|
||||||
|
abStreamDataObj.put("audiobookId", audiobook.id)
|
||||||
abStreamDataObj.put("contentUrl", audiobook.contentUrl)
|
abStreamDataObj.put("contentUrl", audiobook.contentUrl)
|
||||||
abStreamDataObj.put("title", audiobook.book.title)
|
abStreamDataObj.put("title", audiobook.book.title)
|
||||||
abStreamDataObj.put("author", audiobook.book.authorFL)
|
abStreamDataObj.put("author", audiobook.book.authorFL)
|
||||||
|
@ -265,7 +270,8 @@ class AudiobookManager {
|
||||||
|
|
||||||
fun initLocalPlay(local: LocalMediaManager.LocalAudio):AudiobookStreamData {
|
fun initLocalPlay(local: LocalMediaManager.LocalAudio):AudiobookStreamData {
|
||||||
var abStreamDataObj = JSObject()
|
var abStreamDataObj = JSObject()
|
||||||
abStreamDataObj.put("id", local.id)
|
abStreamDataObj.put("id", "local")
|
||||||
|
abStreamDataObj.put("audiobookId", local.id)
|
||||||
abStreamDataObj.put("contentUrl", local.uri.toString())
|
abStreamDataObj.put("contentUrl", local.uri.toString())
|
||||||
abStreamDataObj.put("title", local.name)
|
abStreamDataObj.put("title", local.name)
|
||||||
abStreamDataObj.put("author", "")
|
abStreamDataObj.put("author", "")
|
||||||
|
@ -339,4 +345,25 @@ class AudiobookManager {
|
||||||
if (localMediaManager.localAudioFiles.isEmpty()) return null
|
if (localMediaManager.localAudioFiles.isEmpty()) return null
|
||||||
return localMediaManager.localAudioFiles[0]
|
return localMediaManager.localAudioFiles[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for media browser loadChildren, fallback to using the samples if no audiobooks are there
|
||||||
|
fun getAudiobooksMediaMetadata() : List<MediaMetadataCompat> {
|
||||||
|
var mediaMetadata:MutableList<MediaMetadataCompat> = mutableListOf()
|
||||||
|
if (audiobooks.isEmpty()) {
|
||||||
|
localMediaManager.localAudioFiles.forEach { mediaMetadata.add(it.toMediaMetadata()) }
|
||||||
|
} else {
|
||||||
|
audiobooks.forEach { mediaMetadata.add(it.toMediaMetadata()) }
|
||||||
|
}
|
||||||
|
return mediaMetadata
|
||||||
|
}
|
||||||
|
// Used for media browser loadChildren, fallback to using the samples if no audiobooks are there
|
||||||
|
fun getDownloadedAudiobooksMediaMetadata() : List<MediaMetadataCompat> {
|
||||||
|
var mediaMetadata:MutableList<MediaMetadataCompat> = mutableListOf()
|
||||||
|
if (audiobooks.isEmpty()) {
|
||||||
|
localMediaManager.localAudioFiles.forEach { mediaMetadata.add(it.toMediaMetadata()) }
|
||||||
|
} else {
|
||||||
|
audiobooks.forEach { if (it.isDownloaded) { mediaMetadata.add(it.toMediaMetadata()) } }
|
||||||
|
}
|
||||||
|
return mediaMetadata
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import com.getcapacitor.JSObject
|
||||||
|
import okhttp3.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Normal progress sync is handled in webview, but when using android auto webview may not be open.
|
||||||
|
* If webview is not open sync progress every 5s. Webview can be closed at any time so interval is always set.
|
||||||
|
*/
|
||||||
|
class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotificationService, client: OkHttpClient) {
|
||||||
|
private val tag = "AudiobookProgressSync"
|
||||||
|
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
||||||
|
private val client:OkHttpClient = client
|
||||||
|
|
||||||
|
private var listeningTimerTask: TimerTask? = null
|
||||||
|
var listeningTimerRunning:Boolean = false
|
||||||
|
|
||||||
|
private var webviewOpenOnStart:Boolean = false
|
||||||
|
private var listeningBookTitle:String? = ""
|
||||||
|
private var listeningBookIsLocal:Boolean = false
|
||||||
|
private var listeningBookId:String? = ""
|
||||||
|
private var listeningStreamId:String? = ""
|
||||||
|
|
||||||
|
private var lastPlaybackTime:Long = 0
|
||||||
|
private var lastUpdateTime:Long = 0
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
if (listeningTimerRunning) {
|
||||||
|
Log.d(tag, "start: Timer already running for $listeningBookTitle")
|
||||||
|
if (playerNotificationService.getCurrentBookTitle() != listeningBookTitle) {
|
||||||
|
Log.d(tag, "start: Changed audiobook stream - resetting timer")
|
||||||
|
listeningTimerTask?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listeningTimerRunning = true
|
||||||
|
|
||||||
|
webviewOpenOnStart = playerNotificationService.getIsWebviewOpen()
|
||||||
|
listeningBookTitle = playerNotificationService.getCurrentBookTitle()
|
||||||
|
listeningBookIsLocal = playerNotificationService.getCurrentBookIsLocal()
|
||||||
|
listeningBookId = playerNotificationService.getCurrentBookId()
|
||||||
|
listeningStreamId = playerNotificationService.getCurrentStreamId()
|
||||||
|
|
||||||
|
lastPlaybackTime = playerNotificationService.getCurrentTime()
|
||||||
|
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||||
|
|
||||||
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||||
|
Handler(Looper.getMainLooper()).post() {
|
||||||
|
// Webview was closed while android auto is open - switch to native sync
|
||||||
|
if (!playerNotificationService.getIsWebviewOpen() && webviewOpenOnStart) {
|
||||||
|
Log.d(tag, "Listening Timer: webview closed Switching to native sync tracking")
|
||||||
|
webviewOpenOnStart = false
|
||||||
|
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||||
|
}
|
||||||
|
if (!webviewOpenOnStart && playerNotificationService.currentPlayer.isPlaying) {
|
||||||
|
sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
if (!listeningTimerRunning) return
|
||||||
|
Log.d(tag, "stop: Stopping listening for $listeningBookTitle")
|
||||||
|
|
||||||
|
reset()
|
||||||
|
sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
listeningTimerTask?.cancel()
|
||||||
|
listeningTimerTask = null
|
||||||
|
listeningTimerRunning = false
|
||||||
|
listeningBookTitle = ""
|
||||||
|
listeningBookId = ""
|
||||||
|
listeningBookIsLocal = false
|
||||||
|
listeningStreamId = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sync() {
|
||||||
|
var currTime = System.currentTimeMillis() / 1000L
|
||||||
|
var elapsed = currTime - lastUpdateTime
|
||||||
|
lastUpdateTime = currTime
|
||||||
|
|
||||||
|
if (!listeningBookIsLocal) {
|
||||||
|
Log.d(tag, "ListeningTimer: Sending sync data to server: elapsed $elapsed | $listeningStreamId | $listeningBookId")
|
||||||
|
|
||||||
|
// Send sync data only for streaming books
|
||||||
|
var syncData: JSObject = JSObject()
|
||||||
|
syncData.put("timeListened", elapsed)
|
||||||
|
syncData.put("currentTime", playerNotificationService.getCurrentTime())
|
||||||
|
syncData.put("streamId", listeningStreamId)
|
||||||
|
syncData.put("audiobookId", listeningBookId)
|
||||||
|
sendStreamSyncData(syncData) {
|
||||||
|
Log.d(tag, "Stream sync done")
|
||||||
|
}
|
||||||
|
} else if (listeningStreamId == "download") {
|
||||||
|
// TODO: Save downloaded audiobook progress & send to server if connected
|
||||||
|
Log.d(tag, "ListeningTimer: Is listening download")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendStreamSyncData(payload:JSObject, cb: (() -> Unit)) {
|
||||||
|
var serverUrl = playerNotificationService.getServerUrl()
|
||||||
|
var token = playerNotificationService.getUserToken()
|
||||||
|
|
||||||
|
Log.d(tag, "Sync Stream $serverUrl | $token")
|
||||||
|
var url = "$serverUrl/api/syncStream"
|
||||||
|
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
val requestBody = payload.toString().toRequestBody(mediaType)
|
||||||
|
val request = Request.Builder().post(requestBody)
|
||||||
|
.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")
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,8 @@ import com.google.android.exoplayer2.util.MimeTypes
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
|
||||||
class AudiobookStreamData {
|
class AudiobookStreamData {
|
||||||
var id:String = "audiobook"
|
var id:String = "unset"
|
||||||
|
var audiobookId:String = ""
|
||||||
var token:String = ""
|
var token:String = ""
|
||||||
var playlistUrl:String = ""
|
var playlistUrl:String = ""
|
||||||
var title:String = "No Title"
|
var title:String = "No Title"
|
||||||
|
@ -33,7 +34,8 @@ class AudiobookStreamData {
|
||||||
var contentUri:Uri = Uri.EMPTY // For Local only
|
var contentUri:Uri = Uri.EMPTY // For Local only
|
||||||
|
|
||||||
constructor(jsondata:JSObject) {
|
constructor(jsondata:JSObject) {
|
||||||
id = jsondata.getString("id", "audiobook").toString()
|
id = jsondata.getString("id", "unset").toString()
|
||||||
|
audiobookId = jsondata.getString("audiobookId", "").toString()
|
||||||
title = jsondata.getString("title", "No Title").toString()
|
title = jsondata.getString("title", "No Title").toString()
|
||||||
token = jsondata.getString("token", "").toString()
|
token = jsondata.getString("token", "").toString()
|
||||||
author = jsondata.getString("author", "Unknown").toString()
|
author = jsondata.getString("author", "Unknown").toString()
|
||||||
|
|
|
@ -10,10 +10,9 @@ import androidx.annotation.AnyRes
|
||||||
|
|
||||||
class BrowseTree(
|
class BrowseTree(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val audiobooksInProgress: List<Audiobook>,
|
audiobooksInProgress: List<Audiobook>,
|
||||||
val audiobooks: List<Audiobook>,
|
audiobookMetadata: List<MediaMetadataCompat>,
|
||||||
val localAudio: List<LocalMediaManager.LocalAudio>,
|
downloadedMetadata: List<MediaMetadataCompat>
|
||||||
val recentMediaId: String? = null
|
|
||||||
) {
|
) {
|
||||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
|
|
||||||
|
@ -57,18 +56,18 @@ class BrowseTree(
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val localsMetadata = MediaMetadataCompat.Builder().apply {
|
// val localsMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LOCAL_ROOT)
|
// putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LOCAL_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Samples")
|
// putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Samples")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
// putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
||||||
}.build()
|
// }.build()
|
||||||
|
|
||||||
if (audiobooksInProgress.isNotEmpty()) {
|
if (audiobooksInProgress.isNotEmpty()) {
|
||||||
rootList += continueReadingMetadata
|
rootList += continueReadingMetadata
|
||||||
}
|
}
|
||||||
rootList += allMetadata
|
rootList += allMetadata
|
||||||
rootList += downloadsMetadata
|
rootList += downloadsMetadata
|
||||||
rootList += localsMetadata
|
// rootList += localsMetadata
|
||||||
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
||||||
|
|
||||||
audiobooksInProgress.forEach { audiobook ->
|
audiobooksInProgress.forEach { audiobook ->
|
||||||
|
@ -77,23 +76,24 @@ class BrowseTree(
|
||||||
mediaIdToChildren[CONTINUE_ROOT] = children
|
mediaIdToChildren[CONTINUE_ROOT] = children
|
||||||
}
|
}
|
||||||
|
|
||||||
audiobooks.forEach { audiobook ->
|
audiobookMetadata.forEach {
|
||||||
if (audiobook.isDownloaded) {
|
|
||||||
val downloadsChildren = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
|
||||||
downloadsChildren += audiobook.toMediaMetadata()
|
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = downloadsChildren
|
|
||||||
}
|
|
||||||
val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
||||||
allChildren += audiobook.toMediaMetadata()
|
allChildren += it
|
||||||
mediaIdToChildren[ALL_ROOT] = allChildren
|
mediaIdToChildren[ALL_ROOT] = allChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
localAudio.forEach { local ->
|
downloadedMetadata.forEach {
|
||||||
val localChildren = mediaIdToChildren[LOCAL_ROOT] ?: mutableListOf()
|
val allChildren = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
||||||
localChildren += local.toMediaMetadata()
|
allChildren += it
|
||||||
mediaIdToChildren[LOCAL_ROOT] = localChildren
|
mediaIdToChildren[DOWNLOADS_ROOT] = allChildren
|
||||||
}
|
}
|
||||||
Log.d("BrowseTree", "Set LOCAL AUDIO ${localAudio.size}")
|
|
||||||
|
// localAudio.forEach { local ->
|
||||||
|
// val localChildren = mediaIdToChildren[LOCAL_ROOT] ?: mutableListOf()
|
||||||
|
// localChildren += local.toMediaMetadata()
|
||||||
|
// mediaIdToChildren[LOCAL_ROOT] = localChildren
|
||||||
|
// }
|
||||||
|
// Log.d("BrowseTree", "Set LOCAL AUDIO ${localAudio.size}")
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun get(mediaId: String) = mediaIdToChildren[mediaId]
|
operator fun get(mediaId: String) = mediaIdToChildren[mediaId]
|
||||||
|
@ -103,4 +103,4 @@ const val AUTO_BROWSE_ROOT = "/"
|
||||||
const val ALL_ROOT = "__ALL__"
|
const val ALL_ROOT = "__ALL__"
|
||||||
const val CONTINUE_ROOT = "__CONTINUE__"
|
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
const val LOCAL_ROOT = "__LOCAL__"
|
//const val LOCAL_ROOT = "__LOCAL__"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
@ -12,7 +13,11 @@ import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.AnyRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
@ -32,6 +37,7 @@ class LocalMediaManager {
|
||||||
val size: Int,
|
val size: Int,
|
||||||
val coverUri: Uri?
|
val coverUri: Uri?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toMediaMetadata(): MediaMetadataCompat {
|
fun toMediaMetadata(): MediaMetadataCompat {
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
@ -40,75 +46,75 @@ class LocalMediaManager {
|
||||||
|
|
||||||
if (coverUri != null) {
|
if (coverUri != null) {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
|
||||||
} else {
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, "android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val localAudioFiles = mutableListOf<LocalAudio>()
|
val localAudioFiles = mutableListOf<LocalAudio>()
|
||||||
|
|
||||||
@Throws(IOException::class)
|
/**
|
||||||
fun getFileFromAssets(context: Context, fileName: String): File = File(context.cacheDir, fileName)
|
* get uri to drawable or any other resource type if u wish
|
||||||
.also {
|
* @param context - context
|
||||||
if (!it.exists()) {
|
* @param drawableId - drawable res id
|
||||||
it.outputStream().use { cache ->
|
* @return - uri
|
||||||
context.assets.open(fileName).use { inputStream ->
|
*/
|
||||||
inputStream.copyTo(cache)
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLocalAudio() {
|
fun loadLocalAudio() {
|
||||||
Log.d(tag, "Media store looking for local audio files")
|
|
||||||
localAudioFiles.clear()
|
localAudioFiles.clear()
|
||||||
|
|
||||||
localAudioFiles += LocalAudio(Uri.parse("asset:///public/samples/Anthem/AnthemSample.m4b"), "anthem_sample", "Anthem", 60000, 10000, null)
|
localAudioFiles += LocalAudio(Uri.parse("asset:///public/samples/Anthem/AnthemSample.m4b"), "anthem_sample", "Anthem", 60000, 10000, getUriToDrawable(ctx, R.drawable.exo_icon_localaudio))
|
||||||
localAudioFiles += LocalAudio(Uri.parse("asset:///public/samples/Legend of Sleepy Hollow/LegendOfSleepyHollowSample.m4b"), "sleepy_hollow", "Legend of Sleepy Hollow", 60000, 10000,null)
|
localAudioFiles += LocalAudio(Uri.parse("asset:///public/samples/Legend of Sleepy Hollow/LegendOfSleepyHollowSample.m4b"), "sleepy_hollow", "Legend of Sleepy Hollow", 60000, 10000, getUriToDrawable(ctx, R.drawable.exo_icon_localaudio))
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
// TODO: No longer reading in local audio files - just use samples
|
||||||
Log.e(tag, "Permission not granted to read from external storage")
|
// if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
return
|
// Log.e(tag, "Permission not granted to read from external storage")
|
||||||
}
|
// return
|
||||||
|
// }
|
||||||
val collection =
|
//
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
// val collection =
|
||||||
MediaStore.Audio.Media.getContentUri(
|
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
MediaStore.VOLUME_EXTERNAL
|
// MediaStore.Audio.Media.getContentUri(
|
||||||
)
|
// MediaStore.VOLUME_EXTERNAL
|
||||||
} else {
|
// )
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
// } else {
|
||||||
}
|
// MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||||
|
// }
|
||||||
val proj = arrayOf(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.SIZE)
|
//
|
||||||
val audioCursor: Cursor? = ctx.contentResolver.query(collection, proj, null, null, null)
|
// val proj = arrayOf(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.SIZE)
|
||||||
|
// val audioCursor: Cursor? = ctx.contentResolver.query(collection, proj, null, null, null)
|
||||||
audioCursor?.use { cursor ->
|
//
|
||||||
// Cache column indices.
|
// audioCursor?.use { cursor ->
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
// // Cache column indices.
|
||||||
val nameColumn =
|
// val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
// val nameColumn =
|
||||||
val durationColumn =
|
// cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
// val durationColumn =
|
||||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
|
// cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
||||||
|
// val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
|
||||||
while (cursor.moveToNext()) {
|
//
|
||||||
// Get values of columns for a given video.
|
// while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idColumn)
|
// // Get values of columns for a given video.
|
||||||
val name = cursor.getString(nameColumn)
|
// val id = cursor.getLong(idColumn)
|
||||||
val duration = cursor.getInt(durationColumn)
|
// val name = cursor.getString(nameColumn)
|
||||||
val size = cursor.getInt(sizeColumn)
|
// val duration = cursor.getInt(durationColumn)
|
||||||
|
// val size = cursor.getInt(sizeColumn)
|
||||||
val contentUri: Uri = ContentUris.withAppendedId(
|
//
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
// val contentUri: Uri = ContentUris.withAppendedId(
|
||||||
id
|
// MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
)
|
// id
|
||||||
Log.d(tag, "Found local audio file $name")
|
// )
|
||||||
localAudioFiles += LocalAudio(contentUri, id.toString(), name, duration, size, null)
|
// Log.d(tag, "Found local audio file $name")
|
||||||
}
|
// localAudioFiles += LocalAudio(contentUri, id.toString(), name, duration, size, null)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
Log.d(tag, "${localAudioFiles.size} Local Audio Files found")
|
//
|
||||||
|
// Log.d(tag, "${localAudioFiles.size} Local Audio Files found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.capacitorjs.plugins.app.AppPlugin
|
||||||
import com.getcapacitor.*
|
import com.getcapacitor.*
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
@ -22,32 +23,37 @@ class MyNativeAudio : Plugin() {
|
||||||
var foregroundServiceReady : () -> Unit = {
|
var foregroundServiceReady : () -> Unit = {
|
||||||
playerNotificationService = mainActivity.foregroundService
|
playerNotificationService = mainActivity.foregroundService
|
||||||
|
|
||||||
playerNotificationService.setCustomObjectListener(object: PlayerNotificationService.MyCustomObjectListener {
|
playerNotificationService.setBridge(bridge)
|
||||||
override fun onPlayingUpdate(isPlaying:Boolean) {
|
|
||||||
|
playerNotificationService.setCustomObjectListener(object : PlayerNotificationService.MyCustomObjectListener {
|
||||||
|
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||||
emit("onPlayingUpdate", isPlaying)
|
emit("onPlayingUpdate", isPlaying)
|
||||||
}
|
}
|
||||||
override fun onMetadata(metadata:JSObject) {
|
|
||||||
|
override fun onMetadata(metadata: JSObject) {
|
||||||
notifyListeners("onMetadata", metadata)
|
notifyListeners("onMetadata", metadata)
|
||||||
}
|
}
|
||||||
override fun onPrepare(audiobookId:String, playWhenReady:Boolean) {
|
|
||||||
|
override fun onPrepare(audiobookId: String, playWhenReady: Boolean) {
|
||||||
var jsobj = JSObject()
|
var jsobj = JSObject()
|
||||||
jsobj.put("audiobookId", audiobookId)
|
jsobj.put("audiobookId", audiobookId)
|
||||||
jsobj.put("playWhenReady", playWhenReady)
|
jsobj.put("playWhenReady", playWhenReady)
|
||||||
notifyListeners("onPrepareMedia", jsobj)
|
notifyListeners("onPrepareMedia", jsobj)
|
||||||
}
|
}
|
||||||
override fun onSleepTimerEnded(currentPosition:Long) {
|
|
||||||
|
override fun onSleepTimerEnded(currentPosition: Long) {
|
||||||
emit("onSleepTimerEnded", currentPosition)
|
emit("onSleepTimerEnded", currentPosition)
|
||||||
}
|
}
|
||||||
override fun onSleepTimerSet(sleepTimerEndTime:Long) {
|
|
||||||
|
override fun onSleepTimerSet(sleepTimerEndTime: Long) {
|
||||||
emit("onSleepTimerSet", sleepTimerEndTime)
|
emit("onSleepTimerSet", sleepTimerEndTime)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
mainActivity.pluginCallback = foregroundServiceReady
|
mainActivity.pluginCallback = foregroundServiceReady
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emit(evtName: String, value:Any) {
|
fun emit(evtName: String, value: Any) {
|
||||||
var ret:JSObject = JSObject()
|
var ret:JSObject = JSObject()
|
||||||
ret.put("value", value)
|
ret.put("value", value)
|
||||||
notifyListeners(evtName, ret)
|
notifyListeners(evtName, ret)
|
||||||
|
@ -225,7 +231,7 @@ class MyNativeAudio : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun requestSession(call:PluginCall) {
|
fun requestSession(call: PluginCall) {
|
||||||
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
||||||
|
|
||||||
playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() {
|
playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
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.MediaControllerCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
|
@ -20,6 +21,7 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.getcapacitor.Bridge
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
|
@ -31,7 +33,6 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
import com.google.android.exoplayer2.upstream.*
|
import com.google.android.exoplayer2.upstream.*
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
|
||||||
import com.google.android.gms.cast.*
|
import com.google.android.gms.cast.*
|
||||||
import com.google.android.gms.cast.framework.*
|
import com.google.android.gms.cast.framework.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -39,7 +40,6 @@ import okhttp3.OkHttpClient
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
|
||||||
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
||||||
|
|
||||||
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
@ -66,6 +66,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||||
|
private lateinit var audiobookManager:AudiobookManager
|
||||||
|
|
||||||
lateinit var mPlayer: SimpleExoPlayer
|
lateinit var mPlayer: SimpleExoPlayer
|
||||||
lateinit var currentPlayer:Player
|
lateinit var currentPlayer:Player
|
||||||
|
@ -73,6 +74,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
lateinit var sleepTimerManager:SleepTimerManager
|
lateinit var sleepTimerManager:SleepTimerManager
|
||||||
lateinit var castManager:CastManager
|
lateinit var castManager:CastManager
|
||||||
|
lateinit var audiobookProgressSyncer:AudiobookProgressSyncer
|
||||||
|
|
||||||
private var notificationId = 10;
|
private var notificationId = 10;
|
||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
|
@ -87,6 +89,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private var lastPauseTime: Long = 0 //ms
|
private var lastPauseTime: Long = 0 //ms
|
||||||
private var onSeekBack: Boolean = false
|
private var onSeekBack: Boolean = false
|
||||||
|
|
||||||
|
var isAndroidAuto = false
|
||||||
|
var webviewBridge:Bridge? = null
|
||||||
|
|
||||||
// The following are used for the shake detection
|
// The following are used for the shake detection
|
||||||
private var isShakeSensorRegistered:Boolean = false
|
private var isShakeSensorRegistered:Boolean = false
|
||||||
private var mSensorManager: SensorManager? = null
|
private var mSensorManager: SensorManager? = null
|
||||||
|
@ -94,11 +99,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private var mShakeDetector: ShakeDetector? = null
|
private var mShakeDetector: ShakeDetector? = null
|
||||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||||
|
|
||||||
private lateinit var audiobookManager:AudiobookManager
|
|
||||||
|
|
||||||
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
|
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
|
||||||
listener = mylistener
|
listener = mylistener
|
||||||
}
|
}
|
||||||
|
fun setBridge(bridge: Bridge) {
|
||||||
|
webviewBridge = bridge
|
||||||
|
}
|
||||||
|
fun getIsWebviewOpen():Boolean {
|
||||||
|
return webviewBridge?.app?.isActive == true
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Service related stuff
|
Service related stuff
|
||||||
|
@ -224,6 +233,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
mPlayer.release()
|
mPlayer.release()
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
|
audiobookProgressSyncer.reset()
|
||||||
Log.d(tag, "onDestroy")
|
Log.d(tag, "onDestroy")
|
||||||
isStarted = false
|
isStarted = false
|
||||||
|
|
||||||
|
@ -262,18 +272,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
currentPlayer = mPlayer
|
currentPlayer = mPlayer
|
||||||
|
|
||||||
|
var client: OkHttpClient = OkHttpClient()
|
||||||
|
|
||||||
// Initialize sleep timer
|
// Initialize sleep timer
|
||||||
sleepTimerManager = SleepTimerManager(this)
|
sleepTimerManager = SleepTimerManager(this)
|
||||||
|
|
||||||
// Initialize Cast Manager
|
// Initialize Cast Manager
|
||||||
castManager = CastManager(this)
|
castManager = CastManager(this)
|
||||||
|
|
||||||
|
// Initialize Audiobook Progress Syncer (Only used for android auto when webview is not open)
|
||||||
|
audiobookProgressSyncer = AudiobookProgressSyncer(this, client)
|
||||||
|
|
||||||
// Initialize shake sensor
|
// Initialize shake sensor
|
||||||
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
|
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
|
||||||
initSensor()
|
initSensor()
|
||||||
|
|
||||||
// Initialize audiobook manager
|
// Initialize audiobook manager
|
||||||
var client: OkHttpClient = OkHttpClient()
|
|
||||||
audiobookManager = AudiobookManager(ctx, client)
|
audiobookManager = AudiobookManager(ctx, client)
|
||||||
audiobookManager.init()
|
audiobookManager.init()
|
||||||
|
|
||||||
|
@ -438,7 +452,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PAUSE MEDIA SESSION COMPAT")
|
||||||
pause()
|
pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,6 +640,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else lastPauseTime = System.currentTimeMillis()
|
} else lastPauseTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// If app is only running in android auto then webview will not be open
|
||||||
|
// so progress needs to be synced natively
|
||||||
|
Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}")
|
||||||
|
if (player.isPlaying) {
|
||||||
|
audiobookProgressSyncer.start()
|
||||||
|
}
|
||||||
|
if (!player.isPlaying && audiobookProgressSyncer.listeningTimerRunning) {
|
||||||
|
audiobookProgressSyncer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
listener?.onPlayingUpdate(player.isPlaying)
|
listener?.onPlayingUpdate(player.isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -714,6 +739,30 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
return currentPlayer.duration
|
return currentPlayer.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCurrentBookTitle() : String? {
|
||||||
|
return currentAudiobookStreamData?.title
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentBookIsLocal() : Boolean {
|
||||||
|
return currentAudiobookStreamData?.isLocal == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentBookId() : String? {
|
||||||
|
return currentAudiobookStreamData?.audiobookId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentStreamId() : String? {
|
||||||
|
return currentAudiobookStreamData?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerUrl(): String {
|
||||||
|
return audiobookManager.serverUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUserToken() : String {
|
||||||
|
return audiobookManager.token
|
||||||
|
}
|
||||||
|
|
||||||
fun calcPauseSeekBackTime() : Long {
|
fun calcPauseSeekBackTime() : Long {
|
||||||
if (lastPauseTime <= 0) return 0
|
if (lastPauseTime <= 0) return 0
|
||||||
var time: Long = System.currentTimeMillis() - lastPauseTime
|
var time: Long = System.currentTimeMillis() - lastPauseTime
|
||||||
|
@ -789,24 +838,38 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
|
private val ANDROID_AUTO_PKG_NAME = "com.google.android.projection.gearhead"
|
||||||
|
private val ANDROID_AUTO_SIMULATOR_PKG_NAME = "com.google.android.autosimulator"
|
||||||
|
private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
|
||||||
|
private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
|
||||||
|
private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant"
|
||||||
|
private val VALID_MEDIA_BROWSERS = mutableListOf<String>(ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
|
||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
private val ALL_ROOT = "__ALL__"
|
private val ALL_ROOT = "__ALL__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
|
|
||||||
|
|
||||||
|
// Only allowing android auto or similar to access media browser service
|
||||||
|
// normal loading of audiobooks is handled in webview (not natively)
|
||||||
private fun isValid(packageName: String, uid: Int) : Boolean {
|
private fun isValid(packageName: String, uid: Int) : Boolean {
|
||||||
Log.d(tag, "Check package $packageName is valid with uid $uid")
|
Log.d(tag, "onGetRoot: Checking package $packageName with uid $uid")
|
||||||
|
if (!VALID_MEDIA_BROWSERS.contains(packageName)) {
|
||||||
|
Log.d(tag, "onGetRoot: package $packageName not valid for the media browser service")
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
||||||
// Verify that the specified package is allowed to access your
|
// Verify that the specified package is allowed to access your content
|
||||||
// content! You'll need to write your own logic to do this.
|
|
||||||
return if (!isValid(clientPackageName, clientUid)) {
|
return if (!isValid(clientPackageName, clientUid)) {
|
||||||
// If the request comes from an untrusted package, return null.
|
|
||||||
// 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 {
|
||||||
|
// Flag is used to enable syncing progress natively (normally syncing is handled in webview)
|
||||||
|
isAndroidAuto = true
|
||||||
|
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
extras.putBoolean(
|
extras.putBoolean(
|
||||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
||||||
|
@ -833,14 +896,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
audiobookManager.isLoading = false
|
audiobookManager.isLoading = false
|
||||||
|
|
||||||
Log.d(tag, "LOADED AUDIOBOOKS")
|
Log.d(tag, "LOADED AUDIOBOOKS")
|
||||||
browseTree = BrowseTree(this, audiobookManager.audiobooksInProgress, audiobookManager.audiobooks, audiobookManager.localMediaManager.localAudioFiles, null)
|
|
||||||
|
var audiobooks:List<MediaMetadataCompat> = audiobookManager.getAudiobooksMediaMetadata()
|
||||||
|
var downloadedBooks:List<MediaMetadataCompat> = audiobookManager.getDownloadedAudiobooksMediaMetadata()
|
||||||
|
|
||||||
|
browseTree = BrowseTree(this, audiobookManager.audiobooksInProgress, audiobooks, downloadedBooks)
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
Log.d(tag, "Loaded Audiobook description ${item.description.title} - ${item.description.subtitle}")
|
|
||||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||||
}
|
}
|
||||||
if (children != null) {
|
|
||||||
Log.d(tag, "BROWSE TREE CHILDREN ${children.size}")
|
|
||||||
}
|
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -858,7 +921,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
|
||||||
// Check if this is the root menu:
|
// TODO: For using sub menus. Check if this is the root menu:
|
||||||
if (AUTO_MEDIA_ROOT == 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
|
||||||
|
@ -878,7 +941,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
audiobookManager.isLoading = false
|
audiobookManager.isLoading = false
|
||||||
|
|
||||||
Log.d(tag, "LOADED AUDIOBOOKS")
|
Log.d(tag, "LOADED AUDIOBOOKS")
|
||||||
browseTree = BrowseTree(this, audiobookManager.audiobooksInProgress, audiobookManager.audiobooks, audiobookManager.localMediaManager.localAudioFiles, null)
|
var audiobooks:List<MediaMetadataCompat> = audiobookManager.getAudiobooksMediaMetadata()
|
||||||
|
var downloadedBooks:List<MediaMetadataCompat> = audiobookManager.getDownloadedAudiobooksMediaMetadata()
|
||||||
|
|
||||||
|
browseTree = BrowseTree(this, audiobookManager.audiobooksInProgress, audiobooks, downloadedBooks)
|
||||||
val children = browseTree[ALL_ROOT]?.map { item ->
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|
|
@ -450,8 +450,8 @@ export default {
|
||||||
this.playedTrackWidth = ptWidth
|
this.playedTrackWidth = ptWidth
|
||||||
|
|
||||||
if (this.useChapterTrack) {
|
if (this.useChapterTrack) {
|
||||||
this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
||||||
this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
|
|
|
@ -349,6 +349,7 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
var audiobookStreamData = {
|
var audiobookStreamData = {
|
||||||
|
id: 'download',
|
||||||
title: this.title,
|
title: this.title,
|
||||||
author: this.author,
|
author: this.author,
|
||||||
playWhenReady: !!playOnLoad,
|
playWhenReady: !!playOnLoad,
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="hasCover ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
<img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||||
<div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Capacitor } from '@capacitor/core'
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
import AudioDownloader from '@/plugins/audio-downloader'
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
import StorageManager from '@/plugins/storage-manager'
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
|
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue