@@ -13,7 +13,6 @@ import android.net.Uri
1313import android.os.Build
1414import android.os.Bundle
1515import android.os.SystemClock
16- import android.util.Log
1716import androidx.core.app.NotificationCompat
1817import androidx.glance.appwidget.GlanceAppWidgetManager
1918import androidx.glance.appwidget.state.updateAppWidgetState
@@ -90,8 +89,10 @@ import kotlin.math.abs
9089import java.io.ByteArrayOutputStream
9190import java.net.HttpURLConnection
9291import java.net.URL
92+ import java.util.concurrent.atomic.AtomicInteger
9393
9494import javax.inject.Inject
95+ import androidx.core.net.toUri
9596
9697// Acciones personalizadas para compatibilidad con el widget existente
9798
@@ -163,6 +164,7 @@ class MusicService : MediaLibraryService() {
163164 private var shouldResumeAfterHeadsetReconnect = false
164165 private var lastNoisyPauseRealtimeMs = 0L
165166 private var resumeOnHeadsetReconnectEnabled = false
167+ private var temporaryForegroundStartedInOnCreate = false
166168
167169 companion object {
168170 private const val TAG = " MusicService_PixelPlay"
@@ -171,6 +173,7 @@ class MusicService : MediaLibraryService() {
171173 const val EXTRA_FORCE_FOREGROUND_ON_START =
172174 " com.theveloper.pixelplay.extra.FORCE_FOREGROUND_ON_START"
173175 private const val PLAYBACK_SNAPSHOT_DEBOUNCE_MS = 350L
176+ private val pendingMediaButtonForegroundStarts = AtomicInteger (0 )
174177
175178 private const val APP_PACKAGE_PREFIX = " com.theveloper.pixelplay"
176179 private val BLOCKED_WEAR_CONTROLLER_PREFIXES = listOf (
@@ -199,6 +202,30 @@ class MusicService : MediaLibraryService() {
199202 private const val DEFAULT_STREAM_BUFFER_SIZE = 8 * 1024
200203 private const val WIDGET_ART_FAILURE_RETRY_MS = 30_000L
201204 private const val HEADSET_RECONNECT_RESUME_WINDOW_MS = 15_000L
205+
206+ fun markPendingMediaButtonForegroundStart () {
207+ pendingMediaButtonForegroundStarts.incrementAndGet()
208+ }
209+
210+ fun unmarkPendingMediaButtonForegroundStart () {
211+ while (true ) {
212+ val currentCount = pendingMediaButtonForegroundStarts.get()
213+ if (currentCount <= 0 ) return
214+ if (pendingMediaButtonForegroundStarts.compareAndSet(currentCount, currentCount - 1 )) {
215+ return
216+ }
217+ }
218+ }
219+
220+ private fun consumePendingMediaButtonForegroundStart (): Boolean {
221+ while (true ) {
222+ val currentCount = pendingMediaButtonForegroundStarts.get()
223+ if (currentCount <= 0 ) return false
224+ if (pendingMediaButtonForegroundStarts.compareAndSet(currentCount, currentCount - 1 )) {
225+ return true
226+ }
227+ }
228+ }
202229 }
203230
204231 private val playerSwapListener: (Player ) -> Unit = { newPlayer ->
@@ -239,6 +266,14 @@ class MusicService : MediaLibraryService() {
239266 }
240267
241268 super .onCreate()
269+
270+ // A MEDIA_BUTTON broadcast starts the foreground-service timeout before
271+ // MusicService reaches onStartCommand(). Promote as early as possible so
272+ // cold-start initialization cannot consume the whole timeout window.
273+ temporaryForegroundStartedInOnCreate = consumePendingMediaButtonForegroundStart()
274+ if (temporaryForegroundStartedInOnCreate) {
275+ startTemporaryForegroundForCommand()
276+ }
242277
243278 // Ensure engine is ready (re-initialize if service was restarted)
244279 engine.initialize()
@@ -747,19 +782,12 @@ class MusicService : MediaLibraryService() {
747782 pendingIntent,
748783 )
749784 }
750- } else if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . M ) {
785+ } else
751786 alarmManager.setExactAndAllowWhileIdle(
752787 AlarmManager .RTC_WAKEUP ,
753788 triggerAtMillis,
754789 pendingIntent,
755790 )
756- } else {
757- alarmManager.setExact(
758- AlarmManager .RTC_WAKEUP ,
759- triggerAtMillis,
760- pendingIntent,
761- )
762- }
763791 Timber .tag(TAG ).d(" Sleep timer set from Wear for %d minutes" , minutes)
764792 } catch (e: SecurityException ) {
765793 Timber .tag(TAG ).w(e, " Exact alarm denied; using inexact sleep timer" )
@@ -791,7 +819,6 @@ class MusicService : MediaLibraryService() {
791819 }
792820
793821 private fun startTemporaryForegroundForCommand () {
794- if (Build .VERSION .SDK_INT < Build .VERSION_CODES .O ) return
795822 val notification = NotificationCompat .Builder (
796823 this ,
797824 PixelPlayApplication .NOTIFICATION_CHANNEL_ID
@@ -820,12 +847,18 @@ class MusicService : MediaLibraryService() {
820847 }
821848
822849 override fun onStartCommand (intent : Intent ? , flags : Int , startId : Int ): Int {
850+ val startedTemporaryForegroundInOnCreate = temporaryForegroundStartedInOnCreate
851+ temporaryForegroundStartedInOnCreate = false
852+ val pendingMediaButtonForegroundStart = consumePendingMediaButtonForegroundStart()
823853 val forcedForegroundStart =
824854 intent?.getBooleanExtra(EXTRA_FORCE_FOREGROUND_ON_START , false ) == true
825855 val isMediaButtonIntent = intent?.action == Intent .ACTION_MEDIA_BUTTON
826856 val needsTemporaryForeground = forcedForegroundStart ||
827- (isMediaButtonIntent && ! isServiceAlreadyForeground())
828- if (needsTemporaryForeground) {
857+ pendingMediaButtonForegroundStart ||
858+ (isMediaButtonIntent &&
859+ ! startedTemporaryForegroundInOnCreate &&
860+ ! isServiceAlreadyForeground())
861+ if (needsTemporaryForeground && ! startedTemporaryForegroundInOnCreate) {
829862 startTemporaryForegroundForCommand()
830863 }
831864
@@ -855,7 +888,7 @@ class MusicService : MediaLibraryService() {
855888 if (songId != - 1L ) {
856889 val timeline = player.currentTimeline
857890 if (! timeline.isEmpty) {
858- val window = androidx.media3.common. Timeline .Window ()
891+ val window = Timeline .Window ()
859892 for (i in 0 until timeline.windowCount) {
860893 timeline.getWindow(i, window)
861894 if (window.mediaItem.mediaId.toLongOrNull() == songId) {
@@ -1041,8 +1074,8 @@ class MusicService : MediaLibraryService() {
10411074 }
10421075
10431076 val mediaId = mediaItem.mediaId
1044- val filePath = mediaItem.mediaMetadata? .extras
1045- ?.getString(com.theveloper.pixelplay.utils. MediaItemBuilder .EXTERNAL_EXTRA_FILE_PATH )
1077+ val filePath = mediaItem.mediaMetadata.extras
1078+ ?.getString(MediaItemBuilder .EXTERNAL_EXTRA_FILE_PATH )
10461079
10471080 if (filePath.isNullOrBlank()) {
10481081 Timber .tag(TAG ).d(" ReplayGain: No file path for track, keeping user-selected volume" )
@@ -1055,7 +1088,7 @@ class MusicService : MediaLibraryService() {
10551088 val useAlbumGain = replayGainUseAlbumGain
10561089 // Read ReplayGain tags on IO thread to avoid blocking main
10571090 replayGainJob = serviceScope.launch {
1058- val rgValues = kotlinx.coroutines. withContext(kotlinx.coroutines. Dispatchers .IO ) {
1091+ val rgValues = withContext(Dispatchers .IO ) {
10591092 replayGainManager.readReplayGain(filePath)
10601093 }
10611094
@@ -1078,12 +1111,14 @@ class MusicService : MediaLibraryService() {
10781111 // Store for application after transition completes
10791112 pendingReplayGainVolume = volume
10801113 Timber .tag(TAG ).d(" ReplayGain: Stored pending volume=%.2f for %s (transition running)" ,
1081- volume, mediaItem.mediaMetadata?.title)
1114+ volume, mediaItem.mediaMetadata.title
1115+ )
10821116 } else {
10831117 pendingReplayGainVolume = null
10841118 setPlayerVolume(player, volume)
10851119 Timber .tag(TAG ).d(" ReplayGain: Applied volume=%.2f for %s" ,
1086- volume, mediaItem.mediaMetadata?.title)
1120+ volume, mediaItem.mediaMetadata.title
1121+ )
10871122 }
10881123 }
10891124 }
@@ -1614,12 +1649,11 @@ class MusicService : MediaLibraryService() {
16141649 val streamDurationMs = remoteClient.streamDuration.takeIf { it > 0L }
16151650 val effectiveDurationMs = (streamDurationMs ? : durationHintMs ? : 0L ).coerceAtLeast(0L )
16161651 val imageUri = metadata
1617- ?.images
1618- ?.firstOrNull()
1619- ?.url
1620- ?.toString()
1621- ?.takeIf { it.isNotBlank() }
1622- ?.let { Uri .parse(it) }
1652+ ?.images
1653+ ?.firstOrNull()
1654+ ?.url
1655+ ?.toString()
1656+ ?.takeIf { it.isNotBlank() }?.toUri()
16231657
16241658 val mappedRepeatMode = when (mediaStatus.queueRepeatMode) {
16251659 MediaStatus .REPEAT_MODE_REPEAT_SINGLE -> Player .REPEAT_MODE_ONE
@@ -1761,7 +1795,7 @@ class MusicService : MediaLibraryService() {
17611795 var currentPosition = 0L
17621796 var totalDuration = 0L
17631797 var snapshotWindowIndex = 0
1764- var snapshotTimeline: androidx.media3.common. Timeline = androidx.media3.common. Timeline .EMPTY
1798+ var snapshotTimeline: Timeline = Timeline .EMPTY
17651799
17661800 withContext(Dispatchers .Main ) {
17671801 currentItem = player.currentMediaItem
@@ -1782,10 +1816,10 @@ class MusicService : MediaLibraryService() {
17821816 var artworkData = currentItem?.mediaMetadata?.artworkData
17831817
17841818 resolveCastRemoteSnapshot()?.let { remote ->
1785- if (! remote.title.isNullOrBlank ()) {
1819+ if (remote.title.isNotBlank ()) {
17861820 title = remote.title
17871821 }
1788- if (! remote.artist.isNullOrBlank ()) {
1822+ if (remote.artist.isNotBlank ()) {
17891823 artist = remote.artist
17901824 }
17911825 if (! remote.songId.isNullOrBlank()) {
@@ -1873,7 +1907,7 @@ class MusicService : MediaLibraryService() {
18731907 val queueItems = mutableListOf< com.theveloper.pixelplay.data.model.QueueItem > ()
18741908 // Reuse snapshotTimeline / snapshotWindowIndex captured at the top — no extra main-thread hop
18751909 if (! snapshotTimeline.isEmpty) {
1876- val window = androidx.media3.common. Timeline .Window ()
1910+ val window = Timeline .Window ()
18771911
18781912 // Empezar desde la siguiente canción en la cola
18791913 val startIndex = if (snapshotWindowIndex + 1 < snapshotTimeline.windowCount) snapshotWindowIndex + 1 else 0
@@ -1936,7 +1970,6 @@ class MusicService : MediaLibraryService() {
19361970 if (artUriString.isNullOrBlank()) {
19371971 return @withContext null to artUriString
19381972 }
1939- val safeArtUri = artUri ? : return @withContext null to artUriString
19401973
19411974 if (artUriString == cachedWidgetArtUri && cachedWidgetArtBytes != null ) {
19421975 return @withContext cachedWidgetArtBytes to artUriString
@@ -1948,7 +1981,7 @@ class MusicService : MediaLibraryService() {
19481981 }
19491982 }
19501983
1951- val loadedBytes = loadArtworkBytesForWidget(safeArtUri )
1984+ val loadedBytes = loadArtworkBytesForWidget(artUri )
19521985 if (loadedBytes != null ) {
19531986 cachedWidgetArtUri = artUriString
19541987 cachedWidgetArtBytes = loadedBytes
@@ -1969,7 +2002,7 @@ class MusicService : MediaLibraryService() {
19692002 ?.getString(MediaItemBuilder .EXTERNAL_EXTRA_ALBUM_ART )
19702003 ?.takeIf { it.isNotBlank() }
19712004 ? : return null
1972- return runCatching { Uri .parse(extrasUri ) }.getOrNull()
2005+ return runCatching { extrasUri.toUri( ) }.getOrNull()
19732006 }
19742007
19752008 private fun loadArtworkBytesForWidget (uri : Uri ): ByteArray? {
@@ -2051,12 +2084,13 @@ class MusicService : MediaLibraryService() {
20512084 }
20522085
20532086 if (glanceIds.isNotEmpty() || barGlanceIds.isNotEmpty() || controlGlanceIds.isNotEmpty() || gridGlanceIds.isNotEmpty()) {
2054- Log .d(TAG , " Widgets actualizados: ${playerInfo.songTitle} (Original: ${glanceIds.size} , Bar: ${barGlanceIds.size} , Control: ${controlGlanceIds.size} )" )
2087+ Timber .tag(TAG )
2088+ .d(" Widgets actualizados: ${playerInfo.songTitle} (Original: ${glanceIds.size} , Bar: ${barGlanceIds.size} , Control: ${controlGlanceIds.size} )" )
20552089 } else {
2056- Log .w (TAG , " No se encontraron widgets para actualizar" )
2090+ Timber .tag (TAG ).w( " No se encontraron widgets para actualizar" )
20572091 }
20582092 } catch (e: Exception ) {
2059- Log .e (TAG , " Error al actualizar el widget" , e )
2093+ Timber .tag (TAG ).e(e , " Error al actualizar el widget" )
20602094 }
20612095 }
20622096
0 commit comments