Skip to content
Merged
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 @@ -218,6 +218,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
private lateinit var lastPlayedManager: LastPlayedManager
private lateinit var prefs: SharedPreferences
private var lastSentHighlightedLyric: String? = null
private var lastSentNotificationLyric: String? = null
private lateinit var afFormatTracker: AfFormatTracker
private lateinit var rgAp: ReplayGainAudioProcessor
private var rgMode = 0 // 0 = disabled, 1 = track, 2 = album, 3 = smart
Expand Down Expand Up @@ -405,6 +406,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
onSharedPreferenceChanged(prefs, null) // read initial values
val player = EndedWorkaroundPlayer(
this,
prefs,
exoPlayer = ExoPlayer.Builder(
this,
GramophoneRenderFactory(
Expand Down Expand Up @@ -452,6 +454,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
.build(),
{ lyrics },
queueBoard = qb,
getNotificationLyric = { lastSentNotificationLyric }
)
player.exoPlayer.addAnalyticsListener(EventLogger())
player.exoPlayer.addAnalyticsListener(afFormatTracker)
Expand Down Expand Up @@ -817,6 +820,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
scope.cancel()
endedWorkaroundPlayer!!.stop()
handler.removeCallbacks(timer)
handler.removeCallbacks(sendLyrics)
mediaSession!!.setOptOutOfMediaButtonPlaybackResumption(controller!!.currentTimeline.isEmpty)
proxy?.let {
it.adapter.closeProfileProxy(BluetoothProfile.A2DP, it.a2dp)
Expand Down Expand Up @@ -902,6 +906,10 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
var restart = false
if (key == null || key == "notification_lyrics" || key == "status_bar_lyrics") {
scheduleSendingLyrics(false)
endedWorkaroundPlayer?.updateLyricNow()
}
if (key == null || key == "rg_mode") {
rgMode = prefs.getStringStrict("rg_mode", "0")!!.toInt()
restart = !computeRgMode(true)
Expand Down Expand Up @@ -1582,6 +1590,8 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
//lyrics = null
//scheduleSendingLyrics(true)
}
lastSentNotificationLyric = null
lastSentHighlightedLyric = null

// reshuffle queue when shuffle AND repeat all are enabled
val player = endedWorkaroundPlayer
Expand Down Expand Up @@ -1760,6 +1770,20 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
scheduleSendingLyrics(false)
}

private fun getActiveNotificationLyric(): String? {
val isNotificationLyricsEnabled = prefs.getBooleanStrict("notification_lyrics", false)
if (!isNotificationLyricsEnabled) return null
if (controller?.playbackState == Player.STATE_ENDED || controller?.playbackState == Player.STATE_IDLE) return null

val cPos = (controller?.contentPosition ?: 0).toULong()
val lines = syncedLyrics?.text?.filter {
it.start <= cPos && !it.isTranslated
}
val currentLine = lines?.maxByOrNull { it.start } ?: return null
if (currentLine.text.isBlank()) return null
return currentLine.text
}

private fun scheduleSendingLyrics(new: Boolean) {
handler.removeCallbacks(sendLyrics)
sendLyricNow(new || !updatedLyricAtLeastOnce)
Expand All @@ -1768,8 +1792,9 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
endedWorkaroundPlayer?.updateLyricNow()
}
val isStatusBarLyricsEnabled = prefs.getBooleanStrict("status_bar_lyrics", false)
val isNotificationLyricsEnabled = prefs.getBooleanStrict("notification_lyrics", false)
val hnw = !LyricWidgetProvider.hasWidget(this)
if (controller?.isPlaying != true || (!isStatusBarLyricsEnabled && hnw)) return
if (controller?.isPlaying != true || (!isStatusBarLyricsEnabled && !isNotificationLyricsEnabled && hnw)) return
val cPos = (controller?.contentPosition ?: 0).toULong()
val nextUpdate = syncedLyrics?.text?.flatMap { line ->
if (hnw && line.start <= cPos) listOf() else if (hnw) listOf(line.start) else
Expand All @@ -1795,10 +1820,16 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
syncedLyrics?.text?.get(it)?.text
}
else null
if (lastSentHighlightedLyric != highlightedLyric) {
val notificationLyric = getActiveNotificationLyric()
if (lastSentHighlightedLyric != highlightedLyric || lastSentNotificationLyric != notificationLyric) {
val notifLyricChanged = lastSentNotificationLyric != notificationLyric
lastSentHighlightedLyric = highlightedLyric
lastSentNotificationLyric = notificationLyric
handler.post {
endedWorkaroundPlayer?.let {
if (notifLyricChanged) {
it.updateLyricNow()
}
// This will access the media notification controller's getters. But because
// controller callback ordering is undefined and in practice our service
// controller sometimes gets called first, this would cause us to access a stale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ private class InnerMeiZuLyricsMediaNotificationProvider(
actionFactory: MediaNotification.ActionFactory
): IntArray {
val ticker = tickerProvider()
val title = mediaSession.player.mediaMetadata.title.toString()
val artist = mediaSession.player.mediaMetadata.artist.toString()
val title = mediaSession.player.mediaMetadata.title?.toString() ?: ""
val artist = mediaSession.player.mediaMetadata.artist?.toString() ?: ""

val bundle = IsLandHelp.isLandMusicShare(
addpic = Bundle(),
Expand All @@ -68,7 +68,7 @@ private class InnerMeiZuLyricsMediaNotificationProvider(

class MeiZuLyricsMediaNotificationProvider(
context: MediaSessionService,
private val tickerProvider: () -> CharSequence?,
private val tickerProvider: () -> CharSequence?
) : MediaNotification.Provider {
private val inner = InnerMeiZuLyricsMediaNotificationProvider(context, tickerProvider).apply {
setSmallIcon(R.drawable.ic_gramophone_monochrome)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.util.Log
import androidx.media3.exoplayer.ExoPlayer
import android.content.SharedPreferences
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import org.akanework.gramophone.BuildConfig
Expand All @@ -38,6 +39,7 @@ import org.akanework.gramophone.logic.utils.CircularShuffleOrder
import org.akanework.gramophone.logic.utils.Flags
import org.akanework.gramophone.logic.utils.SemanticLyrics
import org.json.JSONObject
import org.akanework.gramophone.logic.getBooleanStrict
import uk.akane.libphonograph.items.EXTRA_HD_ARTWORK_URI
import uk.akane.libphonograph.items.hdArtworkUri
import java.util.Objects
Expand All @@ -50,9 +52,11 @@ import java.util.Objects
*/
class EndedWorkaroundPlayer(
val context: Context,
private val prefs: SharedPreferences,
exoPlayer: ExoPlayer,
private val getLyric: () -> SemanticLyrics?,
val queueBoard: QueueBoard
val queueBoard: QueueBoard,
private val getNotificationLyric: () -> CharSequence? = { null }
) : ForwardingSimpleBasePlayer(exoPlayer),
Player.Listener {

Expand Down Expand Up @@ -97,7 +101,8 @@ class EndedWorkaroundPlayer(
}

fun updateLyricNow() {
if (context.packageName == "com.tencent.qqmusic") {
val isNotificationLyricsEnabled = prefs.getBooleanStrict("notification_lyrics", false)
if (context.packageName == "com.tencent.qqmusic" || isNotificationLyricsEnabled) {
invalidateState()
}
}
Expand All @@ -119,6 +124,31 @@ class EndedWorkaroundPlayer(
)
.build()
}
if (superState.playWhenReady && superState.playbackState != STATE_ENDED && superState.playbackState != STATE_IDLE) {
val notifLyric = getNotificationLyric()
if (!notifLyric.isNullOrBlank()) {
val origTitle = superState.currentMetadata.title?.toString() ?: ""
val origArtist = superState.currentMetadata.artist?.toString() ?: ""
val subtitle = if (origArtist.isNotBlank() && origTitle.isNotBlank()) {
"$origArtist - $origTitle"
} else {
origArtist.ifBlank { origTitle }
}
val metadataWithLyric = superState.currentMetadata.buildUpon()
.setTitle(notifLyric)
.setArtist(subtitle)

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.

Do you know if it works with setDisplayTitle+setSubtitle but without setTitle+setArtist?

Reason I'm asking: apps like Pano Scrobbler might not work when changing title/artist

@SteveZMTstudios SteveZMTstudios Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gramophone-1.1.2.1666f33-release.apk.zip
snapshot.tar.gz

I forked out and added another switch, compcompiled one for my phone, but that seems glichey, and edit notification seems unstable.

@SteveZMTstudios SteveZMTstudios Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Screenshot_20260828-204436

Android 7, which higher than minsdk, unsupport this.

Android 16, which brought by OEM, will flickers and displays erratically, as if it's unsure which indicator to follow.

https://github.com/nift4/media/blob/5fcf02d05e7e840e12274473420f6268d2cd2925/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java#L601-L635

Perhaps we can explain the destructive nature of this function within the response switch itself.

.setDisplayTitle(notifLyric)
.setSubtitle(subtitle)
.build()
superState = superState.buildUpon()
.setPlaylist(
superState.timeline,
superState.currentTracks,
metadataWithLyric
)
.build()
}
}
if (context.packageName == "com.tencent.qqmusic") {
// Oplus uses package name whitelist for their lockscreen lyric feature
// (don't use BuildConfig in order to allow late patching of package name, after build)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
<string name="tab_order_summary">应用将在启动时显示第一个标签页。分隔线后的标签页将被隐藏。</string>
<string name="settings_status_bar_lyrics_title">状态栏歌词</string>
<string name="settings_status_bar_lyrics_summary">启用魅族状态栏歌词(仅适用于部分设备)</string>
<string name="settings_notification_lyrics_title">通知栏歌词</string>

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.

Thanks for contributing translations for the new feature! However, the automated script that updates changes would override the change and delete the translation. Translations have to be contributed through Weblate for the workflow to work - and this can only be done after english strings in PR was merged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

well-- i speak chinese and i add it for testing and self use :)
i will submit to weblate after merged. Thanks for notice!

<string name="settings_notification_lyrics_summary">在媒体通知中显示当前播放歌词</string>
<string name="settings_lyrics_ui">启用新歌词界面</string>
<string name="lyric_widget_description">显示当前播放媒体歌词的小部件</string>
<string name="scroll_to_albums">滚动到专辑</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@
<string name="settings_status_bar_lyrics_title">Status bar lyrics</string>
<!-- Settings toggle to enable showing lyrics in status bar (requires meizu phone). Subtext -->
<string name="settings_status_bar_lyrics_summary">Enable MeiZu method of displaying lyrics in status bar (only available on some devices)</string>
<!-- Settings toggle to enable showing lyrics in media notification. Title -->
<string name="settings_notification_lyrics_title">Notification lyrics</string>
<!-- Settings toggle to enable showing lyrics in media notification. Subtext -->
<string name="settings_notification_lyrics_summary">Display lyrics as the notification title and artist - title as subtitle</string>
<!-- Description shown about lyric widget in Android launcher / home screen's widget selector on Android 12+ -->
<string name="lyric_widget_description">Widget that shows lyrics of currently playing media</string>
<!-- Context menu item to delete song/album/playlist -->
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/xml/settings_lyric.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@
android:widgetLayout="@layout/preference_switch_widget"
app:iconSpaceReserved="false" />

<SwitchPreferenceCompat
android:defaultValue="false"
android:key="notification_lyrics"
android:layout="@layout/preference_switch"
android:summary="@string/settings_notification_lyrics_summary"
android:title="@string/settings_notification_lyrics_title"
android:widgetLayout="@layout/preference_switch_widget"
app:iconSpaceReserved="false" />

<SwitchPreferenceCompat
android:defaultValue="true"
android:key="trim_lyrics"
Expand Down