mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-09-01 07:30:00 +02:00
Fix: check permission before media store query, Add: start of casting
This commit is contained in:
parent
f40e971b90
commit
6bb8dfeffa
10 changed files with 477 additions and 23 deletions
|
@ -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.
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue