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
*/ */
@ -71,23 +74,19 @@ class NowPlayingNotification(
// starts a new MainActivity Intent when the player notification is clicked // starts a new MainActivity Intent when the player notification is clicked
// it doesn't start a completely new MainActivity because the MainActivity's launchMode // it doesn't start a completely new MainActivity because the MainActivity's launchMode
// is set to "singleTop" in the AndroidManifest (important!!!) // is set to "singleTop" in the AndroidManifest (important!!!)
// that's the only way to launch back into the previous activity (e.g. the player view // that's the only way to launch back into the previous activity (e.g. the player view
val intent = Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
if (isBackgroundPlayerNotification) { if (isBackgroundPlayerNotification) {
putExtra(IntentData.openAudioPlayer, true) putExtra(IntentData.openAudioPlayer, true)
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, PendingIntentCompat.updateCurrentFlags
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT )
)
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
} }
/** /**
@ -111,27 +110,71 @@ 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)
}
}
} }
/** /**
@ -139,32 +182,70 @@ class NowPlayingNotification(
*/ */
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)
override fun getMediaDescription( setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
player: Player, override fun getMediaDescription(
windowIndex: Int player: Player,
): MediaDescriptionCompat { windowIndex: Int
return MediaDescriptionCompat.Builder().apply { ): MediaDescriptionCompat {
setTitle(streams?.title!!) return MediaDescriptionCompat.Builder().apply {
setSubtitle(streams?.uploader) setTitle(streams?.title!!)
val extras = Bundle() setSubtitle(streams?.uploader)
val appIcon = BitmapFactory.decodeResource( val appIcon = BitmapFactory.decodeResource(
Resources.getSystem(), context.resources,
R.drawable.ic_launcher_monochrome R.drawable.ic_launcher_monochrome
)
val extras = Bundle().apply {
putParcelable(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, appIcon)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, streams?.title!!)
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, streams?.uploader)
}
setIconBitmap(appIcon)
setExtras(extras)
}.build()
}
override fun getSupportedQueueNavigatorActions(player: Player): Long {
return PlaybackStateCompat.ACTION_PLAY_PAUSE
}
})
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
) )
extras.putParcelable(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, appIcon) }
extras.putString(MediaMetadataCompat.METADATA_KEY_TITLE, streams?.title!!)
extras.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, streams?.uploader)
setIconBitmap(appIcon)
setExtras(extras)
}.build()
} }
}) PREV -> {
mediaSessionConnector.setPlayer(player) if (PlayingQueue.hasPrev()) {
PlayingQueue.onQueueItemSelected(
PlayingQueue.currentIndex() - 1
)
}
}
REWIND -> {
player.seekTo(player.currentPosition - PlayerHelper.seekIncrement)
}
FORWARD -> {
player.seekTo(player.currentPosition + PlayerHelper.seekIncrement)
}
}
} }
/** /**
@ -190,21 +271,18 @@ 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) setColorized(true)
setUseNextAction(false) setMediaSessionToken(mediaSession.sessionToken)
setUsePreviousAction(false) setSmallIcon(R.drawable.ic_launcher_lockscreen)
setUseStopAction(true) setUseNextAction(false)
setColorized(true) setUsePreviousAction(false)
setMediaSessionToken(mediaSession.sessionToken) setUseStopAction(true)
setSmallIcon(R.drawable.ic_launcher_lockscreen) }
setUseFastForwardActionInCompactView(true)
setUseRewindActionInCompactView(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( activity,
if (!isOfflinePlayer && alternativePiPControls) { R.drawable.ic_headphones,
getRemoteAction( R.string.background_mode,
activity, PlayerEvent.Background
R.drawable.ic_headphones,
R.string.background_mode,
PlayerEvent.Background
)
} else {
getRemoteAction(
activity,
R.drawable.ic_rewind,
R.string.rewind,
PlayerEvent.Rewind
)
}
) )
actions.add( val rewindAction = getRemoteAction(
getRemoteAction( activity,
activity, R.drawable.ic_rewind,
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play, R.string.rewind,
R.string.pause, PlayerEvent.Rewind
if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play
)
) )
actions.add( val playPauseAction = getRemoteAction(
if (!isOfflinePlayer && alternativePiPControls) { activity,
getRemoteAction( if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play,
activity, R.string.pause,
R.drawable.ic_next, if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play
R.string.play_next,
PlayerEvent.Next
)
} else {
getRemoteAction(
activity,
R.drawable.ic_forward,
R.string.forward,
PlayerEvent.Forward
)
}
) )
return actions
val skipNextAction = getRemoteAction(
activity,
R.drawable.ic_next,
R.string.play_next,
PlayerEvent.Next
)
val forwardAction = getRemoteAction(
activity,
R.drawable.ic_forward,
R.string.forward,
PlayerEvent.Forward
)
return if (
!isOfflinePlayer && alternativePiPControls
) {
arrayListOf(audioModeAction, playPauseAction, skipNextAction)
} else {
arrayListOf(rewindAction, playPauseAction, forwardAction)
}
} }
/** /**

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>