fix(android): show controls in notification on older androids (#3886)
This commit is contained in:
parent
2d793dbde1
commit
098a754110
@ -5,9 +5,12 @@ import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.brentvatne.exoplayer.VideoPlaybackService.Companion.COMMAND
|
||||
import com.brentvatne.exoplayer.VideoPlaybackService.Companion.commandFromString
|
||||
import com.brentvatne.exoplayer.VideoPlaybackService.Companion.handleCommand
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
class VideoPlaybackCallback(private val seekIntervalMS: Long) : MediaSession.Callback {
|
||||
class VideoPlaybackCallback : MediaSession.Callback {
|
||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||
try {
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
@ -18,8 +21,8 @@ class VideoPlaybackCallback(private val seekIntervalMS: Long) : MediaSession.Cal
|
||||
.build()
|
||||
).setAvailableSessionCommands(
|
||||
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||
.add(SessionCommand(VideoPlaybackService.COMMAND_SEEK_FORWARD, Bundle.EMPTY))
|
||||
.add(SessionCommand(VideoPlaybackService.COMMAND_SEEK_BACKWARD, Bundle.EMPTY))
|
||||
.add(SessionCommand(COMMAND.SEEK_FORWARD.stringValue, Bundle.EMPTY))
|
||||
.add(SessionCommand(COMMAND.SEEK_BACKWARD.stringValue, Bundle.EMPTY))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
@ -34,10 +37,7 @@ class VideoPlaybackCallback(private val seekIntervalMS: Long) : MediaSession.Cal
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
when (customCommand.customAction) {
|
||||
VideoPlaybackService.COMMAND_SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + seekIntervalMS)
|
||||
VideoPlaybackService.COMMAND_SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition - seekIntervalMS)
|
||||
}
|
||||
handleCommand(commandFromString(customCommand.customAction), session)
|
||||
return super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package com.brentvatne.exoplayer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
@ -18,6 +19,7 @@ import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.MediaStyleNotificationHelper
|
||||
import androidx.media3.session.SessionCommand
|
||||
import com.brentvatne.common.toolbox.DebugLog
|
||||
import okhttp3.internal.immutableListOf
|
||||
|
||||
class PlaybackServiceBinder(val service: VideoPlaybackService) : Binder()
|
||||
@ -27,9 +29,9 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
private var binder = PlaybackServiceBinder(this)
|
||||
private var sourceActivity: Class<Activity>? = null
|
||||
|
||||
// Controls
|
||||
private val commandSeekForward = SessionCommand(COMMAND_SEEK_FORWARD, Bundle.EMPTY)
|
||||
private val commandSeekBackward = SessionCommand(COMMAND_SEEK_BACKWARD, Bundle.EMPTY)
|
||||
// Controls for Android 13+ - see buildNotification function
|
||||
private val commandSeekForward = SessionCommand(COMMAND.SEEK_FORWARD.stringValue, Bundle.EMPTY)
|
||||
private val commandSeekBackward = SessionCommand(COMMAND.SEEK_BACKWARD.stringValue, Bundle.EMPTY)
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private val seekForwardBtn = CommandButton.Builder()
|
||||
@ -55,7 +57,7 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
|
||||
val mediaSession = MediaSession.Builder(this, player)
|
||||
.setId("RNVideoPlaybackService_" + player.hashCode())
|
||||
.setCallback(VideoPlaybackCallback(SEEK_INTERVAL_MS))
|
||||
.setCallback(VideoPlaybackCallback())
|
||||
.setCustomLayout(immutableListOf(seekBackwardBtn, seekForwardBtn))
|
||||
.build()
|
||||
|
||||
@ -115,16 +117,90 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
return
|
||||
}
|
||||
|
||||
val notification = buildNotification(session)
|
||||
|
||||
notificationManager.notify(session.player.hashCode(), notification)
|
||||
}
|
||||
|
||||
private fun buildNotification(session: MediaSession): Notification {
|
||||
val returnToPlayer = Intent(this, sourceActivity).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val notificationCompact = NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
.build()
|
||||
|
||||
notificationManager.notify(session.player.hashCode(), notificationCompact)
|
||||
/*
|
||||
* On Android 13+ controls are automatically handled via media session
|
||||
* On Android 12 and bellow we need to add controls manually
|
||||
*/
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
.build()
|
||||
} else {
|
||||
val playerId = session.player.hashCode()
|
||||
|
||||
// Action for COMMAND.SEEK_BACKWARD
|
||||
val seekBackwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
|
||||
putExtra("PLAYER_ID", playerId)
|
||||
putExtra("ACTION", COMMAND.SEEK_BACKWARD.stringValue)
|
||||
}
|
||||
val seekBackwardPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
playerId * 10,
|
||||
seekBackwardIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// ACTION FOR COMMAND.TOGGLE_PLAY
|
||||
val togglePlayIntent = Intent(this, VideoPlaybackService::class.java).apply {
|
||||
putExtra("PLAYER_ID", playerId)
|
||||
putExtra("ACTION", COMMAND.TOGGLE_PLAY.stringValue)
|
||||
}
|
||||
val togglePlayPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
playerId * 10 + 1,
|
||||
togglePlayIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// ACTION FOR COMMAND.SEEK_FORWARD
|
||||
val seekForwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
|
||||
putExtra("PLAYER_ID", playerId)
|
||||
putExtra("ACTION", COMMAND.SEEK_FORWARD.stringValue)
|
||||
}
|
||||
val seekForwardPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
playerId * 10 + 2,
|
||||
seekForwardIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
// Show controls on lock screen even when user hides sensitive content.
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
// Add media control buttons that invoke intents in your media service
|
||||
.addAction(androidx.media3.session.R.drawable.media3_notification_seek_back, "Seek Backward", seekBackwardPendingIntent) // #0
|
||||
.addAction(
|
||||
if (session.player.isPlaying) {
|
||||
androidx.media3.session.R.drawable.media3_notification_pause
|
||||
} else {
|
||||
androidx.media3.session.R.drawable.media3_notification_play
|
||||
},
|
||||
"Toggle Play",
|
||||
togglePlayPendingIntent
|
||||
) // #1
|
||||
.addAction(androidx.media3.session.R.drawable.media3_notification_seek_forward, "Seek Forward", seekForwardPendingIntent) // #2
|
||||
// Apply the media style template
|
||||
.setStyle(MediaStyleNotificationHelper.MediaStyle(session).setShowActionsInCompactView(0, 1, 2))
|
||||
.setContentTitle(session.player.mediaMetadata.title)
|
||||
.setContentText(session.player.mediaMetadata.description)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
.setLargeIcon(session.player.mediaMetadata.artworkUri?.let { session.bitmapLoader.loadBitmap(it).get() })
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hidePlayerNotification(player: ExoPlayer) {
|
||||
@ -148,10 +224,63 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
mediaSessionsList.clear()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.let {
|
||||
val playerId = it.getIntExtra("PLAYER_ID", -1)
|
||||
val actionCommand = it.getStringExtra("ACTION")
|
||||
|
||||
if (playerId < 0) {
|
||||
DebugLog.w(TAG, "Received Command without playerId")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
if (actionCommand == null) {
|
||||
DebugLog.w(TAG, "Received Command without action command")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
val session = mediaSessionsList.values.find { s -> s.player.hashCode() == playerId } ?: return super.onStartCommand(intent, flags, startId)
|
||||
|
||||
handleCommand(commandFromString(actionCommand), session)
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val COMMAND_SEEK_FORWARD = "SEEK_FORWARD"
|
||||
const val COMMAND_SEEK_BACKWARD = "SEEK_BACKWARD"
|
||||
private const val SEEK_INTERVAL_MS = 10000L
|
||||
private const val TAG = "VideoPlaybackService"
|
||||
|
||||
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
|
||||
const val SEEK_INTERVAL_MS = 10000L
|
||||
|
||||
enum class COMMAND(val stringValue: String) {
|
||||
NONE("NONE"),
|
||||
SEEK_FORWARD("COMMAND_SEEK_FORWARD"),
|
||||
SEEK_BACKWARD("COMMAND_SEEK_BACKWARD"),
|
||||
TOGGLE_PLAY("COMMAND_TOGGLE_PLAY"),
|
||||
PLAY("COMMAND_PLAY"),
|
||||
PAUSE("COMMAND_PAUSE")
|
||||
}
|
||||
|
||||
fun commandFromString(value: String): COMMAND =
|
||||
when (value) {
|
||||
COMMAND.SEEK_FORWARD.stringValue -> COMMAND.SEEK_FORWARD
|
||||
COMMAND.SEEK_BACKWARD.stringValue -> COMMAND.SEEK_BACKWARD
|
||||
COMMAND.TOGGLE_PLAY.stringValue -> COMMAND.TOGGLE_PLAY
|
||||
COMMAND.PLAY.stringValue -> COMMAND.PLAY
|
||||
COMMAND.PAUSE.stringValue -> COMMAND.PAUSE
|
||||
else -> COMMAND.NONE
|
||||
}
|
||||
fun handleCommand(command: COMMAND, session: MediaSession) {
|
||||
// TODO: get somehow ControlsConfig here - for now hardcoded 10000ms
|
||||
|
||||
when (command) {
|
||||
COMMAND.SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition - SEEK_INTERVAL_MS)
|
||||
COMMAND.SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + SEEK_INTERVAL_MS)
|
||||
COMMAND.TOGGLE_PLAY -> handleCommand(if (session.player.isPlaying) COMMAND.PAUSE else COMMAND.PLAY, session)
|
||||
COMMAND.PLAY -> session.player.play()
|
||||
COMMAND.PAUSE -> session.player.pause()
|
||||
else -> DebugLog.w(TAG, "Received COMMAND.NONE - was there an error?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user