diff --git a/android/app/build.gradle b/android/app/build.gradle
index eb3aa474..36f55d37 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 42
- versionName "0.9.23-beta"
+ versionCode 43
+ versionName "0.9.24-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/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 030ce910..67bbe6d1 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -29,17 +29,22 @@
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
+
+
+
-
-
-
-
+
+
+
+
diff --git a/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt b/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt
new file mode 100644
index 00000000..27b34caf
--- /dev/null
+++ b/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt
@@ -0,0 +1,28 @@
+package com.audiobookshelf.app
+
+import android.content.Context
+import android.util.Log
+import com.google.android.gms.cast.CastMediaControlIntent
+import com.google.android.gms.cast.framework.CastOptions
+import com.google.android.gms.cast.framework.OptionsProvider
+import com.google.android.gms.cast.framework.SessionProvider
+import com.google.android.gms.cast.framework.media.CastMediaOptions
+
+class CastOptionsProvider : OptionsProvider {
+ override fun getCastOptions(context: Context): CastOptions {
+ Log.d("CastOptionsProvider", "getCastOptions")
+ return CastOptions.Builder()
+ .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID).setCastMediaOptions(
+ CastMediaOptions.Builder()
+ // We manage the media session and the notifications ourselves.
+ .setMediaSessionEnabled(false)
+ .setNotificationOptions(null)
+ .build()
+ )
+ .setStopReceiverApplicationWhenEndingSession(true).build()
+ }
+
+ override fun getAdditionalSessionProviders(context: Context): List? {
+ return null
+ }
+}
diff --git a/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt
index ba3c9695..51dd4306 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/LocalMediaManager.kt
@@ -1,12 +1,19 @@
package com.audiobookshelf.app
+import android.Manifest
import android.content.ContentUris
import android.content.Context
+import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
+import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
class LocalMediaManager {
private var ctx: Context
@@ -40,8 +47,23 @@ class LocalMediaManager {
fun loadLocalAudio() {
Log.d(tag, "Media store looking for local audio files")
+
+ if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ Log.e(tag, "Permission not granted to read from external storage")
+ return
+ }
+
+ val collection =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStore.Audio.Media.getContentUri(
+ MediaStore.VOLUME_EXTERNAL
+ )
+ } 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(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, proj, null, null, null)
+ val audioCursor: Cursor? = ctx.contentResolver.query(collection, proj, null, null, null)
audioCursor?.use { cursor ->
// Cache column indices.
diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt
index c59fef38..435d2931 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt
@@ -1,10 +1,14 @@
package com.audiobookshelf.app
+import android.Manifest
import android.app.DownloadManager
import android.app.SearchManager
import android.content.*
+import android.content.pm.PackageManager
import android.os.*
import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper
import com.getcapacitor.BridgeActivity
@@ -41,13 +45,6 @@ class MainActivity : BridgeActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // REMOVE FOR TESTING
- Log.d(tag, "STARTING UP APP")
-// var client: OkHttpClient = OkHttpClient()
-// var abManager = AudiobookManager(this, client)
-// abManager.init()
-// abManager.fetchAudiobooks()
-
Log.d(tag, "onCreate")
registerPlugin(MyNativeAudio::class.java)
registerPlugin(AudioDownloader::class.java)
diff --git a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt
index d794ebaf..7a767636 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt
@@ -231,4 +231,27 @@ class MyNativeAudio : Plugin() {
playerNotificationService.cancelSleepTimer()
call.resolve()
}
+
+ @PluginMethod
+ fun requestSession(call:PluginCall) {
+ Log.d(tag, "CAST REQUEST SESSION PLUGIN")
+
+ playerNotificationService.requestSession(mainActivity, object : PlayerNotificationService.RequestSessionCallback() {
+ override fun onError(errorCode: Int) {
+ Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
+ }
+
+ override fun onCancel() {
+ Log.d(tag, "CAST REQUEST SESSION ON CANCEL")
+ }
+
+ override fun onJoin(jsonSession: JSONObject?) {
+ Log.d(tag, "CAST REQUEST SESSION ON JOIN")
+ }
+
+ })
+
+
+
+ }
}
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 bdf24a58..a6860817 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
@@ -20,12 +20,18 @@ import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
+import androidx.mediarouter.app.MediaRouteChooserDialog
+import androidx.mediarouter.media.MediaRouteSelector
+import androidx.mediarouter.media.MediaRouter
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.getcapacitor.JSObject
+import com.getcapacitor.PluginCall
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
+import com.google.android.exoplayer2.ext.cast.CastPlayer
+import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.MediaSource
@@ -33,8 +39,13 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.*
+import com.google.android.gms.cast.Cast.MessageReceivedCallback
+import com.google.android.gms.cast.CastDevice
+import com.google.android.gms.cast.CastMediaControlIntent
+import com.google.android.gms.cast.framework.*
import kotlinx.coroutines.*
import okhttp3.OkHttpClient
+import org.json.JSONObject
import java.util.*
import kotlin.concurrent.schedule
@@ -90,6 +101,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var sleepChapterTime:Long = 0L
private lateinit var audiobookManager:AudiobookManager
+ private var newConnectionListener:SessionListener? = null
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
listener = mylistener
@@ -490,9 +502,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
terminateStream()
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
- Log.d(tag, "PLAY PAUSE TEST")
-// transportControls.playFromSearch("Brave New World", Bundle())
-
if (mPlayer.isPlaying) {
if (0 == mediaButtonClickCount) pause()
handleMediaButtonClickCount()
@@ -997,5 +1006,361 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
sleepTimerTask = null
sleepChapterTime = 0L
}
+
+ /**
+ * If Cast is available, create a CastPlayer to handle communication with a Cast session.
+ */
+ private val castPlayer: CastPlayer? by lazy {
+ try {
+ val castContext = CastContext.getSharedInstance(this)
+ CastPlayer(castContext).apply {
+ setSessionAvailabilityListener(CastSessionAvailabilityListener())
+// addListener(playerListener)
+ }
+ } catch (e: Exception) {
+ // We wouldn't normally catch the generic `Exception` however
+ // calling `CastContext.getSharedInstance` can throw various exceptions, all of which
+ // indicate that Cast is unavailable.
+ // Related internal bug b/68009560.
+ Log.i(tag, "Cast is not available on this device. " +
+ "Exception thrown when attempting to obtain CastContext. " + e.message)
+ null
+ }
+ }
+
+ private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
+
+ /**
+ * Called when a Cast session has started and the user wishes to control playback on a
+ * remote Cast receiver rather than play audio locally.
+ */
+ override fun onCastSessionAvailable() {
+// switchToPlayer(currentPlayer, castPlayer!!)
+ Log.d(tag, "CAST SeSSION AVAILABLE " + castPlayer?.deviceInfo)
+ mediaSessionConnector.setPlayer(castPlayer)
+ }
+
+ /**
+ * Called when a Cast session has ended and the user wishes to control playback locally.
+ */
+ override fun onCastSessionUnavailable() {
+// switchToPlayer(currentPlayer, exoPlayer)
+ Log.d(tag, "CAST SESSION UNAVAILABLE")
+ mediaSessionConnector.setPlayer(mPlayer)
+ }
+ }
+
+ fun requestSession(mainActivity:Activity, callback: RequestSessionCallback) {
+ mainActivity.runOnUiThread(object: Runnable {
+ override fun run() {
+ Log.d(tag, "CAST RUNNING ON MAIN THREAD")
+
+ val session: CastSession? = getSession()
+ if (session == null) {
+ // show the "choose a connection" dialog
+
+ // Add the connection listener callback
+ listenForConnection(callback)
+
+ // Create the dialog
+ // TODO accept theme as a config.xml option
+ val builder = MediaRouteChooserDialog(mainActivity, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar)
+ builder.routeSelector = MediaRouteSelector.Builder()
+ .addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
+ .build()
+ builder.setCanceledOnTouchOutside(true)
+ builder.setOnCancelListener {
+ getSessionManager()!!.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
+ callback.onCancel()
+ }
+ builder.show()
+ } else {
+ // We are are already connected, so show the "connection options" Dialog
+ val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
+ if (session.castDevice != null) {
+ builder.setTitle(session.castDevice.friendlyName)
+ }
+ builder.setOnDismissListener { callback.onCancel() }
+ builder.setPositiveButton("Stop Casting") { dialog, which -> endSession(true, null) }
+ builder.show()
+ }
+ }
+ })
+ }
+
+ abstract class RequestSessionCallback : ConnectionCallback {
+ abstract fun onError(errorCode: Int)
+ abstract fun onCancel()
+ override fun onSessionEndedBeforeStart(errorCode: Int): Boolean {
+ onSessionStartFailed(errorCode)
+ return true
+ }
+
+ override fun onSessionStartFailed(errorCode: Int): Boolean {
+ onError(errorCode)
+ return true
+ }
+ }
+
+ fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) {
+
+ getSessionManager()!!.addSessionManagerListener(object : SessionListener() {
+ override fun onSessionEnded(castSession: CastSession?, error: Int) {
+ getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java)
+ Log.d(tag, "CAST END SESSION")
+// media.setSession(null)
+ pluginCall?.resolve()
+// listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, if (stopCasting) "stopped" else "disconnected"))
+ }
+ }, CastSession::class.java)
+ getSessionManager()!!.endCurrentSession(stopCasting)
+
+ }
+
+ open class SessionListener : SessionManagerListener {
+ override fun onSessionStarting(castSession: CastSession?) {}
+ override fun onSessionStarted(castSession: CastSession?, sessionId: String) {}
+ override fun onSessionStartFailed(castSession: CastSession?, error: Int) {}
+ override fun onSessionEnding(castSession: CastSession?) {}
+ override fun onSessionEnded(castSession: CastSession?, error: Int) {}
+ override fun onSessionResuming(castSession: CastSession?, sessionId: String) {}
+ override fun onSessionResumed(castSession: CastSession?, wasSuspended: Boolean) {}
+ override fun onSessionResumeFailed(castSession: CastSession?, error: Int) {}
+ override fun onSessionSuspended(castSession: CastSession?, reason: Int) {}
+ }
+
+ private fun startRouteScan() {
+ var connListener = object: ChromecastListener() {
+ override fun onReceiverAvailableUpdate(available: Boolean) {
+ Log.d(tag, "CAST RECEIVER UPDATE AVAILABLE $available")
+ }
+
+ override fun onSessionRejoin(jsonSession: JSONObject?) {
+ Log.d(tag, "CAST onSessionRejoin")
+ }
+
+ override fun onMediaLoaded(jsonMedia: JSONObject?) {
+ Log.d(tag, "CAST onMediaLoaded")
+ }
+
+ override fun onMediaUpdate(jsonMedia: JSONObject?) {
+ Log.d(tag, "CAST onMediaUpdate")
+ }
+
+ override fun onSessionUpdate(jsonSession: JSONObject?) {
+ Log.d(tag, "CAST onSessionUpdate")
+ }
+
+ override fun onSessionEnd(jsonSession: JSONObject?) {
+ Log.d(tag, "CAST onSessionEnd")
+ }
+
+ override fun onMessageReceived(p0: CastDevice, p1: String, p2: String) {
+ Log.d(tag, "CAST onMessageReceived")
+ }
+ }
+
+ var callback = object : ScanCallback() {
+ override fun onRouteUpdate(routes: List?) {
+ Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
+ // if the routes have changed, we may have an available device
+ // If there is at least one device available
+ if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) {
+
+ routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") }
+
+ // Stop the scan
+ stopRouteScan(this, null);
+ // Let the client know a receiver is available
+ connListener.onReceiverAvailableUpdate(true);
+ // Since we have a receiver we may also have an active session
+ var session = getSessionManager()?.currentCastSession;
+ // If we do have a session
+ if (session != null) {
+ // Let the client know
+ Log.d(tag, "LET SESSION KNOW ABOUT")
+// media.setSession(session);
+// connListener.onSessionRejoin(ChromecastUtilities.createSessionObject(session));
+ }
+ }
+ }
+ }
+ callback.setMediaRouter(getMediaRouter())
+
+ callback.onFilteredRouteUpdate();
+
+ getMediaRouter()!!.addCallback(MediaRouteSelector.Builder()
+ .addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
+ .build(),
+ callback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)
+ }
+
+ internal interface CastListener : MessageReceivedCallback {
+ fun onMediaLoaded(jsonMedia: JSONObject?)
+ fun onMediaUpdate(jsonMedia: JSONObject?)
+ fun onSessionUpdate(jsonSession: JSONObject?)
+ fun onSessionEnd(jsonSession: JSONObject?)
+ }
+
+ internal abstract class ChromecastListener : CastStateListener, CastListener {
+ abstract fun onReceiverAvailableUpdate(available: Boolean)
+ abstract fun onSessionRejoin(jsonSession: JSONObject?)
+
+ /** CastStateListener functions. */
+ override fun onCastStateChanged(state: Int) {
+ onReceiverAvailableUpdate(state != CastState.NO_DEVICES_AVAILABLE)
+ }
+ }
+
+ fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) {
+ if (callback == null) {
+ completionCallback!!.run()
+ return
+ }
+// ctx.runOnUiThread(Runnable {
+ callback.stop()
+ getMediaRouter()!!.removeCallback(callback)
+ completionCallback?.run()
+// })
+ }
+
+ abstract class ScanCallback : MediaRouter.Callback() {
+ /**
+ * Called whenever a route is updated.
+ * @param routes the currently available routes
+ */
+ abstract fun onRouteUpdate(routes: List?)
+
+ /** records whether we have been stopped or not. */
+ private var stopped = false
+
+ /** Global mediaRouter object. */
+ private var mediaRouter: MediaRouter? = null
+
+ /**
+ * Sets the mediaRouter object.
+ * @param router mediaRouter object
+ */
+ fun setMediaRouter(router: MediaRouter?) {
+ mediaRouter = router
+ }
+
+ /**
+ * Call this method when you wish to stop scanning.
+ * It is important that it is called, otherwise battery
+ * life will drain more quickly.
+ */
+ fun stop() {
+ stopped = true
+ }
+
+ fun onFilteredRouteUpdate() {
+ if (stopped || mediaRouter == null) {
+ return
+ }
+ val outRoutes: MutableList = ArrayList()
+ // Filter the routes
+ for (route in mediaRouter!!.routes) {
+ // We don't want default routes, or duplicate active routes
+ // or multizone duplicates https://github.com/jellyfin/cordova-plugin-chromecast/issues/32
+ val extras: Bundle? = route.extras
+ if (extras != null) {
+ CastDevice.getFromBundle(extras)
+ if (extras.getString("com.google.android.gms.cast.EXTRA_SESSION_ID") != null) {
+ continue
+ }
+ }
+ if (!route.isDefault
+ && !route.description.equals("Google Cast Multizone Member")
+ && route.playbackType === MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
+ outRoutes.add(route)
+ }
+ }
+ onRouteUpdate(outRoutes)
+ }
+
+ override fun onRouteAdded(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
+ onFilteredRouteUpdate()
+ }
+
+ override fun onRouteChanged(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
+ onFilteredRouteUpdate()
+ }
+
+ override fun onRouteRemoved(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
+ onFilteredRouteUpdate()
+ }
+ }
+
+ private fun listenForConnection(callback: ConnectionCallback) {
+ // We should only ever have one of these listeners active at a time, so remove previous
+ getSessionManager()?.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
+
+ newConnectionListener = object : SessionListener() {
+ override fun onSessionStarted(castSession: CastSession?, sessionId: String) {
+ Log.d(tag, "CAST SESSION STARTED ${castSession?.castDevice?.friendlyName}")
+ getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
+// media.setSession(castSession)
+// callback.onJoin(ChromecastUtilities.createSessionObject(castSession))
+ }
+
+ override fun onSessionStartFailed(castSession: CastSession?, errCode: Int) {
+ if (callback.onSessionStartFailed(errCode)) {
+ getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
+ }
+ }
+
+ override fun onSessionEnded(castSession: CastSession?, errCode: Int) {
+ if (callback.onSessionEndedBeforeStart(errCode)) {
+ getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
+ }
+ }
+ }
+
+ getSessionManager()?.addSessionManagerListener(newConnectionListener, CastSession::class.java)
+ }
+
+ private fun getContext(): CastContext {
+ return CastContext.getSharedInstance(ctx)
+ }
+
+ private fun getSessionManager(): SessionManager? {
+ return getContext().sessionManager
+ }
+
+ private fun getMediaRouter(): MediaRouter? {
+ return MediaRouter.getInstance(ctx)
+ }
+
+ private fun getSession(): CastSession? {
+ return getSessionManager()?.currentCastSession
+ }
+
+ internal interface ConnectionCallback {
+ /**
+ * Successfully joined a session on a route.
+ * @param jsonSession the session we joined
+ */
+ fun onJoin(jsonSession: JSONObject?)
+
+ /**
+ * Called if we received an error.
+ * @param errorCode You can find the error meaning here:
+ * https://developers.google.com/android/reference/com/google/android/gms/cast/CastStatusCodes
+ * @return true if we are done listening for join, false, if we to keep listening
+ */
+ fun onSessionStartFailed(errorCode: Int): Boolean
+
+ /**
+ * Called when we detect a session ended event before session started.
+ * See issues:
+ * https://github.com/jellyfin/cordova-plugin-chromecast/issues/49
+ * https://github.com/jellyfin/cordova-plugin-chromecast/issues/48
+ * @param errorCode error to output
+ * @return true if we are done listening for join, false, if we to keep listening
+ */
+ fun onSessionEndedBeforeStart(errorCode: Int): Boolean
+ }
}
diff --git a/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt b/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt
index 8edb6269..312065e3 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt
@@ -11,10 +11,7 @@ import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.callback.FolderPickerCallback
import com.anggrayudi.storage.callback.StorageAccessCallback
import com.anggrayudi.storage.file.*
-import com.getcapacitor.JSObject
-import com.getcapacitor.Plugin
-import com.getcapacitor.PluginCall
-import com.getcapacitor.PluginMethod
+import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin(name = "StorageManager")
@@ -160,7 +157,17 @@ class StorageManager : Plugin() {
var folderUrl = call.data.getString("folderUrl", "").toString()
Log.d(TAG, "Searching folder $folderUrl")
- var df: DocumentFile = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))!!
+ var df: DocumentFile? = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
+
+ if (df == null) {
+ Log.e(TAG, "Folder Doc File Invalid $folderUrl")
+ var jsobj = JSObject()
+ jsobj.put("folders", JSArray())
+ jsobj.put("files", JSArray())
+ call.resolve(jsobj)
+ return
+ }
+
Log.d(TAG, "Folder as DF ${df.isDirectory} | ${df.getSimplePath(context)} | ${df.getBasePath(context)} | ${df.name}")
var mediaFolders = mutableListOf()
diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue
index 21b59d65..223cf9ff 100644
--- a/components/app/Appbar.vue
+++ b/components/app/Appbar.vue
@@ -23,6 +23,7 @@
+
search
@@ -36,6 +37,8 @@