mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-03 09:34:51 +02:00
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:
parent
0abefbd9bc
commit
2965ccb513
11 changed files with 176 additions and 23 deletions
|
@ -13,8 +13,8 @@ android {
|
|||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 39
|
||||
versionName "0.9.20-beta"
|
||||
versionCode 40
|
||||
versionName "0.9.21-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
@ -26,11 +26,15 @@ class AudiobookManager {
|
|||
var token = ""
|
||||
private var client:OkHttpClient
|
||||
|
||||
lateinit var localMediaManager:LocalMediaManager
|
||||
|
||||
var audiobooks:MutableList<Audiobook> = mutableListOf()
|
||||
|
||||
constructor(_ctx:Context, _client:OkHttpClient) {
|
||||
ctx = _ctx
|
||||
client = _client
|
||||
|
||||
localMediaManager = LocalMediaManager(ctx)
|
||||
}
|
||||
|
||||
fun init() {
|
||||
|
@ -102,6 +106,8 @@ class AudiobookManager {
|
|||
isLoading = true
|
||||
hasLoaded = true
|
||||
|
||||
localMediaManager.loadLocalAudio()
|
||||
|
||||
var db = CapacitorDataStorageSqlite(ctx)
|
||||
db.openStore("storage", "downloads", false, "no-encryption", 1)
|
||||
var keyvalues = db.keysvalues()
|
||||
|
@ -171,8 +177,7 @@ class AudiobookManager {
|
|||
})
|
||||
}
|
||||
|
||||
fun initLocalPlay(audiobook:Audiobook):AudiobookStreamData {
|
||||
|
||||
fun initDownloadPlay(audiobook:Audiobook):AudiobookStreamData {
|
||||
var abStreamDataObj = JSObject()
|
||||
abStreamDataObj.put("id", audiobook.id)
|
||||
abStreamDataObj.put("contentUrl", audiobook.contentUrl)
|
||||
|
@ -190,6 +195,24 @@ class AudiobookManager {
|
|||
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 {
|
||||
val lhsLength = lhs.length + 1
|
||||
val rhsLength = rhs.length + 1
|
||||
|
@ -243,4 +266,9 @@ class AudiobookManager {
|
|||
if (audiobooks.isEmpty()) return null
|
||||
return audiobooks[0]
|
||||
}
|
||||
|
||||
fun getFirstLocal(): LocalMediaManager.LocalAudio? {
|
||||
if (localMediaManager.localAudioFiles.isEmpty()) return null
|
||||
return localMediaManager.localAudioFiles[0]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.annotation.AnyRes
|
|||
class BrowseTree(
|
||||
val context: Context,
|
||||
val audiobooks: List<Audiobook>,
|
||||
val localAudio: List<LocalMediaManager.LocalAudio>,
|
||||
val recentMediaId: String? = null
|
||||
) {
|
||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||
|
@ -41,14 +42,21 @@ class BrowseTree(
|
|||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource)
|
||||
}.build()
|
||||
|
||||
val albumsMetadata = MediaMetadataCompat.Builder().apply {
|
||||
val downloadsMetadata = 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()
|
||||
|
||||
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 += albumsMetadata
|
||||
rootList += downloadsMetadata
|
||||
rootList += localsMetadata
|
||||
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
||||
|
||||
audiobooks.forEach { audiobook ->
|
||||
|
@ -61,6 +69,13 @@ class BrowseTree(
|
|||
allChildren += audiobook.toMediaMetadata()
|
||||
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]
|
||||
|
@ -69,3 +84,4 @@ class BrowseTree(
|
|||
const val AUTO_BROWSE_ROOT = "/"
|
||||
const val ALL_ROOT = "__ALL__"
|
||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||
const val LOCAL_ROOT = "__LOCAL__"
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -141,6 +141,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
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) {
|
||||
if (!audiobook.isDownloaded) {
|
||||
var streamListener = object : AudiobookManager.OnStreamData {
|
||||
|
@ -152,7 +165,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
audiobookManager.openStream(audiobook, streamListener)
|
||||
} else {
|
||||
var asd = audiobookManager.initLocalPlay(audiobook)
|
||||
var asd = audiobookManager.initDownloadPlay(audiobook)
|
||||
asd.playWhenReady = playWhenReady
|
||||
initPlayer(asd)
|
||||
}
|
||||
|
@ -162,12 +175,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
var firstAudiobook = audiobookManager.getFirstAudiobook()
|
||||
if (firstAudiobook != null) {
|
||||
playAudiobookFromMediaBrowser(firstAudiobook, playWhenReady)
|
||||
} else {
|
||||
playFirstLocal(playWhenReady)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFromMediaId(mediaId: String, playWhenReady: Boolean) {
|
||||
var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
@ -470,15 +491,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||
Log.d(tag, "PLAY PAUSE TEST")
|
||||
transportControls.playFromSearch("Brave New World", Bundle())
|
||||
// transportControls.playFromSearch("Brave New World", Bundle())
|
||||
|
||||
// if (mPlayer.isPlaying) {
|
||||
// if (0 == mediaButtonClickCount) pause()
|
||||
// handleMediaButtonClickCount()
|
||||
// } else {
|
||||
// if (0 == mediaButtonClickCount) play()
|
||||
// handleMediaButtonClickCount()
|
||||
// }
|
||||
if (mPlayer.isPlaying) {
|
||||
if (0 == mediaButtonClickCount) pause()
|
||||
handleMediaButtonClickCount()
|
||||
} else {
|
||||
if (0 == mediaButtonClickCount) play()
|
||||
handleMediaButtonClickCount()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
|
||||
|
@ -834,7 +855,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
audiobookManager.isLoading = false
|
||||
|
||||
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 ->
|
||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||
}
|
||||
|
@ -850,11 +871,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
return
|
||||
}
|
||||
|
||||
if (audiobookManager.audiobooks.size == 0) {
|
||||
Log.d(tag, "AudiobookManager: Sending no items")
|
||||
result.sendResult(mediaItems)
|
||||
return
|
||||
}
|
||||
// if (audiobookManager.audiobooks.size == 0) {
|
||||
// Log.d(tag, "AudiobookManager: Sending no items")
|
||||
// result.sendResult(mediaItems)
|
||||
// return
|
||||
// }
|
||||
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||
|
@ -887,7 +908,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
audiobookManager.isLoading = false
|
||||
|
||||
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 ->
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
|
|
|
@ -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>
|
BIN
android/app/src/main/res/drawable-hdpi/exo_icon_localaudio.png
Normal file
BIN
android/app/src/main/res/drawable-hdpi/exo_icon_localaudio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 250 B |
BIN
android/app/src/main/res/drawable-mdpi/exo_icon_localaudio.png
Normal file
BIN
android/app/src/main/res/drawable-mdpi/exo_icon_localaudio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 186 B |
BIN
android/app/src/main/res/drawable-xhdpi/exo_icon_localaudio.png
Normal file
BIN
android/app/src/main/res/drawable-xhdpi/exo_icon_localaudio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 289 B |
BIN
android/app/src/main/res/drawable-xxhdpi/exo_icon_localaudio.png
Normal file
BIN
android/app/src/main/res/drawable-xxhdpi/exo_icon_localaudio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 477 B |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.9.20-beta",
|
||||
"version": "v0.9.21-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue