-
-
Notifications
You must be signed in to change notification settings - Fork 116
[WIP] Initial multiple queues support #865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Changes from 10 commits
471afcd
4f39f96
60e3b6f
3ed4ed8
73bfe3f
048130e
8b2ff7f
d917eee
c6ae8cb
33caec9
9e9001a
aa7d9a0
fe511a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| /* | ||
|
Check warning on line 1 in app/src/main/java/org/akanework/gramophone/logic/GramophoneExtensions.kt
|
||
| * Copyright (C) 2024 Akane Foundation | ||
| * | ||
| * Gramophone is free software: you can redistribute it and/or modify | ||
|
|
@@ -59,12 +59,14 @@ | |
| import androidx.core.view.children | ||
| import androidx.core.view.updateLayoutParams | ||
| import androidx.core.view.updateMargins | ||
| import androidx.media3.common.BundleListRetriever | ||
| import androidx.media3.common.C | ||
| import androidx.media3.common.Format | ||
| import androidx.media3.common.MediaItem | ||
| import androidx.media3.common.Player | ||
| import androidx.media3.common.Tracks | ||
| import androidx.media3.common.util.Log | ||
| import androidx.media3.exoplayer.source.ShuffleOrder | ||
| import androidx.media3.session.MediaController | ||
| import androidx.media3.session.SessionCommand | ||
| import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat | ||
|
|
@@ -75,19 +77,29 @@ | |
| import org.akanework.gramophone.R | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_GET_AUDIO_FORMAT | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_GET_LYRICS | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_DEL | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_ENQUEUE | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_GET_INACTIVE | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_GET_QUEUE | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_LOAD_QUEUE | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_PIN_QUEUE | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_REORDER | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_UNPIN_QUEUE | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QUERY_TIMER | ||
| import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_SET_TIMER | ||
| import org.akanework.gramophone.logic.utils.AfFormatInfo | ||
| import org.akanework.gramophone.logic.utils.AudioFormatDetector | ||
| import org.akanework.gramophone.logic.utils.AudioTrackInfo | ||
| import org.akanework.gramophone.logic.utils.BtCodecInfo | ||
| import org.akanework.gramophone.logic.utils.CalculationUtils | ||
| import org.akanework.gramophone.logic.utils.CircularShuffleOrder | ||
| import org.akanework.gramophone.logic.utils.ReplayGainUtil | ||
| import org.akanework.gramophone.logic.utils.SemanticLyrics | ||
| import org.akanework.gramophone.ui.MainActivity | ||
| import org.jetbrains.annotations.Contract | ||
| import java.io.File | ||
| import java.io.FileInputStream | ||
| import java.util.LinkedList | ||
| import java.util.Locale | ||
| import kotlin.math.max | ||
|
|
||
|
|
@@ -337,6 +349,149 @@ | |
| ) | ||
| } | ||
|
|
||
| fun MediaController.getInactiveQueues(): List<MultiQueueObject> = | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_GET_INACTIVE, Bundle.EMPTY), | ||
| Bundle.EMPTY | ||
| ).get().extras.run { | ||
| val binder = getBinder("allQueues")!! | ||
| BundleListRetriever.getList(binder).map { | ||
| MultiQueueObject.fromBundle(it) | ||
| } | ||
| } | ||
|
|
||
| fun MediaController.getQueue(index: Int = C.INDEX_UNSET): MultiQueueObject? = | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_GET_QUEUE, Bundle.EMPTY).apply { | ||
| customExtras.putInt("index", index) | ||
| }, Bundle.EMPTY | ||
| ).get().extras.run { | ||
| val binder = getBinder("allQueues")!! | ||
| BundleListRetriever.getList(binder).map { | ||
| MultiQueueObject.fromBundle(it) | ||
| }.firstOrNull() | ||
| } | ||
|
|
||
|
|
||
| fun shuffledItems( | ||
| items: List<MediaItem>, | ||
| order: ShuffleOrder | ||
| ): List<MediaItem> { | ||
| val result = mutableListOf<MediaItem>() | ||
|
|
||
| var i = order.firstIndex | ||
| while (i != C.INDEX_UNSET) { | ||
| result.add(items[i]) | ||
| i = order.getNextIndex(i) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| fun shuffledIndices(order: ShuffleOrder): MutableList<Int> { | ||
| val result = mutableListOf<Int>() | ||
|
|
||
| var i = order.firstIndex | ||
| while (i != C.INDEX_UNSET) { | ||
| result.add(i) | ||
| i = order.getNextIndex(i) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| fun MediaController.getQueueForUi(index: Int = -1): Pair<MutableList<Int>, MultiQueueObject>? { | ||
| if (index == -1) { | ||
| return null | ||
| } | ||
| return sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_GET_QUEUE, Bundle.EMPTY).apply { | ||
| customExtras.putInt("index", index) | ||
| }, Bundle.EMPTY | ||
| ).get().extras.run { | ||
| val binder = getBinder("allQueues")!! | ||
| BundleListRetriever.getList(binder).map { | ||
| val mq = MultiQueueObject.fromBundle(it) | ||
| val indexes: MutableList<Int> = if (mq.shuffleOrder == null) { | ||
| (0 until mq.getSize()).toMutableList() | ||
| } else { | ||
| getIntArray("shuffleIndexes")!!.toMutableList() | ||
| } | ||
|
|
||
| Pair(indexes, mq) | ||
| }.firstOrNull() | ||
| } | ||
| } | ||
|
|
||
| fun MediaController.loadQueue(index: Int) { | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_LOAD_QUEUE, Bundle.EMPTY).apply { | ||
| customExtras.putInt("index", index) | ||
| }, Bundle.EMPTY | ||
| ) | ||
| } | ||
|
|
||
| fun MediaController.pinQueue(index: Int) { | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_PIN_QUEUE, Bundle.EMPTY).apply { | ||
| customExtras.putInt("index", index) | ||
| }, Bundle.EMPTY | ||
| ) | ||
| } | ||
|
|
||
|
|
||
| fun MediaController.unQueue(index: Int) { | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_UNPIN_QUEUE, Bundle.EMPTY).apply { | ||
| customExtras.putInt("index", index) | ||
| }, Bundle.EMPTY | ||
| ) | ||
| } | ||
|
|
||
|
|
||
| fun MediaController.deleteQueue(index: Int): Boolean = | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_DEL, Bundle.EMPTY).apply { | ||
| customExtras.putInt("index", index) | ||
| }, Bundle.EMPTY | ||
| ).get().extras.run { | ||
| if (containsKey("status")) | ||
| getBoolean("status") | ||
| else throw IllegalArgumentException("expected status to be set") | ||
| } | ||
|
|
||
| fun MediaController.reorderQueue(from: Int, to: Int): Boolean = | ||
| sendCustomCommand( | ||
| SessionCommand(SERVICE_QB_REORDER, Bundle.EMPTY).apply { | ||
| customExtras.putInt("from", from) | ||
| customExtras.putInt("to", to) | ||
| }, Bundle.EMPTY | ||
| ).get().extras.run { | ||
| if (containsKey("status")) | ||
| getBoolean("status") | ||
| else throw IllegalArgumentException("expected status to be set") | ||
| } | ||
|
|
||
| /* | ||
| // TODO: shuffle and repeat mode | ||
| fun MediaController.playQueue( | ||
| title: String?, | ||
| mediaList: List<MediaItem>, | ||
| mediaItemIndex: Int, | ||
| isOriginal: Boolean | ||
| ) { | ||
| sendCustomCommand( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commented out much?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. qb doesn't own the current queue. Yes, a full queue was added into qb via addqueue, but all that data (with the exception of the title) remains untouched and isnt actually used anywhere. All the old data is overwritten anyways with the latest from the player when setmediaitems is called again, so there is no spaghetti required. I see how that commitQueue nonsense is redundant, so I'll change that. I'll null out the info in addQueue to prevent that info from creeping in in the future, and also call super. Now (fe511a1) it works like this for when setmediaitems is called: handleSetMediaItems -> addqueue adds skeleton queue to qb, -> commitqueue facilitates the active/inactive swap within qb (without its own setmediaitems) -> super.handleSetMediaItems And then for user initiated queue swaps: commitqueue facilitates the active/inactive swap within qb (with realSetMediaItems) -> super.handleSetMediaItems. You cant call setmediaitems again
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
should be pretty obvious why thats a bad idea, you can just save the current queue's title in extra member field or something (note: i did not check latest commit)
Yes, that sounds reasonable |
||
| SessionCommand(SERVICE_QB_ENQUEUE, Bundle.EMPTY).apply { | ||
| customExtras.putString("title", title) | ||
| customExtras.putInt("mediaItemIndex", mediaItemIndex) | ||
| customExtras.putBoolean("isOriginal", isOriginal) | ||
| val binder = BundleListRetriever(mediaList.map { it.toBundleIncludeLocalConfiguration() }) | ||
| customExtras.putBinder("mediaList", binder) | ||
| }, Bundle.EMPTY | ||
| ) | ||
| } | ||
| */ | ||
|
|
||
| fun Tracks.getFirstSelectedTrackFormatByType(type: @C.TrackType Int): Format? { | ||
| for (i in groups) { | ||
| if (i.type == type) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,7 @@ | |
| import androidx.core.content.IntentCompat | ||
| import androidx.lifecycle.lifecycleScope | ||
| import androidx.media3.common.AudioAttributes | ||
| import androidx.media3.common.BundleListRetriever | ||
| import androidx.media3.common.C | ||
| import androidx.media3.common.DeviceInfo | ||
| import androidx.media3.common.Format | ||
|
|
@@ -141,11 +142,22 @@ | |
| private const val PENDING_INTENT_SESSION_ID = 0 | ||
| const val PENDING_INTENT_NOTIFY_ID = 1 | ||
| const val PENDING_INTENT_WIDGET_ID = 2 | ||
|
|
||
| const val SERVICE_SET_TIMER = "set_timer" | ||
| const val SERVICE_QUERY_TIMER = "query_timer" | ||
| const val SERVICE_GET_AUDIO_FORMAT = "get_audio_format" | ||
| const val SERVICE_GET_LYRICS = "get_lyrics" | ||
| const val SERVICE_TIMER_CHANGED = "changed_timer" | ||
|
|
||
| const val SERVICE_QB_GET_INACTIVE = "qb_get_all" | ||
| const val SERVICE_QB_LOAD_QUEUE = "qb_load" | ||
| const val SERVICE_QB_GET_QUEUE = "qb_get_curr_queue" | ||
| const val SERVICE_QB_DEL = "qb_delete" | ||
| const val SERVICE_QB_REORDER = "qb_reorder" | ||
| const val SERVICE_QB_ENQUEUE = "qb_enqueue" | ||
| const val SERVICE_QB_PIN_QUEUE ="qb_pin_queue" | ||
| const val SERVICE_QB_UNPIN_QUEUE ="qb_unpin_queue" | ||
|
|
||
| var instanceForWidgetAndLyricsOnly: GramophonePlaybackService? = null | ||
| } | ||
|
|
||
|
|
@@ -156,6 +168,7 @@ | |
| val endedWorkaroundPlayer | ||
| get() = mediaSession?.player as EndedWorkaroundPlayer? | ||
| private var controller: MediaBrowser? = null | ||
| lateinit var qb: QueueBoard | ||
| private val sendLyrics = Runnable { scheduleSendingLyrics(false) } | ||
| var lyrics: SemanticLyrics? = null | ||
| private set | ||
|
|
@@ -261,288 +274,294 @@ | |
| handler = Handler(Looper.getMainLooper()) | ||
| nm = NotificationManagerCompat.from(this) | ||
| prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) | ||
| qb = QueueBoard(this) | ||
| setListener(this) | ||
| setMediaNotificationProvider( | ||
| MeiZuLyricsMediaNotificationProvider(this) { lastSentHighlightedLyric } | ||
| ) | ||
| setForegroundServiceTimeoutMs(120000) | ||
| setShowNotificationForEmptyPlayer(SHOW_NOTIFICATION_FOR_EMPTY_PLAYER_AFTER_STOP_OR_ERROR) | ||
| if (mayThrowForegroundServiceStartNotAllowed() | ||
| || mayThrowForegroundServiceStartNotAllowedMiui() | ||
| ) { | ||
| nm.createNotificationChannel( | ||
| NotificationChannelCompat.Builder( | ||
| NOTIFY_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH | ||
| ).apply { | ||
| setName(getString(R.string.fgs_failed_channel)) | ||
| setVibrationEnabled(true) | ||
| setVibrationPattern(longArrayOf(0L, 200L)) | ||
| setLightsEnabled(false) | ||
| setShowBadge(false) | ||
| setSound(null, null) | ||
| }.build() | ||
| ) | ||
| } else if (nm.getNotificationChannel(NOTIFY_CHANNEL_ID) != null) { | ||
| // for people who upgraded from S/S_V2 to newer version | ||
| nm.deleteNotificationChannel(NOTIFY_CHANNEL_ID) | ||
| } | ||
|
|
||
| customCommands = | ||
| listOf( | ||
| CommandButton.Builder(CommandButton.ICON_SHUFFLE_OFF) // shuffle currently disabled, click will enable | ||
| .setDisplayName(getString(R.string.shuffle)) | ||
| .setPlayerCommand(Player.COMMAND_SET_SHUFFLE_MODE, true) | ||
| .build(), | ||
| CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) // shuffle currently enabled, click will disable | ||
| .setDisplayName(getString(R.string.shuffle)) | ||
| .setPlayerCommand(Player.COMMAND_SET_SHUFFLE_MODE, false) | ||
| .build(), | ||
| CommandButton.Builder(CommandButton.ICON_REPEAT_OFF) // repeat currently disabled, click will repeat all | ||
| .setDisplayName(getString(R.string.repeat_mode)) | ||
| .setPlayerCommand(Player.COMMAND_SET_REPEAT_MODE, Player.REPEAT_MODE_ALL) | ||
| .build(), | ||
| CommandButton.Builder(CommandButton.ICON_REPEAT_ALL) // repeat all currently enabled, click will repeat one | ||
| .setDisplayName(getString(R.string.repeat_mode)) | ||
| .setPlayerCommand(Player.COMMAND_SET_REPEAT_MODE, Player.REPEAT_MODE_ONE) | ||
| .build(), | ||
| CommandButton.Builder(CommandButton.ICON_REPEAT_ONE) // repeat one currently enabled, click will disable | ||
| .setDisplayName(getString(R.string.repeat_mode)) | ||
| .setPlayerCommand(Player.COMMAND_SET_REPEAT_MODE, Player.REPEAT_MODE_OFF) | ||
| .build(), | ||
| ) | ||
| afFormatTracker = AfFormatTracker(this, playbackHandler, handler) | ||
| afFormatTracker.formatChangedCallback = { format, period -> | ||
| if (period != null) { | ||
| handler.post { | ||
| val currentPeriod = controller?.currentPeriodIndex?.takeIf { | ||
| it != C.INDEX_UNSET && | ||
| (controller?.currentTimeline?.periodCount ?: 0) > it | ||
| } | ||
| ?.let { controller!!.currentTimeline.getUidOfPeriod(it) } | ||
| if (currentPeriod != period) { | ||
| if (format != null) { | ||
| pendingAfTrackFormats[period] = format | ||
| } else { | ||
| pendingAfTrackFormats.remove(period) | ||
| } | ||
| } else { | ||
| afTrackFormat = format?.let { period to it } | ||
| mediaSession?.broadcastCustomCommand( | ||
| SessionCommand(SERVICE_GET_AUDIO_FORMAT, Bundle.EMPTY), | ||
| Bundle.EMPTY | ||
| ) | ||
| } | ||
| } | ||
| } else { | ||
| Log.e(TAG, "mediaPeriodId is NULL in formatChangedCallback!!") | ||
| } | ||
| } | ||
| rgAp = ReplayGainAudioProcessor() | ||
| prefs.registerOnSharedPreferenceChangeListener(this) | ||
| onSharedPreferenceChanged(prefs, null) // read initial values | ||
| val player = EndedWorkaroundPlayer( | ||
| ExoPlayer.Builder( | ||
| exoPlayer = ExoPlayer.Builder( | ||
| this, | ||
| GramophoneRenderFactory( | ||
| this, rgAp, this::onAudioSinkInputFormatChanged, | ||
| afFormatTracker::setAudioSink | ||
| ) | ||
| .setPcmEncodingRestrictionLifted(true) | ||
| .setEnableDecoderFallback(true) | ||
| .setEnableAudioTrackPlaybackParams(true) | ||
| .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON), | ||
| GramophoneMediaSourceFactory( | ||
| DefaultDataSource.Factory(this), | ||
| GramophoneExtractorsFactory().also { | ||
| it.setConstantBitrateSeekingEnabled(true) | ||
| if (prefs.getBooleanStrict("mp3_index_seeking", false)) | ||
| it.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING) | ||
| }) | ||
| ) | ||
| .setWakeMode(C.WAKE_MODE_LOCAL) | ||
| .setAudioAttributes( | ||
| AudioAttributes | ||
| .Builder() | ||
| .setUsage(C.USAGE_MEDIA) | ||
| .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) | ||
| .build(), true | ||
| ) | ||
| .setHandleAudioBecomingNoisy(true) | ||
| .setTrackSelector(DefaultTrackSelector(this).apply { | ||
| setParameters( | ||
| buildUponParameters() | ||
| .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true) | ||
| .setAudioOffloadPreferences( | ||
| TrackSelectionParameters.AudioOffloadPreferences.Builder() | ||
| .apply { | ||
| val config = | ||
| prefs.getStringStrict("offload", "0")?.toIntOrNull() | ||
| if (config != null && config > 0 && Flags.OFFLOAD) { | ||
| rgAp.setOffloadEnabled(true) | ||
| setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) | ||
| setIsGaplessSupportRequired(config == 2) | ||
| } | ||
| } | ||
| .build())) | ||
| }) | ||
| .setPlaybackLooper(internalPlaybackThread.looper) | ||
| .build() | ||
| .build(), | ||
| queueBoard = qb, | ||
| ) | ||
| player.exoPlayer.addAnalyticsListener(EventLogger()) | ||
| player.exoPlayer.addAnalyticsListener(afFormatTracker) | ||
| player.exoPlayer.addAnalyticsListener(this) | ||
| player.exoPlayer.setShuffleOrder(CircularShuffleOrder(player, 0, 0, Random.nextLong())) | ||
| lastPlayedManager = LastPlayedManager(this, player) | ||
| lastPlayedManager.allowSavingState = false | ||
|
|
||
| mediaSession = | ||
| MediaLibrarySession | ||
| .Builder(this, player, this) | ||
| // CacheBitmapLoader is required for MeiZuLyricsMediaNotificationProvider | ||
| .setBitmapLoader(CacheBitmapLoader(object : BitmapLoader { | ||
| // Coil-based bitmap loader to reuse Coil's caching and to make sure we use | ||
| // the same cover art as the rest of the app, ie MediaStore's cover | ||
|
|
||
| private val limit by lazy { MediaSession.getBitmapDimensionLimit(this@GramophonePlaybackService) } | ||
|
|
||
| override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> { | ||
| return CallbackToFutureAdapter.getFuture { completer -> | ||
| imageLoader.enqueue( | ||
| ImageRequest.Builder(this@GramophonePlaybackService) | ||
| .data(data) | ||
| .memoryCacheKey(data.hashCode().toString()) | ||
| .size(limit, limit) | ||
| .allowHardware(false) | ||
| .target( | ||
| onStart = { _ -> | ||
| // We don't need or want a placeholder. | ||
| }, | ||
| onSuccess = { result -> | ||
| completer.set((result as BitmapImage).bitmap) | ||
| }, | ||
| onError = { _ -> | ||
| completer.setException( | ||
| Exception( | ||
| "coil onError called for byte array" | ||
| ) | ||
| ) | ||
| } | ||
| ) | ||
| .build()) | ||
| .also { | ||
| completer.addCancellationListener( | ||
| { it.dispose() }, | ||
| ContextCompat.getMainExecutor( | ||
| this@GramophonePlaybackService | ||
| ) | ||
| ) | ||
| } | ||
| "coil load for ${data.hashCode()}" | ||
| } | ||
| } | ||
|
|
||
| override fun loadBitmap( | ||
| uri: Uri | ||
| ): ListenableFuture<Bitmap> { | ||
| return CallbackToFutureAdapter.getFuture { completer -> | ||
| imageLoader.enqueue( | ||
| ImageRequest.Builder(this@GramophonePlaybackService) | ||
| .data(uri) | ||
| .size(limit, limit) | ||
| .allowHardware(false) | ||
| .target( | ||
| onStart = { _ -> | ||
| // We don't need or want a placeholder. | ||
| }, | ||
| onSuccess = { result -> | ||
| completer.set((result as BitmapImage).bitmap) | ||
| }, | ||
| onError = { _ -> | ||
| completer.setException( | ||
| Exception( | ||
| "coil onError called" + | ||
| " (normal if no album art exists)" | ||
| ) | ||
| ) | ||
| } | ||
| ) | ||
| .build()) | ||
| .also { | ||
| completer.addCancellationListener( | ||
| { it.dispose() }, | ||
| ContextCompat.getMainExecutor( | ||
| this@GramophonePlaybackService | ||
| ) | ||
| ) | ||
| } | ||
| "coil load for $uri" | ||
| } | ||
| } | ||
|
|
||
| override fun supportsMimeType(mimeType: String): Boolean { | ||
| return isBitmapFactorySupportedMimeType(mimeType) | ||
| } | ||
|
|
||
| override fun loadBitmapFromMetadata(metadata: MediaMetadata): ListenableFuture<Bitmap>? { | ||
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { | ||
| // allow using exoplayer's copy extracted here on P- for now | ||
| // refer to the TO DO in GramophoneApplication | ||
| return super.loadBitmapFromMetadata(metadata) | ||
| } | ||
| return metadata.artworkUri?.let { loadBitmap(it) } | ||
| } | ||
| })) | ||
| .setSessionActivity( | ||
| PendingIntent.getActivity( | ||
| this, | ||
| PENDING_INTENT_SESSION_ID, | ||
| Intent(this, MainActivity::class.java), | ||
| PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, | ||
| ) | ||
| ) | ||
| .setSystemUiPlaybackResumptionOptIn(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) | ||
| .build() | ||
| addSession(mediaSession!!) | ||
| controller = MediaBrowser.Builder(this, mediaSession!!.token).buildAsync().get() | ||
| controller!!.addListener(this) | ||
| if (controller!!.audioSessionId != C.AUDIO_SESSION_ID_UNSET) { | ||
| onAudioSessionIdChanged(controller!!.audioSessionId) | ||
| } | ||
| ContextCompat.registerReceiver( | ||
| this, | ||
| seekReceiver, | ||
| IntentFilter("$packageName.SEEK_TO"), | ||
| @SuppressLint("WrongConstant") // why is this needed? | ||
| ContextCompat.RECEIVER_NOT_EXPORTED | ||
| ) | ||
| ContextCompat.registerReceiver( | ||
| this, | ||
| btReceiver, | ||
| IntentFilter("android.bluetooth.a2dp.profile.action.CODEC_CONFIG_CHANGED"), | ||
| @SuppressLint("WrongConstant") // why is this needed? | ||
| ContextCompat.RECEIVER_EXPORTED | ||
| ) | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /* before 8, only sbc was supported */) { | ||
| proxy = BtCodecInfo.getCodec(this) { | ||
| Log.d(TAG, "first bluetooth codec config $btInfo") | ||
| btInfo = it | ||
| mediaSession?.broadcastCustomCommand( | ||
| SessionCommand(SERVICE_GET_AUDIO_FORMAT, Bundle.EMPTY), | ||
| Bundle.EMPTY | ||
| ) | ||
| } | ||
| } | ||
| scope.launch { | ||
| lastPlayedManager.restore { items, factory -> | ||
| if (mediaSession == null) return@restore | ||
| if (items != null) { | ||
| if (endedWorkaroundPlayer?.nextShuffleOrder != null) | ||
| throw IllegalStateException("shuffleFactory was found orphaned") | ||
| endedWorkaroundPlayer?.nextShuffleOrder = factory.toFactory() | ||
| try { | ||
| mediaSession?.player?.setMediaItems( | ||
| items.mediaItems, items.startIndex, items.startPositionMs | ||
| val mq = endedWorkaroundPlayer?.queueBoard?.addQueue( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above, I think this part shouldnt need to change at all and qb should only manage inactive queue |
||
| title = "[LastPlayedManager]", | ||
| mediaList = items.mediaItems, | ||
| mediaItemIndex = items.startIndex, | ||
| startPositionMs = items.startPositionMs, | ||
| // shouldPin = true | ||
| ) | ||
| mq?.let { endedWorkaroundPlayer?.queueBoard?.commitQueue(it) } | ||
|
Check warning on line 564 in app/src/main/java/org/akanework/gramophone/logic/GramophonePlaybackService.kt
|
||
| } catch (e: IllegalSeekPositionException) { | ||
| try { | ||
| mediaSession?.player?.setMediaItems(items.mediaItems) | ||
|
|
@@ -682,6 +701,14 @@ | |
| availableSessionCommands.add(SessionCommand(SERVICE_QUERY_TIMER, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_GET_LYRICS, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_GET_AUDIO_FORMAT, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_GET_INACTIVE, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_GET_QUEUE, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_LOAD_QUEUE, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_DEL, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_REORDER, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_ENQUEUE, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_PIN_QUEUE, Bundle.EMPTY)) | ||
| availableSessionCommands.add(SessionCommand(SERVICE_QB_UNPIN_QUEUE, Bundle.EMPTY)) | ||
| return builder.setAvailableSessionCommands(availableSessionCommands.build()).build() | ||
| } | ||
|
|
||
|
|
@@ -864,6 +891,91 @@ | |
| } | ||
| } | ||
|
|
||
| SERVICE_QB_GET_INACTIVE -> { | ||
| SessionResult(SessionResult.RESULT_SUCCESS).also { res -> | ||
| val queueList: List<MultiQueueObject> = qb.getInactiveQueues() | ||
| val binder = BundleListRetriever(queueList.map { it.toBundle() }) | ||
| res.extras.putBinder("allQueues", binder) | ||
| } | ||
| } | ||
|
|
||
| SERVICE_QB_GET_QUEUE -> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is get for? can't we just have user call LOAD and then read it? or is there some UI where content of inactive is needed?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GET returns the queue with shuffle indexes for ui purposes, while LOAD is intended to facilitate the loading of the queue into the player. I don't believe these should be the same command.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UI purposes as in? The UI is just the dropdown thingie where the user can select a queue. The user hence has no way to view an inactive queue without activating it in UI, no? Hence, I can't think of any UI purpose that would require a LOAD
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. When you click on the queue, it previews it, hence GET is used. LOAD is when you click the load queue Button to activate it. |
||
| SessionResult(SessionResult.RESULT_SUCCESS).also { res -> | ||
| val index = customCommand.customExtras.getInt("index") | ||
| val queueList: List<MultiQueueObject> = qb.getQueue(index) | ||
| val binder = BundleListRetriever(queueList.map { it.toBundle() }) | ||
| res.extras.putBinder("allQueues", binder) | ||
|
|
||
| // assume ui does not expect shuffleIndexes if shuffle is off | ||
| if (!queueList.isEmpty()) { | ||
| val mq = queueList.first() | ||
| val factory = | ||
| CircularShuffleOrder.Persistent.deserialize(mq.shuffleOrder) | ||
| .toFactory() | ||
| val shuffleOrder = factory(0, mq.getSize(), endedWorkaroundPlayer!!) | ||
| val shuffleIndexesList: List<Int> = shuffledIndices(shuffleOrder) | ||
| res.extras.putIntArray("shuffleIndexes", shuffleIndexesList.toIntArray()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| SERVICE_QB_ENQUEUE -> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commented out much? |
||
| val title = customCommand.customExtras.getString("title") ?: "Queue" | ||
| val mediaItemIndex = customCommand.customExtras.getInt("mediaItemIndex") | ||
| val isOriginal = customCommand.customExtras.getBoolean("isOriginal") | ||
| val binder = customCommand.customExtras.getBinder("mediaList")!! | ||
| val mediaList = BundleListRetriever.getList(binder).map { | ||
| MediaItem.fromBundle(it) | ||
| } | ||
|
|
||
| if (Flags.MQ_PREVIEW && prefs.getBooleanStrict("mq_preview", false)) { | ||
| val mq = qb.addQueue(title, mediaList, mediaItemIndex, isOriginal) | ||
| qb.commitQueue(mq) | ||
| if (!mq.queue.isEmpty()) { | ||
| endedWorkaroundPlayer!!.prepare() | ||
| endedWorkaroundPlayer!!.play() | ||
| } | ||
| } else { | ||
| endedWorkaroundPlayer!!.setMediaItems(mediaList, mediaItemIndex, C.TIME_UNSET) | ||
| endedWorkaroundPlayer!!.prepare() | ||
| endedWorkaroundPlayer!!.play() | ||
| } | ||
|
|
||
| SessionResult(SessionResult.RESULT_SUCCESS) | ||
| } | ||
| */ | ||
|
|
||
| SERVICE_QB_LOAD_QUEUE -> { | ||
| val index = customCommand.customExtras.getInt("index") | ||
| qb.commitQueue(index) | ||
| SessionResult(SessionResult.RESULT_SUCCESS) | ||
| } | ||
|
|
||
| SERVICE_QB_PIN_QUEUE -> { | ||
| val index = customCommand.customExtras.getInt("index") | ||
| qb.pinQueue(index) | ||
| SessionResult(SessionResult.RESULT_SUCCESS).also { res -> | ||
| res.extras.putBoolean("status", false) | ||
| } | ||
| } | ||
|
|
||
| SERVICE_QB_UNPIN_QUEUE -> { | ||
| val index = customCommand.customExtras.getInt("index") | ||
| qb.unpinQueue(index) | ||
| SessionResult(SessionResult.RESULT_SUCCESS).also { res -> | ||
| res.extras.putBoolean("status", false) | ||
| } | ||
| } | ||
|
|
||
| SERVICE_QB_DEL -> { | ||
| val index = customCommand.customExtras.getInt("index") | ||
| qb.deleteQueue(index) | ||
| SessionResult(SessionResult.RESULT_SUCCESS).also { res -> | ||
| res.extras.putBoolean("status", false) | ||
| } | ||
| } | ||
|
|
||
|
Check warning on line 978 in app/src/main/java/org/akanework/gramophone/logic/GramophonePlaybackService.kt
|
||
| else -> { | ||
| SessionResult(SessionError.ERROR_BAD_VALUE) | ||
| } | ||
|
|
@@ -897,7 +1009,7 @@ | |
| if (endedWorkaroundPlayer?.nextShuffleOrder != null) | ||
| throw IllegalStateException("shuffleFactory was found orphaned") | ||
| if (isForPlayback && items.mediaItems.isNotEmpty()) { | ||
| endedWorkaroundPlayer?.nextShuffleOrder = factory.toFactory() | ||
| // endedWorkaroundPlayer?.nextShuffleOrder = factory.toFactory() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah...no |
||
| settable.set(items) | ||
| if (endedWorkaroundPlayer?.nextShuffleOrder != null) | ||
| throw IllegalStateException("shuffleFactory was not consumed during resumption") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are doing this at the wrong level, Android AUto for example will just not call your SERVICE_QB_ENQUEUE, it will keep doing setMediaItems(). Instead a player wrapper should give old queue to queueboard before executing setMediaItems()