Merge pull request #2713 from Bnyro/master

Improved player notification
This commit is contained in:
Bnyro 2023-01-16 15:01:33 +01:00 committed by GitHub
commit 6703a866c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 242 additions and 106 deletions

View File

@ -88,4 +88,20 @@ object ImageHelper {
} }
return null return null
} }
/**
* Get a squared bitmap with the same width and height from a bitmap
* @param bitmap The bitmap to resize
*/
fun getSquareBitmap(bitmap: Bitmap?): Bitmap? {
bitmap ?: return null
val newSize = minOf(bitmap.width, bitmap.height)
return Bitmap.createBitmap(
bitmap,
(bitmap.width - newSize) / 2,
(bitmap.height - newSize) / 2,
newSize,
newSize
)
}
} }

View File

@ -5,7 +5,6 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
@ -14,9 +13,13 @@ import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import coil.request.ImageRequest import coil.request.ImageRequest
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Streams
import com.github.libretube.compat.PendingIntentCompat
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
@ -26,6 +29,7 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.ui.PlayerNotificationManager.CustomActionReceiver
class NowPlayingNotification( class NowPlayingNotification(
private val context: Context, private val context: Context,
@ -51,11 +55,10 @@ class NowPlayingNotification(
private var playerNotification: PlayerNotificationManager? = null private var playerNotification: PlayerNotificationManager? = null
/** /**
* The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification * The [descriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
* Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest) * Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
*/ */
inner class DescriptionAdapter : private val descriptionAdapter = object : PlayerNotificationManager.MediaDescriptionAdapter {
PlayerNotificationManager.MediaDescriptionAdapter {
/** /**
* sets the title of the notification * sets the title of the notification
*/ */
@ -78,16 +81,12 @@ class NowPlayingNotification(
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
} }
} }
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return PendingIntent.getActivity(
PendingIntent.getActivity(
context, context,
0, 0,
intent, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntentCompat.updateCurrentFlags
) )
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
} }
/** /**
@ -111,39 +110,85 @@ class NowPlayingNotification(
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(streams?.thumbnailUrl) .data(streams?.thumbnailUrl)
.target { result -> .target { result ->
bitmap = (result as BitmapDrawable).bitmap val bm = (result as BitmapDrawable).bitmap
// returns the bitmap on Android 13+, for everything below scaled down to a square
bitmap = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
ImageHelper.getSquareBitmap(bm)
} else {
bm
}
callback.onBitmap(bitmap!!)
} }
.build() .build()
// enqueue the thumbnail loading request
ImageHelper.imageLoader.enqueue(request) ImageHelper.imageLoader.enqueue(request)
// returns the bitmap on Android 13+, for everything below scaled down to a square return bitmap
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSquareBitmap(bitmap) else bitmap }
override fun getCurrentSubText(player: Player): CharSequence? {
return streams?.uploader
} }
} }
private fun getSquareBitmap(bitmap: Bitmap?): Bitmap? { private val customActionReceiver = object : CustomActionReceiver {
bitmap ?: return null override fun createCustomActions(
val newSize = minOf(bitmap.width, bitmap.height) context: Context,
return Bitmap.createBitmap( instanceId: Int
bitmap, ): MutableMap<String, NotificationCompat.Action> {
(bitmap.width - newSize) / 2, return mutableMapOf(
(bitmap.height - newSize) / 2, PREV to createNotificationAction(R.drawable.ic_prev_outlined, PREV, instanceId),
newSize, NEXT to createNotificationAction(R.drawable.ic_next_outlined, NEXT, instanceId),
newSize REWIND to createNotificationAction(R.drawable.ic_rewind_md, REWIND, instanceId),
FORWARD to createNotificationAction(R.drawable.ic_forward_md, FORWARD, instanceId)
) )
} }
override fun getCustomActions(player: Player): MutableList<String> {
return mutableListOf(PREV, NEXT, REWIND, FORWARD)
}
override fun onCustomAction(player: Player, action: String, intent: Intent) {
handlePlayerAction(action)
}
}
private fun createNotificationAction(drawableRes: Int, actionName: String, instanceId: Int): NotificationCompat.Action {
val intent: Intent = Intent(actionName).setPackage(context.packageName)
val pendingIntent = PendingIntent.getBroadcast(
context,
instanceId,
intent,
PendingIntentCompat.cancelCurrentFlags
)
return NotificationCompat.Action.Builder(drawableRes, actionName, pendingIntent).build()
}
private fun createMediaSessionAction(@DrawableRes drawableRes: Int, actionName: String): MediaSessionConnector.CustomActionProvider {
return object : MediaSessionConnector.CustomActionProvider {
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
return PlaybackStateCompat.CustomAction.Builder(actionName, actionName, drawableRes).build()
}
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
handlePlayerAction(action)
}
}
}
/** /**
* Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player * Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player
*/ */
private fun createMediaSession() { private fun createMediaSession() {
if (this::mediaSession.isInitialized) return if (this::mediaSession.isInitialized) return
mediaSession = MediaSessionCompat(context, this.javaClass.name) mediaSession = MediaSessionCompat(context, this.javaClass.name).apply {
mediaSession.isActive = true isActive = true
}
mediaSessionConnector = MediaSessionConnector(mediaSession) mediaSessionConnector = MediaSessionConnector(mediaSession).apply {
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) { setPlayer(player)
setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription( override fun getMediaDescription(
player: Player, player: Player,
windowIndex: Int windowIndex: Int
@ -151,20 +196,56 @@ class NowPlayingNotification(
return MediaDescriptionCompat.Builder().apply { return MediaDescriptionCompat.Builder().apply {
setTitle(streams?.title!!) setTitle(streams?.title!!)
setSubtitle(streams?.uploader) setSubtitle(streams?.uploader)
val extras = Bundle()
val appIcon = BitmapFactory.decodeResource( val appIcon = BitmapFactory.decodeResource(
Resources.getSystem(), context.resources,
R.drawable.ic_launcher_monochrome R.drawable.ic_launcher_monochrome
) )
extras.putParcelable(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, appIcon) val extras = Bundle().apply {
extras.putString(MediaMetadataCompat.METADATA_KEY_TITLE, streams?.title!!) putParcelable(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, appIcon)
extras.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, streams?.uploader) putString(MediaMetadataCompat.METADATA_KEY_TITLE, streams?.title!!)
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, streams?.uploader)
}
setIconBitmap(appIcon) setIconBitmap(appIcon)
setExtras(extras) setExtras(extras)
}.build() }.build()
} }
override fun getSupportedQueueNavigatorActions(player: Player): Long {
return PlaybackStateCompat.ACTION_PLAY_PAUSE
}
}) })
mediaSessionConnector.setPlayer(player) setCustomActionProviders(
createMediaSessionAction(R.drawable.ic_prev_outlined, PREV),
createMediaSessionAction(R.drawable.ic_next_outlined, NEXT),
createMediaSessionAction(R.drawable.ic_rewind_md, REWIND),
createMediaSessionAction(R.drawable.ic_forward_md, FORWARD)
)
}
}
private fun handlePlayerAction(action: String) {
when (action) {
NEXT -> {
if (PlayingQueue.hasNext()) {
PlayingQueue.onQueueItemSelected(
PlayingQueue.currentIndex() + 1
)
}
}
PREV -> {
if (PlayingQueue.hasPrev()) {
PlayingQueue.onQueueItemSelected(
PlayingQueue.currentIndex() - 1
)
}
}
REWIND -> {
player.seekTo(player.currentPosition - PlayerHelper.seekIncrement)
}
FORWARD -> {
player.seekTo(player.currentPosition + PlayerHelper.seekIncrement)
}
}
} }
/** /**
@ -190,20 +271,17 @@ class NowPlayingNotification(
playerNotification = PlayerNotificationManager playerNotification = PlayerNotificationManager
.Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID) .Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
// set the description of the notification // set the description of the notification
.setMediaDescriptionAdapter( .setMediaDescriptionAdapter(descriptionAdapter)
DescriptionAdapter() // register the receiver for custom actions, doesn't seem to change anything
) .setCustomActionReceiver(customActionReceiver)
.build() .build().apply {
playerNotification?.apply {
setPlayer(player) setPlayer(player)
setUseNextAction(false)
setUsePreviousAction(false)
setUseStopAction(true)
setColorized(true) setColorized(true)
setMediaSessionToken(mediaSession.sessionToken) setMediaSessionToken(mediaSession.sessionToken)
setSmallIcon(R.drawable.ic_launcher_lockscreen) setSmallIcon(R.drawable.ic_launcher_lockscreen)
setUseFastForwardActionInCompactView(true) setUseNextAction(false)
setUseRewindActionInCompactView(true) setUsePreviousAction(false)
setUseStopAction(true)
} }
} }
@ -224,4 +302,11 @@ class NowPlayingNotification(
) as NotificationManager ) as NotificationManager
notificationManager.cancel(PLAYER_NOTIFICATION_ID) notificationManager.cancel(PLAYER_NOTIFICATION_ID)
} }
companion object {
private const val PREV = "prev"
private const val NEXT = "next"
private const val REWIND = "rewind"
private const val FORWARD = "forward"
}
} }

View File

@ -397,52 +397,47 @@ object PlayerHelper {
*/ */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList<RemoteAction> { fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList<RemoteAction> {
val actions: ArrayList<RemoteAction> = ArrayList() val audioModeAction = getRemoteAction(
actions.add(
if (!isOfflinePlayer && alternativePiPControls) {
getRemoteAction(
activity, activity,
R.drawable.ic_headphones, R.drawable.ic_headphones,
R.string.background_mode, R.string.background_mode,
PlayerEvent.Background PlayerEvent.Background
) )
} else {
getRemoteAction( val rewindAction = getRemoteAction(
activity, activity,
R.drawable.ic_rewind, R.drawable.ic_rewind,
R.string.rewind, R.string.rewind,
PlayerEvent.Rewind PlayerEvent.Rewind
) )
}
)
actions.add( val playPauseAction = getRemoteAction(
getRemoteAction(
activity, activity,
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play, if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play,
R.string.pause, R.string.pause,
if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play
) )
)
actions.add( val skipNextAction = getRemoteAction(
if (!isOfflinePlayer && alternativePiPControls) {
getRemoteAction(
activity, activity,
R.drawable.ic_next, R.drawable.ic_next,
R.string.play_next, R.string.play_next,
PlayerEvent.Next PlayerEvent.Next
) )
} else {
getRemoteAction( val forwardAction = getRemoteAction(
activity, activity,
R.drawable.ic_forward, R.drawable.ic_forward,
R.string.forward, R.string.forward,
PlayerEvent.Forward PlayerEvent.Forward
) )
return if (
!isOfflinePlayer && alternativePiPControls
) {
arrayListOf(audioModeAction, playPauseAction, skipNextAction)
} else {
arrayListOf(rewindAction, playPauseAction, forwardAction)
} }
)
return actions
} }
/** /**

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,18l8.5,-6L6,6v12zM8,9.86L11.03,12 8,14.14L8,9.86zM16,6h2v12h-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6l-8.5,6zM16,14.14L12.97,12 16,9.86v4.28z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z" />
</vector>