Add: play local audio files in android auto to demonstrate to the google app testers that the android auto player works

This commit is contained in:
advplyr 2021-11-18 18:22:38 -06:00
parent 0abefbd9bc
commit 2965ccb513
11 changed files with 176 additions and 23 deletions

View file

@ -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 39 versionCode 40
versionName "0.9.20-beta" versionName "0.9.21-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.

View file

@ -26,11 +26,15 @@ class AudiobookManager {
var token = "" var token = ""
private var client:OkHttpClient private var client:OkHttpClient
lateinit var localMediaManager:LocalMediaManager
var audiobooks:MutableList<Audiobook> = mutableListOf() var audiobooks:MutableList<Audiobook> = mutableListOf()
constructor(_ctx:Context, _client:OkHttpClient) { constructor(_ctx:Context, _client:OkHttpClient) {
ctx = _ctx ctx = _ctx
client = _client client = _client
localMediaManager = LocalMediaManager(ctx)
} }
fun init() { fun init() {
@ -102,6 +106,8 @@ class AudiobookManager {
isLoading = true isLoading = true
hasLoaded = true hasLoaded = true
localMediaManager.loadLocalAudio()
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()
@ -171,8 +177,7 @@ class AudiobookManager {
}) })
} }
fun initLocalPlay(audiobook:Audiobook):AudiobookStreamData { fun initDownloadPlay(audiobook:Audiobook):AudiobookStreamData {
var abStreamDataObj = JSObject() var abStreamDataObj = JSObject()
abStreamDataObj.put("id", audiobook.id) abStreamDataObj.put("id", audiobook.id)
abStreamDataObj.put("contentUrl", audiobook.contentUrl) abStreamDataObj.put("contentUrl", audiobook.contentUrl)
@ -190,6 +195,24 @@ class AudiobookManager {
return audiobookStreamData return audiobookStreamData
} }
fun initLocalPlay(local: LocalMediaManager.LocalAudio):AudiobookStreamData {
var abStreamDataObj = JSObject()
abStreamDataObj.put("id", local.id)
abStreamDataObj.put("contentUrl", local.uri.toString())
abStreamDataObj.put("title", local.name)
abStreamDataObj.put("author", "")
abStreamDataObj.put("token", null)
abStreamDataObj.put("cover", "")
abStreamDataObj.put("duration", local.duration)
abStreamDataObj.put("startTime", 0)
abStreamDataObj.put("playbackSpeed", 1)
abStreamDataObj.put("playWhenReady", true)
abStreamDataObj.put("isLocal", true)
var audiobookStreamData = AudiobookStreamData(abStreamDataObj)
return audiobookStreamData
}
fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
val lhsLength = lhs.length + 1 val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1 val rhsLength = rhs.length + 1
@ -243,4 +266,9 @@ class AudiobookManager {
if (audiobooks.isEmpty()) return null if (audiobooks.isEmpty()) return null
return audiobooks[0] return audiobooks[0]
} }
fun getFirstLocal(): LocalMediaManager.LocalAudio? {
if (localMediaManager.localAudioFiles.isEmpty()) return null
return localMediaManager.localAudioFiles[0]
}
} }

View file

@ -11,6 +11,7 @@ import androidx.annotation.AnyRes
class BrowseTree( class BrowseTree(
val context: Context, val context: Context,
val audiobooks: List<Audiobook>, val audiobooks: List<Audiobook>,
val localAudio: List<LocalMediaManager.LocalAudio>,
val recentMediaId: String? = null val recentMediaId: String? = null
) { ) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>() private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
@ -41,14 +42,21 @@ class BrowseTree(
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource) putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource)
}.build() }.build()
val albumsMetadata = MediaMetadataCompat.Builder().apply { val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads") putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
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 {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LOCAL_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Local Audio")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
}.build()
rootList += allMetadata rootList += allMetadata
rootList += albumsMetadata rootList += downloadsMetadata
rootList += localsMetadata
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
audiobooks.forEach { audiobook -> audiobooks.forEach { audiobook ->
@ -61,6 +69,13 @@ class BrowseTree(
allChildren += audiobook.toMediaMetadata() allChildren += audiobook.toMediaMetadata()
mediaIdToChildren[ALL_ROOT] = allChildren mediaIdToChildren[ALL_ROOT] = allChildren
} }
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]
@ -69,3 +84,4 @@ class BrowseTree(
const val AUTO_BROWSE_ROOT = "/" const val AUTO_BROWSE_ROOT = "/"
const val ALL_ROOT = "__ALL__" const val ALL_ROOT = "__ALL__"
const val DOWNLOADS_ROOT = "__DOWNLOADS__" const val DOWNLOADS_ROOT = "__DOWNLOADS__"
const val LOCAL_ROOT = "__LOCAL__"

View file

@ -0,0 +1,73 @@
package com.audiobookshelf.app
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
class LocalMediaManager {
private var ctx: Context
val tag = "LocalAudioManager"
constructor(ctx:Context) {
this.ctx = ctx
}
data class LocalAudio(val uri: Uri,
val id:String,
val name: String,
val duration: Int,
val size: Int
) {
fun toMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, book.authorFL)
// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCover().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, "android.resource://com.audiobookshelf.app/" + R.drawable.icon)
// putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCover().toString())
// putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, book.authorFL)
}.build()
}
}
val localAudioFiles = mutableListOf<LocalAudio>()
fun loadLocalAudio() {
Log.d(tag, "Media store looking for local audio files")
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(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, proj, null, null, null)
audioCursor?.use { cursor ->
// Cache column indices.
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val durationColumn =
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.
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val duration = cursor.getInt(durationColumn)
val size = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
Log.d(tag, "Found local audio file $name")
localAudioFiles += LocalAudio(contentUri, id.toString(), name, duration, size)
}
}
Log.d(tag, "${localAudioFiles.size} Local Audio Files found")
}
}

