@@ -356,10 +356,12 @@ class MainActivity : AppCompatActivity() {
356356
357357 // --- CUSTOM CENTERING LOGIC ---
358358 val recyclerView = findViewById<RecyclerView >(R .id.lyricRecyclerView)
359- val layoutManager = recyclerView.layoutManager as androidx.recyclerview.widget.LinearLayoutManager
359+ val layoutManager =
360+ recyclerView.layoutManager as androidx.recyclerview.widget.LinearLayoutManager
360361
361362 // Calculate the middle of the RecyclerView
362- val offset = recyclerView.height / 2 - 100 // Subtracting 100px for the approximate height of one lyric row
363+ val offset =
364+ recyclerView.height / 2 - 100 // Subtracting 100px for the approximate height of one lyric row
363365
364366 // This force-scrolls the index to the top, then applies the 'offset'
365367 // to push it down to the middle.
@@ -675,7 +677,7 @@ class MainActivity : AppCompatActivity() {
675677 syncLyricsToPosition(playerState.playbackPosition)
676678 }
677679 }
678- delay(32 )
680+ delay(100 )
679681 }
680682 }
681683 }
@@ -708,37 +710,79 @@ class MainActivity : AppCompatActivity() {
708710 }
709711 }
710712
713+ private var lastPlaybackPosition: Long = - 1L
714+
711715 private fun startConnectionMonitor () {
712- connectionMonitorJob?.cancel() // Cancel any existing job just in case
716+ connectionMonitorJob?.cancel()
713717 Log .d(" Lyrisync" , " Heartbeat start" )
718+
714719 connectionMonitorJob = lifecycleScope.launch(Dispatchers .Main ) {
715720 while (isActive) {
716721 val banner = findViewById<TextView >(R .id.spotifyOfflineBanner)
717- // Check if the remote exists and is actively connected
718722 val isConnected = spotifyAppRemote?.isConnected == true
719- Log .d( " Lyrisync " , " Heartbeat active: $isConnected " )
723+
720724 if (isConnected) {
721- if (banner.visibility == View .VISIBLE ) {
722- banner.visibility = View .GONE
725+ // Fetch the player state to verify the timestamp is actually moving
726+ spotifyAppRemote?.playerApi?.playerState?.setResultCallback { playerState ->
727+ val isPlaying = ! playerState.isPaused
728+ val currentPos = playerState.playbackPosition
729+ Log .d(" Lyrisync" , " pos: $currentPos , isPlaying: $isPlaying " )
730+
731+ // If it claims to be playing, but the position hasn't moved since our last 2-second check, it's frozen.
732+ val isStale = isPlaying && currentPos == lastPlaybackPosition && currentPos > 0
733+
734+ lastPlaybackPosition = currentPos
735+
736+ if (isStale) {
737+ if (banner.visibility == View .GONE ) {
738+ banner.text = " Spotify is sleeping. Tap to sync."
739+ banner.visibility = View .VISIBLE
740+
741+ // Let the user tap the banner to instantly fix the issue
742+ banner.setOnClickListener { wakeUpSpotify() }
743+ }
744+ } else {
745+ // Everything is normal and playing properly
746+ if (banner.visibility == View .VISIBLE ) {
747+ banner.visibility = View .GONE
748+ }
749+ }
723750 }
724751 } else {
752+ // Completely disconnected from the local App Remote
725753 if (banner.visibility == View .GONE ) {
754+ banner.text = " Spotify disconnected. Attempting to reconnect..."
726755 banner.visibility = View .VISIBLE
756+ banner.setOnClickListener(null ) // Remove click listener
727757 }
728758
729- // If we disconnected but aren't currently trying to connect, trigger a silent reconnect
730759 if (! isConnecting) {
731760 Log .d(" Lyrisync" , " Heartbeat missed: Attempting background reconnect" )
732761 reconnectToSpotify(forceAuthView = false )
733762 }
734763 }
735764
736- // Wait 5 seconds before checking again
737- delay(5000 )
765+ // Check every 2 seconds. This guarantees enough time has passed to see if the
766+ // playback position naturally advanced.
767+ delay(2000 )
738768 }
739769 }
740770 }
741771
772+ private fun wakeUpSpotify () {
773+ val spotifyPackage = " com.spotify.music"
774+ val launchIntent = packageManager.getLaunchIntentForPackage(spotifyPackage)
775+
776+ if (launchIntent != null ) {
777+ Toast .makeText(this , " Waking Spotify to sync..." , Toast .LENGTH_SHORT ).show()
778+ // Brings Spotify to the foreground briefly to restore its network privileges
779+ launchIntent.addFlags(Intent .FLAG_ACTIVITY_NEW_TASK or Intent .FLAG_ACTIVITY_REORDER_TO_FRONT )
780+ startActivity(launchIntent)
781+ } else {
782+ Toast .makeText(this , " Spotify not installed." , Toast .LENGTH_SHORT ).show()
783+ }
784+ }
785+
742786 // <------- support funcs ------->
743787 // Setup Coil ImageLoader with GIF support
744788 private fun showFirstStartDialog (prefs : android.content.SharedPreferences ) {
0 commit comments