Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(android): show controls in notification on older androids #3886

Merged
merged 2 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
freeboub marked this conversation as resolved.
Show resolved Hide resolved
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