Files
musicdl-catalog-sync-suite/MusicFree/patches/react-native-track-player+4.1.1.patch

254 lines
12 KiB
Diff

diff --git a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
index 9d6d869..8f149f8 100644
--- a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
+++ b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
@@ -4,17 +4,23 @@ import android.app.*
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
+import android.graphics.Bitmap
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.support.v4.media.RatingCompat
+import android.support.v4.media.session.MediaSessionCompat
+import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_LOW
+import androidx.core.app.NotificationManagerCompat
+import androidx.media.session.MediaButtonReceiver
import com.doublesymmetry.kotlinaudio.models.*
import com.doublesymmetry.kotlinaudio.models.NotificationButton.*
+import com.doublesymmetry.kotlinaudio.notification.NotificationManager as KotlinAudioNotificationManager
import com.doublesymmetry.kotlinaudio.players.QueuedAudioPlayer
import com.doublesymmetry.trackplayer.R as TrackPlayerR
import com.doublesymmetry.trackplayer.extensions.NumberExt.Companion.toMilliseconds
@@ -94,11 +100,52 @@ class MusicService : HeadlessJsTaskService() {
private var compactCapabilities: List<Capability> = emptyList()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (handleNotificationAction(intent)) {
+ return START_STICKY
+ }
startTask(getTaskConfig(intent))
startAndStopEmptyNotificationToAvoidANR()
return START_STICKY
}
+ private fun handleNotificationAction(intent: Intent?): Boolean {
+ val action = intent?.action ?: return false
+ if (!this::player.isInitialized) {
+ Timber.w("Ignoring notification action before player initialization: %s", action)
+ return true
+ }
+ when (action) {
+ ACTION_NOTIFICATION_PLAY -> play()
+ ACTION_NOTIFICATION_PAUSE -> pause()
+ ACTION_NOTIFICATION_NEXT -> skipToNext()
+ ACTION_NOTIFICATION_PREVIOUS -> {
+ dispatchPlaybackButtonEvent(MusicEvents.BUTTON_SKIP_PREVIOUS)
+ }
+ else -> return false
+ }
+ Timber.d("Handled explicit notification action: %s", action)
+ return true
+ }
+
+ private fun dispatchPlaybackButtonEvent(event: String, data: Bundle? = null) {
+ val reactInstanceManager = reactNativeHost.reactInstanceManager
+ if (reactInstanceManager.currentReactContext != null) {
+ emit(event, data)
+ return
+ }
+ startTask(getTaskConfig(null))
+ scope.launch {
+ repeat(40) {
+ if (reactInstanceManager.currentReactContext != null) {
+ emit(event, data)
+ return@launch
+ }
+ delay(100)
+ }
+ Timber.w("Timed out waiting for React context before dispatching event: %s", event)
+ }
+ }
+
/**
* Workaround for the "Context.startForegroundService() did not then call Service.startForeground()"
* within 5s" ANR and crash by creating an empty notification and stopping it right after. For more
@@ -236,6 +283,11 @@ class MusicService : HeadlessJsTaskService() {
val notificationConfig = NotificationConfig(buttonsList, accentColor, smallIcon, pendingIntent)
player.notificationManager.createNotification(notificationConfig)
+ scope.launch {
+ delay(500)
+ player.notificationManager.createNotification(notificationConfig)
+ player.notificationManager.invalidate()
+ }
// setup progress update events if configured
progressUpdateJob?.cancel()
@@ -279,6 +331,138 @@ class MusicService : HeadlessJsTaskService() {
}
}
+ private fun hasNotificationCapability(capability: Capability): Boolean {
+ return notificationCapabilities.contains(capability) || capabilities.contains(capability)
+ }
+
+ private fun getMediaSessionCompat(): MediaSessionCompat? {
+ return try {
+ val field = KotlinAudioNotificationManager::class.java.getDeclaredField("mediaSession")
+ field.isAccessible = true
+ field.get(player.notificationManager) as? MediaSessionCompat
+ } catch (error: Exception) {
+ Timber.w(error, "Failed to access media session from notification manager")
+ null
+ }
+ }
+
+ private fun buildTransportControlNotification(original: Notification): Notification {
+ val builder = NotificationCompat.Builder(this, getNotificationChannelId(original))
+ .setContentTitle(original.extras?.getCharSequence(Notification.EXTRA_TITLE))
+ .setContentText(original.extras?.getCharSequence(Notification.EXTRA_TEXT))
+ .setSubText(original.extras?.getCharSequence(Notification.EXTRA_SUB_TEXT))
+ .setContentIntent(original.contentIntent)
+ .setDeleteIntent(original.deleteIntent)
+ .setCategory(original.category ?: Notification.CATEGORY_TRANSPORT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setOnlyAlertOnce(true)
+ .setOngoing(player.playWhenReady)
+ .setShowWhen(original.`when` > 0)
+ .setWhen(original.`when`)
+ .setSmallIcon(ExoPlayerR.drawable.exo_notification_small_icon)
+ val mediaSession = getMediaSessionCompat()
+ val compactActionIndexes = mutableListOf<Int>()
+
+ getNotificationLargeIcon(original)?.let { builder.setLargeIcon(it) }
+ if (original.color != 0) {
+ builder.color = original.color
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
+ }
+
+ if (hasNotificationCapability(Capability.SKIP_TO_PREVIOUS)) {
+ val previousIntent = createServiceActionPendingIntent(ACTION_NOTIFICATION_PREVIOUS, 1)
+ if (previousIntent != null) {
+ builder.addAction(
+ ExoPlayerR.drawable.exo_notification_previous,
+ "Previous",
+ previousIntent
+ )
+ compactActionIndexes += compactActionIndexes.size
+ }
+ }
+
+ if (hasNotificationCapability(Capability.PLAY) || hasNotificationCapability(Capability.PAUSE)) {
+ val playPauseIntent = if (player.isPlaying) {
+ createServiceActionPendingIntent(ACTION_NOTIFICATION_PAUSE, 2)
+ } else {
+ createServiceActionPendingIntent(ACTION_NOTIFICATION_PLAY, 2)
+ }
+ if (playPauseIntent != null) {
+ builder.addAction(
+ if (player.isPlaying) ExoPlayerR.drawable.exo_notification_pause
+ else ExoPlayerR.drawable.exo_notification_play,
+ if (player.isPlaying) "Pause" else "Play",
+ playPauseIntent
+ )
+ compactActionIndexes += compactActionIndexes.size
+ }
+ }
+
+ if (hasNotificationCapability(Capability.SKIP_TO_NEXT)) {
+ val nextIntent = createServiceActionPendingIntent(ACTION_NOTIFICATION_NEXT, 3)
+ if (nextIntent != null) {
+ builder.addAction(
+ ExoPlayerR.drawable.exo_notification_next,
+ "Next",
+ nextIntent
+ )
+ compactActionIndexes += compactActionIndexes.size
+ }
+ }
+
+ val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
+ mediaSession?.sessionToken?.let { mediaStyle.setMediaSession(it) }
+ if (compactActionIndexes.isNotEmpty()) {
+ mediaStyle.setShowActionsInCompactView(*compactActionIndexes.toIntArray())
+ }
+
+ builder.setStyle(mediaStyle)
+ return builder.build()
+ }
+
+ private fun createServiceActionPendingIntent(action: String, requestCode: Int): PendingIntent? {
+ val intent = Intent(this, MusicService::class.java).apply {
+ this.action = action
+ `package` = packageName
+ }
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ return PendingIntent.getService(this, requestCode, intent, flags)
+ }
+
+ private fun getNotificationChannelId(original: Notification): String {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ original.channelId ?: getString(TrackPlayerR.string.rntp_temporary_channel_id)
+ } else {
+ getString(TrackPlayerR.string.rntp_temporary_channel_id)
+ }
+ }
+
+ private fun getNotificationLargeIcon(original: Notification): Bitmap? {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
+ original.getLargeIcon()?.loadDrawable(this)?.let { drawable ->
+ val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 1
+ val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: 1
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { bitmap ->
+ val canvas = android.graphics.Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+ }
+ }
+ }
+ else -> {
+ @Suppress("DEPRECATION")
+ original.largeIcon
+ }
+ }
+ }
+
private fun isCompact(capability: Capability): Boolean {
return compactCapabilities.contains(capability)
}
@@ -573,7 +757,8 @@ class MusicService : HeadlessJsTaskService() {
is NotificationState.POSTED -> {
Timber.d("notification posted with id=%s, ongoing=%s", it.notificationId, it.ongoing)
notificationId = it.notificationId;
- notification = it.notification;
+ notification = buildTransportControlNotification(it.notification)
+ NotificationManagerCompat.from(this@MusicService).notify(notificationId!!, notification!!)
if (it.ongoing) {
if (player.playWhenReady) {
startForegroundIfNecessary()
@@ -850,5 +1035,10 @@ class MusicService : HeadlessJsTaskService() {
const val DEFAULT_JUMP_INTERVAL = 15.0
const val DEFAULT_STOP_FOREGROUND_GRACE_PERIOD = 5
+
+ private const val ACTION_NOTIFICATION_PLAY = "com.doublesymmetry.trackplayer.notification.PLAY"
+ private const val ACTION_NOTIFICATION_PAUSE = "com.doublesymmetry.trackplayer.notification.PAUSE"
+ private const val ACTION_NOTIFICATION_NEXT = "com.doublesymmetry.trackplayer.notification.NEXT"
+ private const val ACTION_NOTIFICATION_PREVIOUS = "com.doublesymmetry.trackplayer.notification.PREVIOUS"
}
}