diff --git a/android/app/build.gradle b/android/app/build.gradle index 179ad30e..8ff1751b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 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. diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt index edce7676..dd371cda 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt @@ -26,11 +26,15 @@ class AudiobookManager { var token = "" private var client:OkHttpClient + lateinit var localMediaManager:LocalMediaManager + var audiobooks:MutableList = 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] + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt b/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt index ebec88fc..90e95dfc 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/BrowseTree.kt @@ -11,6 +11,7 @@ import androidx.annotation.AnyRes class BrowseTree( val context: Context, val audiobooks: List, + val localAudio: List, val recentMediaId: String? = null ) { private val mediaIdToChildren = mutableMapOf>() @@ -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__" diff --git a/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt new file mode 100644 index 00000000..ba3c9695 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt @@ -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() + + 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") + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt index 245674f7..bdf24a58 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt @@ -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) } diff --git a/android/app/src/main/res/drawable-anydpi-v24/exo_icon_localaudio.xml b/android/app/src/main/res/drawable-anydpi-v24/exo_icon_localaudio.xml new file mode 100644 index 00000000..87354c3f --- /dev/null +++ b/android/app/src/main/res/drawable-anydpi-v24/exo_icon_localaudio.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/app/src/main/res/drawable-hdpi/exo_icon_localaudio.png b/android/app/src/main/res/drawable-hdpi/exo_icon_localaudio.png new file mode 100644 index 00000000..81972efc Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/exo_icon_localaudio.png differ diff --git a/android/app/src/main/res/drawable-mdpi/exo_icon_localaudio.png b/android/app/src/main/res/drawable-mdpi/exo_icon_localaudio.png new file mode 100644 index 00000000..a3724bb2 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/exo_icon_localaudio.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/exo_icon_localaudio.png b/android/app/src/main/res/drawable-xhdpi/exo_icon_localaudio.png new file mode 100644 index 00000000..7317d3a0 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/exo_icon_localaudio.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/exo_icon_localaudio.png b/android/app/src/main/res/drawable-xxhdpi/exo_icon_localaudio.png new file mode 100644 index 00000000..62f24451 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/exo_icon_localaudio.png differ diff --git a/package.json b/package.json index a97bc131..01739a01 100644 --- a/package.json +++ b/package.json @@ -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",