Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class GramophoneApplication : Application(), SingletonImageLoader.Factory,
} else it
}
.penaltyLog()
.penaltyDeath()
// .penaltyDeath()
.build()
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
Expand All @@ -157,7 +157,7 @@ class GramophoneApplication : Application(), SingletonImageLoader.Factory,
//.detectTargetFragmentUsage() TODO onDisplayPreferenceDialog()
.detectWrongFragmentContainer()
.detectWrongNestedHierarchy()
.penaltyDeath()
// .penaltyDeath()
.build()
}
android.util.Log.d(TAG, "GramophoneApplication.onCreate()")
Expand Down
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

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (beta)

❌ Getting worse: Global Conditionals

The global code outside of functions increases in cyclomatic complexity from 31 to 37, threshold = 10. The code has become too complex as it contains many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more.

Check warning on line 1 in app/src/main/java/org/akanework/gramophone/logic/GramophoneExtensions.kt

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (beta)

❌ New issue: Primitive Obsession

In this module, 50.0% of all function arguments are primitive types, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
* Copyright (C) 2024 Akane Foundation
*
* Gramophone is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -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
Expand All @@ -75,6 +77,13 @@
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_GET_INACTIVE_LIST
import org.akanework.gramophone.logic.GramophonePlaybackService.Companion.SERVICE_QB_GET_QUEUE_FOR_UI
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
Expand Down Expand Up @@ -337,6 +346,149 @@
)
}

fun MediaController.getInactiveQueues(): List<MultiQueueObject> =
sendCustomCommand(
SessionCommand(SERVICE_QB_GET_INACTIVE_LIST, 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_FOR_UI, 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_FOR_UI, 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(
Copy link
Copy Markdown
Member

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()

title: String?,
mediaList: List<MediaItem>,
mediaItemIndex: Int,
isOriginal: Boolean
) {
sendCustomCommand(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented out much?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -141,11 +142,21 @@
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_LIST = "qb_get_inactive_list"
const val SERVICE_QB_LOAD_QUEUE = "qb_load"
const val SERVICE_QB_GET_QUEUE_FOR_UI = "qb_get_queue_for_ui"
const val SERVICE_QB_DEL = "qb_delete"
const val SERVICE_QB_REORDER = "qb_reorder"
const val SERVICE_QB_PIN_QUEUE ="qb_pin_queue"
const val SERVICE_QB_UNPIN_QUEUE ="qb_unpin_queue"

var instanceForWidgetAndLyricsOnly: GramophonePlaybackService? = null
}

Expand All @@ -156,6 +167,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
Expand Down Expand Up @@ -261,132 +273,134 @@
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,

Check warning on line 403 in app/src/main/java/org/akanework/gramophone/logic/GramophonePlaybackService.kt

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (beta)

❌ Getting worse: Complex Method

GramophonePlaybackService.onCreate already has high cyclomatic complexity, and now it increases in Lines of Code from 311 to 313. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
)
player.exoPlayer.addAnalyticsListener(EventLogger())
player.exoPlayer.addAnalyticsListener(afFormatTracker)
Expand Down Expand Up @@ -682,6 +696,13 @@
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_LIST, Bundle.EMPTY))
availableSessionCommands.add(SessionCommand(SERVICE_QB_GET_QUEUE_FOR_UI, 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_PIN_QUEUE, Bundle.EMPTY))
availableSessionCommands.add(SessionCommand(SERVICE_QB_UNPIN_QUEUE, Bundle.EMPTY))
return builder.setAvailableSessionCommands(availableSessionCommands.build()).build()
}

Expand Down Expand Up @@ -864,6 +885,64 @@
}
}

SERVICE_QB_GET_INACTIVE_LIST -> {
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_FOR_UI -> {
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_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 945 in app/src/main/java/org/akanework/gramophone/logic/GramophonePlaybackService.kt

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (beta)

❌ Getting worse: Complex Method

GramophonePlaybackService.onCustomCommand increases in cyclomatic complexity from 9 to 10, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
else -> {
SessionResult(SessionError.ERROR_BAD_VALUE)
}
Expand Down
Loading
Loading