Skip to content

Commit

Permalink
fix(android): show controls in notification on older androids (#3886)
Browse files Browse the repository at this point in the history
  • Loading branch information
KrzysztofMoch authored Jun 12, 2024
1 parent 2d793db commit 098a754
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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)
}
}
155 changes: 142 additions & 13 deletions android/src/main/java/com/brentvatne/exoplayer/VideoPlaybackService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()

Expand Down Expand Up @@ -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) {
Expand All @@ -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?")
}
}
}
}

0 comments on commit 098a754

Please sign in to comment.