# Lyricon
Lyricon is an Android status bar lyrics enhancement tool that displays synchronized lyrics directly in the system status bar. Built on the LSPosed/Xposed framework, it hooks into the Android System UI to inject a custom lyric view that supports word-by-word animations, translations, duet mode, and deep visual customization. The tool requires a rooted Android device with LSPosed framework (Android 8.1+ / API 27+) and works through a modular plugin architecture.
The project provides a Provider SDK published to Maven Central (`io.github.proify.lyricon:provider`) that enables third-party music player apps to push lyrics to Lyricon. Plugin developers can create adapters for any music player by implementing the Provider API, which handles service connection, playback state synchronization, and lyric data transmission. The SDK supports simple text lyrics, LRC-style timed lyrics, and rich lyrics with word-level timing, translations, romanization, and secondary text for duet songs.
## LyriconFactory.createProvider
Creates and initializes a LyriconProvider instance for connecting to the Lyricon central service. This is the entry point for any plugin that wants to push lyrics to Lyricon.
```kotlin
import io.github.proify.lyricon.provider.LyriconFactory
import io.github.proify.lyricon.provider.ProviderLogo
// Basic provider creation
val provider = LyriconFactory.createProvider(context)
// Provider with custom logo and metadata
val provider = LyriconFactory.createProvider(
context = context,
providerPackageName = "com.example.musicplugin",
playerPackageName = "com.example.musicplayer",
logo = ProviderLogo.fromDrawable(context, R.drawable.ic_logo),
metadata = providerMetadataOf(
"version" to "1.0.0",
"author" to "Developer Name"
),
processName = "com.example.musicplayer:main"
)
// For testing without LSPosed (uses LocalCentralService)
val testProvider = LyriconFactory.createProvider(
context = context,
centralPackageName = "io.github.lyricon.localcentralapp"
)
```
## LyriconProvider.register / unregister / destroy
Manages the provider lifecycle with the Lyricon central service. Registration connects the provider and enables lyric data transmission.
```kotlin
val provider = LyriconFactory.createProvider(context)
// Register with the central service
val success = provider.register()
if (success) {
// Provider is now active and can send lyrics
}
// Temporarily disconnect while keeping resources
provider.unregister()
// Permanently destroy and release all resources
provider.destroy()
// Check provider state
val info = provider.providerInfo
println("Package: ${info.providerPackageName}")
println("Player: ${info.playerPackageName}")
```
## RemoteService.addConnectionListener
Monitors connection status changes between the provider and the Lyricon central service. Essential for handling reconnection scenarios and service availability.
```kotlin
val provider = LyriconFactory.createProvider(context)
// DSL-style listener registration
provider.service.addConnectionListener {
onConnected { provider ->
Log.d("Lyricon", "Connected to central service")
// Safe to send lyrics now
}
onReconnected { provider ->
Log.d("Lyricon", "Reconnected - resending current song")
// Re-send current playback state after reconnection
resendCurrentSong(provider.player)
}
onDisconnected { provider ->
Log.w("Lyricon", "Disconnected from central service")
// Handle disconnection gracefully
}
onConnectTimeout { provider ->
Log.e("Lyricon", "Connection timeout - service may not be running")
// Show user notification or retry
}
}
// Check current connection status
val status = provider.service.connectionStatus
when {
status.isConnected() -> println("Connected")
status.isConnecting() -> println("Connecting...")
status.isDisconnected() -> println("Disconnected")
}
provider.register()
```
## RemotePlayer.sendText
Sends simple plain text lyrics without timing information. Ideal for real-time lyric display where precise synchronization is not required.
```kotlin
val player = provider.player
// Set playback state first
player.setPlaybackState(true) // true = playing
// Send plain text lyric
player.sendText("I can't just be an ordinary friend")
// Clear the current lyric display
player.sendText(null)
// Check if player connection is active
if (player.isActive) {
player.sendText("Next lyric line here...")
}
```
## RemotePlayer.setSong with Song
Sets complete song information including metadata and optional lyrics. Use this for timed lyric synchronization with playback position tracking.
```kotlin
import io.github.proify.lyricon.lyric.model.Song
val player = provider.player
// Basic song metadata (placeholder before lyrics are ready)
player.setSong(
Song(
name = "Ordinary Friend",
artist = "David Tao"
)
)
// Song with duration (for progress tracking)
player.setSong(
Song(
id = "song_unique_id_123",
name = "Ordinary Friend",
artist = "David Tao",
duration = 245000 // Duration in milliseconds
)
)
// Clear current song
player.setSong(null)
// After setting song, sync playback position
player.setPosition(15000) // Current position: 15 seconds
player.setPlaybackState(true) // Playing
```
## RichLyricLine for LRC-style timed lyrics
Creates line-based timed lyrics with start and end timestamps. Perfect for standard LRC format lyrics where each line has timing information.
```kotlin
import io.github.proify.lyricon.lyric.model.Song
import io.github.proify.lyricon.lyric.model.RichLyricLine
val player = provider.player
// LRC-style timed lyrics
player.setSong(
Song(
id = "song_123",
name = "Ordinary Friend",
artist = "David Tao",
duration = 245000,
lyrics = listOf(
RichLyricLine(
begin = 0,
end = 5200,
text = "I've been thinking about us lately"
),
RichLyricLine(
begin = 5200,
end = 10500,
text = "Wondering where we went wrong"
),
RichLyricLine(
begin = 10500,
end = 15800,
text = "I can't just be an ordinary friend"
),
RichLyricLine(
begin = 15800,
end = 21000,
text = "Don't want to be just friends"
)
)
)
)
// Sync playback position (milliseconds)
player.setPosition(6000) // Will display second line
player.setPlaybackState(true)
// Seek to specific position
player.seekTo(10500) // Jump to third line
```
## LyricWord for word-by-word animation
Creates syllable-level timing for karaoke-style word-by-word highlighting. Enables smooth animated transitions between words as the song plays.
```kotlin
import io.github.proify.lyricon.lyric.model.Song
import io.github.proify.lyricon.lyric.model.RichLyricLine
import io.github.proify.lyricon.lyric.model.LyricWord
val player = provider.player
// Word-by-word lyrics with precise timing
player.setSong(
Song(
id = "song_123",
name = "Ordinary Friend",
artist = "David Tao",
duration = 245000,
lyrics = listOf(
RichLyricLine(
begin = 0,
end = 5200,
text = "I can't just be friends",
words = listOf(
LyricWord(text = "I", begin = 0, end = 800),
LyricWord(text = " can't", begin = 800, end = 1600),
LyricWord(text = " just", begin = 1600, end = 2400),
LyricWord(text = " be", begin = 2400, end = 3200),
LyricWord(text = " friends", begin = 3200, end = 5200)
)
),
RichLyricLine(
begin = 5200,
end = 10500,
text = "Not anymore",
words = listOf(
LyricWord(text = "Not", begin = 5200, end = 6500),
LyricWord(text = " anymore", begin = 6500, end = 10500)
)
)
)
)
)
player.setPosition(0)
player.setPlaybackState(true)
```
## RichLyricLine with translation and secondary text
Creates rich lyrics with translations, romanization, and secondary text for duet songs. Supports displaying multiple text layers simultaneously.
```kotlin
import io.github.proify.lyricon.lyric.model.Song
import io.github.proify.lyricon.lyric.model.RichLyricLine
import io.github.proify.lyricon.lyric.model.LyricWord
val player = provider.player
// Full-featured lyrics with translation and duet support
player.setSong(
Song(
id = "song_456",
name = "Ordinary Friend",
artist = "David Tao",
duration = 245000,
lyrics = listOf(
// Line with translation
RichLyricLine(
begin = 0,
end = 5200,
text = "I can't just be an ordinary friend",
translation = "Je ne peux pas etre juste un ami ordinaire",
words = listOf(
LyricWord(text = "I", begin = 0, end = 400),
LyricWord(text = " can't", begin = 400, end = 1000),
LyricWord(text = " just", begin = 1000, end = 1600),
LyricWord(text = " be", begin = 1600, end = 2000),
LyricWord(text = " an", begin = 2000, end = 2400),
LyricWord(text = " ordinary", begin = 2400, end = 3800),
LyricWord(text = " friend", begin = 3800, end = 5200)
)
),
// Duet line (secondary singer)
RichLyricLine(
begin = 5200,
end = 10500,
text = "I know what you mean",
secondary = "(Background vocals: Oh yeah...)",
isAlignedRight = true, // Display on right side for duet
translation = "Je sais ce que tu veux dire"
),
// Line with romanization (for non-Latin scripts)
RichLyricLine(
begin = 10500,
end = 15800,
text = "Don't want to be just friends",
roma = "Dont want to be just friends", // Romanization
translation = "Ne veux pas etre juste amis"
)
)
)
)
// Enable translation display
player.setDisplayTranslation(true)
// Enable romanization display
player.setDisplayRoma(true)
player.setPosition(0)
player.setPlaybackState(true)
```
## ProviderLogo creation methods
Creates logo icons for the provider that display in the Lyricon UI. Supports bitmap, drawable resource, and SVG formats.
```kotlin
import io.github.proify.lyricon.provider.ProviderLogo
import android.graphics.Bitmap
// From drawable resource (recommended)
val logoFromResource = ProviderLogo.fromDrawable(
context = context,
id = R.drawable.ic_music_player_logo
)
// From drawable with custom dimensions
val logoWithSize = ProviderLogo.fromDrawable(
context = context,
id = R.drawable.ic_logo,
width = 48,
height = 48
)
// From Bitmap directly
val bitmap: Bitmap = // ... your bitmap
val logoFromBitmap = ProviderLogo.fromBitmap(
bitmap = bitmap,
recycle = true // Recycle source bitmap after conversion
)
// From SVG string
val svgContent = """"""
val logoFromSvg = ProviderLogo.fromSvg(svgContent)
// From Base64-encoded PNG
val base64Png = "iVBORw0KGgoAAAANSUhEUgAA..."
val logoFromBase64 = ProviderLogo.fromBase64(base64Png)
// Use in provider creation
val provider = LyriconFactory.createProvider(
context = context,
logo = logoFromResource
)
```
## AndroidManifest.xml configuration
Required manifest configuration to declare your app as a Lyricon plugin module. The metadata entries are read by Lyricon to identify and display your plugin.
```xml
```
```xml
$syllable$translation
```
## Complete plugin implementation example
Full working example of a Lyricon plugin that integrates with a music player service to push lyrics to the status bar.
```kotlin
import android.app.Service
import android.content.Intent
import android.os.IBinder
import io.github.proify.lyricon.provider.LyriconFactory
import io.github.proify.lyricon.provider.LyriconProvider
import io.github.proify.lyricon.provider.ProviderLogo
import io.github.proify.lyricon.lyric.model.Song
import io.github.proify.lyricon.lyric.model.RichLyricLine
import io.github.proify.lyricon.lyric.model.LyricWord
class LyricProviderService : Service() {
private lateinit var provider: LyriconProvider
private var isConnected = false
override fun onCreate() {
super.onCreate()
// Initialize the provider
provider = LyriconFactory.createProvider(
context = this,
logo = ProviderLogo.fromDrawable(this, R.drawable.ic_logo)
)
// Set up connection monitoring
provider.service.addConnectionListener {
onConnected {
isConnected = true
// Send current playing song if any
getCurrentSong()?.let { sendSong(it) }
}
onReconnected {
isConnected = true
getCurrentSong()?.let { sendSong(it) }
}
onDisconnected {
isConnected = false
}
onConnectTimeout {
isConnected = false
// Retry connection after delay
retryConnection()
}
}
// Register with Lyricon service
provider.register()
}
fun sendSong(songData: SongData) {
if (!isConnected) return
val player = provider.player
player.setSong(
Song(
id = songData.id,
name = songData.title,
artist = songData.artist,
duration = songData.durationMs,
lyrics = songData.lyrics.map { line ->
RichLyricLine(
begin = line.startMs,
end = line.endMs,
text = line.text,
translation = line.translation,
words = line.words?.map { word ->
LyricWord(
text = word.text,
begin = word.startMs,
end = word.endMs
)
}
)
}
)
)
player.setDisplayTranslation(songData.showTranslation)
player.setPosition(songData.currentPositionMs)
player.setPlaybackState(songData.isPlaying)
}
fun updatePosition(positionMs: Long) {
if (isConnected) {
provider.player.setPosition(positionMs)
}
}
fun updatePlaybackState(isPlaying: Boolean) {
if (isConnected) {
provider.player.setPlaybackState(isPlaying)
}
}
override fun onDestroy() {
provider.destroy()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun getCurrentSong(): SongData? = null // Implement based on your player
private fun retryConnection() { /* Implement retry logic */ }
}
// Data classes for your music player integration
data class SongData(
val id: String,
val title: String,
val artist: String,
val durationMs: Long,
val currentPositionMs: Long,
val isPlaying: Boolean,
val showTranslation: Boolean,
val lyrics: List
)
data class LyricLineData(
val startMs: Long,
val endMs: Long,
val text: String,
val translation: String? = null,
val words: List? = null
)
data class WordData(
val text: String,
val startMs: Long,
val endMs: Long
)
```
## Gradle dependency configuration
Add the Lyricon Provider SDK dependency to your Android project. The SDK is published to Maven Central.
```kotlin
// build.gradle.kts (Module level)
plugins {
id("com.android.application") // or "com.android.library"
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.musicplugin"
compileSdk = 35
defaultConfig {
minSdk = 27 // Minimum Android 8.1 (API 27)
// ...
}
}
dependencies {
// Lyricon Provider SDK
implementation("io.github.proify.lyricon:provider:0.1.68")
// Required transitive dependencies (included automatically)
// - androidx.core:core-ktx
// - androidx.appcompat:appcompat-resources
// - org.jetbrains.kotlinx:kotlinx-serialization-json
}
```
```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral() // Required for Lyricon SDK
}
}
```
Lyricon serves as a bridge between music player applications and the Android status bar, enabling any music app to display synchronized lyrics without modifying the system UI directly. The primary use cases include: music player developers adding status bar lyric support to their apps through the Provider SDK, enthusiasts creating lyric plugins for popular streaming services that don't natively support status bar lyrics, and power users customizing their Android experience with advanced lyric visualization including animations, translations, and duet mode display.
The integration pattern follows a client-server architecture where the Lyricon core module (activated via LSPosed) runs within System UI as the central service, while music player plugins act as clients pushing lyric data through the Provider API. Plugins can be standalone apps that hook into existing music players using Xposed, or they can be integrated directly into music player source code. The SDK handles all IPC communication, connection management, and data serialization, allowing developers to focus on extracting lyrics from their target music source and formatting them into the Song/RichLyricLine data models.