View file

@ -141,6 +141,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return channelId return channelId
} }
private fun playLocal(local: LocalMediaManager.LocalAudio, playWhenReady: Boolean) {
var asd = audiobookManager.initLocalPlay(local)
asd.playWhenReady = playWhenReady
initPlayer(asd)
}
private fun playFirstLocal(playWhenReady: Boolean) {
var localAudio = audiobookManager.getFirstLocal()
if (localAudio != null) {
playLocal(localAudio, playWhenReady)
}
}
private fun playAudiobookFromMediaBrowser(audiobook: Audiobook, playWhenReady: Boolean) { private fun playAudiobookFromMediaBrowser(audiobook: Audiobook, playWhenReady: Boolean) {
if (!audiobook.isDownloaded) { if (!audiobook.isDownloaded) {
var streamListener = object : AudiobookManager.OnStreamData { var streamListener = object : AudiobookManager.OnStreamData {
@ -152,7 +165,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
audiobookManager.openStream(audiobook, streamListener) audiobookManager.openStream(audiobook, streamListener)
} else { } else {
var asd = audiobookManager.initLocalPlay(audiobook) var asd = audiobookManager.initDownloadPlay(audiobook)
asd.playWhenReady = playWhenReady asd.playWhenReady = playWhenReady
initPlayer(asd) initPlayer(asd)
} }
@ -162,12 +175,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var firstAudiobook = audiobookManager.getFirstAudiobook() var firstAudiobook = audiobookManager.getFirstAudiobook()
if (firstAudiobook != null) { if (firstAudiobook != null) {
playAudiobookFromMediaBrowser(firstAudiobook, playWhenReady) playAudiobookFromMediaBrowser(firstAudiobook, playWhenReady)
} else {
playFirstLocal(playWhenReady)
} }
} }
private fun openFromMediaId(mediaId: String, playWhenReady: Boolean) { private fun openFromMediaId(mediaId: String, playWhenReady: Boolean) {
var audiobook = audiobookManager.audiobooks.find { it.id == mediaId } var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
if (audiobook == null) { if (audiobook == null) {
var localAudio = audiobookManager.localMediaManager.localAudioFiles.find { it.id == mediaId }
if (localAudio != null) {
playLocal(localAudio, playWhenReady)
return
}
Log.e(tag, "Audiobook NOT FOUND") Log.e(tag, "Audiobook NOT FOUND")
return return
} }
@ -470,15 +491,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(tag, "PLAY PAUSE TEST") Log.d(tag, "PLAY PAUSE TEST")
transportControls.playFromSearch("Brave New World", Bundle()) // transportControls.playFromSearch("Brave New World", Bundle())
// if (mPlayer.isPlaying) { if (mPlayer.isPlaying) {
// if (0 == mediaButtonClickCount) pause() if (0 == mediaButtonClickCount) pause()
// handleMediaButtonClickCount() handleMediaButtonClickCount()
// } else { } else {
// if (0 == mediaButtonClickCount) play() if (0 == mediaButtonClickCount) play()
// handleMediaButtonClickCount() handleMediaButtonClickCount()
// } }
} }
else -> { else -> {
Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}") Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
@ -834,7 +855,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
audiobookManager.isLoading = false audiobookManager.isLoading = false
Log.d(tag, "LOADED AUDIOBOOKS") Log.d(tag, "LOADED AUDIOBOOKS")
browseTree = BrowseTree(this, audiobookManager.audiobooks, null) browseTree = BrowseTree(this, audiobookManager.audiobooks, audiobookManager.localMediaManager.localAudioFiles, null)
val children = browseTree[parentMediaId]?.map { item -> val children = browseTree[parentMediaId]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, flag) MediaBrowserCompat.MediaItem(item.description, flag)
} }
@ -850,11 +871,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return return
} }
if (audiobookManager.audiobooks.size == 0) { // if (audiobookManager.audiobooks.size == 0) {
Log.d(tag, "AudiobookManager: Sending no items") // Log.d(tag, "AudiobookManager: Sending no items")
result.sendResult(mediaItems) // result.sendResult(mediaItems)
return // return
} // }
val children = browseTree[parentMediaId]?.map { item -> val children = browseTree[parentMediaId]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, flag) MediaBrowserCompat.MediaItem(item.description, flag)
@ -887,7 +908,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
audiobookManager.isLoading = false audiobookManager.isLoading = false
Log.d(tag, "LOADED AUDIOBOOKS") Log.d(tag, "LOADED AUDIOBOOKS")
browseTree = BrowseTree(this, audiobookManager.audiobooks, null) browseTree = BrowseTree(this, audiobookManager.audiobooks, audiobookManager.localMediaManager.localAudioFiles, null)
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)
} }

View file

@ -0,0 +1,15 @@
<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="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

View file

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "v0.9.20-beta", "version": "v0.9.21-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname localhost --port 1337", "dev": "nuxt --hostname localhost --port 1337",