Fix: check permission before media store query, Add: start of casting

This commit is contained in:
advplyr 2021-11-20 17:27:03 -06:00
parent f40e971b90
commit 6bb8dfeffa
10 changed files with 477 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 42 versionCode 43
versionName "0.9.23-beta" versionName "0.9.24-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

@ -29,17 +29,22 @@
android:name="com.google.android.gms.car.application" android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/> android:resource="@xml/automotive_app_desc"/>
<!-- Support for Cast -->
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider"/>
<activity <activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="com.audiobookshelf.app.MainActivity" android:name="com.audiobookshelf.app.MainActivity"
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:exported="true"
android:theme="@style/AppTheme.NoActionBarLaunch" android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" /> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />

View file

@ -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<SessionProvider>? {
return null
}
}

View file

@ -1,12 +1,19 @@
package com.audiobookshelf.app package com.audiobookshelf.app
import android.Manifest
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.util.Log 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 { class LocalMediaManager {
private var ctx: Context private var ctx: Context
@ -40,8 +47,23 @@ class LocalMediaManager {
fun loadLocalAudio() { fun loadLocalAudio() {
Log.d(tag, "Media store looking for local audio files") 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 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 -> audioCursor?.use { cursor ->
// Cache column indices. // Cache column indices.

View file

@ -1,10 +1,14 @@
package com.audiobookshelf.app package com.audiobookshelf.app
import android.Manifest
import android.app.DownloadManager import android.app.DownloadManager
import android.app.SearchManager import android.app.SearchManager
import android.content.* import android.content.*
import android.content.pm.PackageManager
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.anggrayudi.storage.SimpleStorage import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import com.getcapacitor.BridgeActivity import com.getcapacitor.BridgeActivity
@ -41,13 +45,6 @@ class MainActivity : BridgeActivity() {
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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") Log.d(tag, "onCreate")
registerPlugin(MyNativeAudio::class.java) registerPlugin(MyNativeAudio::class.java)
registerPlugin(AudioDownloader::class.java) registerPlugin(AudioDownloader::class.java)

View file

@ -231,4 +231,27 @@ class MyNativeAudio : Plugin() {
playerNotificationService.cancelSleepTimer() playerNotificationService.cancelSleepTimer()
call.resolve() 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")
}
})
}
} }

View file

@ -20,12 +20,18 @@ import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants 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.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import com.getcapacitor.PluginCall
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes 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.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.MediaSource 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.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.* 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 kotlinx.coroutines.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.json.JSONObject
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
@ -90,6 +101,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var sleepChapterTime:Long = 0L private var sleepChapterTime:Long = 0L
private lateinit var audiobookManager:AudiobookManager private lateinit var audiobookManager:AudiobookManager
private var newConnectionListener:SessionListener? = null
fun setCustomObjectListener(mylistener: MyCustomObjectListener) { fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
listener = mylistener listener = mylistener
@ -490,9 +502,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
terminateStream() terminateStream()
} }
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(tag, "PLAY PAUSE TEST")
// transportControls.playFromSearch("Brave New World", Bundle())
if (mPlayer.isPlaying) { if (mPlayer.isPlaying) {
if (0 == mediaButtonClickCount) pause() if (0 == mediaButtonClickCount) pause()
handleMediaButtonClickCount() handleMediaButtonClickCount()
@ -997,5 +1006,361 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
sleepTimerTask = null sleepTimerTask = null
sleepChapterTime = 0L 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<CastSession> {
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<MediaRouter.RouteInfo>?) {
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<MediaRouter.RouteInfo>?)
/** 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<MediaRouter.RouteInfo> = 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
}
} }

View file

@ -11,10 +11,7 @@ import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.callback.FolderPickerCallback import com.anggrayudi.storage.callback.FolderPickerCallback
import com.anggrayudi.storage.callback.StorageAccessCallback import com.anggrayudi.storage.callback.StorageAccessCallback
import com.anggrayudi.storage.file.* import com.anggrayudi.storage.file.*
import com.getcapacitor.JSObject import com.getcapacitor.*
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin(name = "StorageManager") @CapacitorPlugin(name = "StorageManager")
@ -160,7 +157,17 @@ class StorageManager : Plugin() {
var folderUrl = call.data.getString("folderUrl", "").toString() var folderUrl = call.data.getString("folderUrl", "").toString()
Log.d(TAG, "Searching folder $folderUrl") 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}") Log.d(TAG, "Folder as DF ${df.isDirectory} | ${df.getSimplePath(context)} | ${df.getBasePath(context)} | ${df.name}")
var mediaFolders = mutableListOf<MediaFolder>() var mediaFolders = mutableListOf<MediaFolder>()

View file

@ -23,6 +23,7 @@
<!-- <span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span> --> <!-- <span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span> -->
<!-- <widgets-connection-icon /> --> <!-- <widgets-connection-icon /> -->
<!-- <span class="material-icons" style="font-size: 1.75rem" @click="testCast">menu</span> -->
<nuxt-link class="h-7 mx-2" to="/search"> <nuxt-link class="h-7 mx-2" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span> <span class="material-icons" style="font-size: 1.75rem">search</span>
@ -36,6 +37,8 @@
</template> </template>
<script> <script>
// import MyNativeAudio from '@/plugins/my-native-audio'
export default { export default {
data() { data() {
return { return {
@ -83,6 +86,10 @@ export default {
} }
}, },
methods: { methods: {
// testCast() {
// console.log('TEST CAST CLICK')
// MyNativeAudio.requestSession()
// },
clickShowSideDrawer() { clickShowSideDrawer() {
this.$store.commit('setShowSideDrawer', true) this.$store.commit('setShowSideDrawer', true)
}, },

View file

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