Skip to content
Closed
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
7 changes: 1 addition & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ plugins {
kotlin("plugin.compose")
id("com.mikepenz.aboutlibraries.plugin")
id("com.mikepenz.aboutlibraries.plugin.android")
id("pt.jcosta.resourceplaceholders")
}

android {
Expand Down Expand Up @@ -257,10 +256,6 @@ android {
testOptions.unitTests.isIncludeAndroidResources = true
}

resourcePlaceholders {
files.set(listOf("xml/shortcuts.xml"))
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
Expand All @@ -287,7 +282,7 @@ tasks.withType<PackageAndroidArtifact> {
}

aboutLibraries {
offlineMode = true
offlineMode = false
collect {
configPath = file("config")
filterVariants.add("release")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.guava.future
import kotlinx.coroutines.guava.await
import org.akanework.gramophone.R
import org.akanework.gramophone.logic.car.CarLyricsConstants
import org.akanework.gramophone.logic.car.CarLyricsManager
import org.akanework.gramophone.logic.ui.MeiZuLyricsMediaNotificationProvider
import org.akanework.gramophone.logic.ui.isManualNotificationUpdate
import org.akanework.gramophone.logic.utils.AfFormatInfo
Expand Down Expand Up @@ -218,6 +220,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
private lateinit var lastPlayedManager: LastPlayedManager
private lateinit var prefs: SharedPreferences
private var lastSentHighlightedLyric: String? = null
private var carLyricsManager: CarLyricsManager? = 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 @@ -452,6 +455,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
.build(),
{ lyrics },
queueBoard = qb,
getCarLyricsExtras = { carLyricsManager?.buildMetadataExtras() ?: Bundle() },
)
player.exoPlayer.addAnalyticsListener(EventLogger())
player.exoPlayer.addAnalyticsListener(afFormatTracker)
Expand Down Expand Up @@ -571,6 +575,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
.build()
addSession(mediaSession!!)
controller = MediaBrowser.Builder(this, mediaSession!!.token).buildAsync().get()
carLyricsManager = CarLyricsManager(mediaSession!!)
controller!!.addListener(this)
if (controller!!.audioSessionId != C.AUDIO_SESSION_ID_UNSET) {
onAudioSessionIdChanged(controller!!.audioSessionId)
Expand Down Expand Up @@ -922,6 +927,10 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
val boostGain = prefs.getIntStrict("rg_boost_gain", 0)
restart = !rgAp.setBoostGain(boostGain) || restart
}
if (key == null || key == CarLyricsConstants.PREF_CAR_LYRICS_ENABLED) {
// vivo smart car lyrics switch toggled: (re)push both channels immediately.
pushCarLyrics()
}
if (restart) {
controller?.stop()
controller?.prepare()
Expand Down Expand Up @@ -1417,6 +1426,11 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
}

val mediaItem = controller?.currentMediaItem
// Push the "loading" state to the car display while the lyrics for the new song are being
// loaded, so the head unit shows a loading indicator instead of "-1".
if (carLyricsManager?.setLoading() == true) {
endedWorkaroundPlayer?.updateCarLyrics()
}
lyricsFetcher.launch {
val trim = prefs.getBoolean("trim_lyrics", true)
val options = LrcParserOptions(
Expand Down Expand Up @@ -1789,6 +1803,8 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
LyricWidgetProvider.update(this)
else
LyricWidgetProvider.adapterUpdate(this)
// vivo smart car projected lyrics: push both channels on every lyric update.
pushCarLyrics()
val isStatusBarLyricsEnabled = prefs.getBooleanStrict("status_bar_lyrics", false)
val highlightedLyric = if (isStatusBarLyricsEnabled && controller?.playWhenReady == true)
getCurrentLyricIndex(false)?.let {
Expand All @@ -1814,6 +1830,29 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
}
}

/**
* Pushes the vivo smart car projected lyrics through both MediaSession channels.
* Reads the master switch each time so that toggling it in settings takes effect promptly.
*/
private fun pushCarLyrics() {
val manager = carLyricsManager ?: return
val enabled = prefs.getBooleanStrict(CarLyricsConstants.PREF_CAR_LYRICS_ENABLED, false)
manager.enabled = enabled
val changed = if (!enabled) {
manager.push()
} else {
val currentLine = getCurrentLyricIndex(false)?.let {
syncedLyrics?.text?.get(it)?.text
}
manager.updateLyric(currentLine, lyrics)
}
// Refresh the metadata channel injected in EndedWorkaroundPlayer.getState() only when the
// pushed values actually changed, to avoid needless state broadcasts.
if (changed) {
endedWorkaroundPlayer?.updateCarLyrics()
}
}

fun getCurrentLyricIndex(withTranslation: Boolean): Int? {
val lines = syncedLyrics?.text?.mapIndexed { i, it -> i to it }?.filter {
it.second.start <= (controller?.currentPosition ?: 0).toULong()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2024 Akane Foundation
*
* Gramophone is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Gramophone is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package org.akanework.gramophone.logic.car

/**
* Constants for the vivo smart car (JoviInCar / uCar) lyrics projection protocol.
*
* Two independent channels must be implemented simultaneously:
* - Channel A (Metadata): the car launcher reads `ucar.media.metadata.*` keys directly from
* the MediaSession metadata.
* - Channel B (Extras): the phone-side "smart car" app reads `music.media.extras.*` keys from
* the MediaSession extras and forwards them to the car head unit.
*
* See the vivo "车载投屏歌词适配开发指南" for details.
*/
object CarLyricsConstants {
// ---- Channel A: Metadata (car launcher direct) ----
const val METADATA_KEY_LYRICS_LINE = "ucar.media.metadata.LYRICS_LINE"
const val METADATA_KEY_LYRICS_WHOLE = "ucar.media.metadata.LYRICS_WHOLE"
const val METADATA_KEY_LYRICS_STATUS = "ucar.media.metadata.LYRICS_STATUS"

// ---- Channel B: Extras (phone-side smart car app forwarding) ----
const val EXTRAS_KEY_LYRIC = "music.media.extras.LYRIC"
const val EXTRAS_KEY_LYRIC_ALLOWED = "music.media.extras.LYRIC_IS_ALLOWED"
const val EXTRAS_KEY_NOTICE_CAR = "music.media.extras.NOTICE_CAR"

// ---- Lyrics status enum (MediaConstants$LyricsState) ----
const val LYRICS_STATUS_SUCCESS = 0L // has lyrics
const val LYRICS_STATUS_NO_LYRICS = 1L // no lyrics
const val LYRICS_STATUS_LOADING = 2L // loading
const val LYRICS_STATUS_FAIL = 3L // load failed

/** Preference key for the car lyrics master switch. */
const val PREF_CAR_LYRICS_ENABLED = "car_lyrics_enabled"
}
Loading