From 6bb8dfeffa4fae914f6dce4d2e2c597a7e2f3361 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 20 Nov 2021 17:27:03 -0600 Subject: [PATCH] Fix: check permission before media store query, Add: start of casting --- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 13 +- .../audiobookshelf/app/CastOptionsProvider.kt | 28 ++ .../audiobookshelf/app/LocalMediaManager.kt | 24 +- .../com/audiobookshelf/app/MainActivity.kt | 11 +- .../com/audiobookshelf/app/MyNativeAudio.kt | 23 ++ .../app/PlayerNotificationService.kt | 371 +++++++++++++++++- .../com/audiobookshelf/app/StorageManager.kt | 17 +- components/app/Appbar.vue | 7 + package.json | 2 +- 10 files changed, 477 insertions(+), 23 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt 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 @@