mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
Merge pull request #2713 from Bnyro/master
Improved player notification
This commit is contained in:
commit
6703a866c8
@ -88,4 +88,20 @@ object ImageHelper {
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
@ -14,9 +13,13 @@ import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
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 com.github.libretube.R
|
||||
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.IntentData
|
||||
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.TimelineQueueNavigator
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager.CustomActionReceiver
|
||||
|
||||
class NowPlayingNotification(
|
||||
private val context: Context,
|
||||
@ -51,11 +55,10 @@ class NowPlayingNotification(
|
||||
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)
|
||||
*/
|
||||
inner class DescriptionAdapter :
|
||||
PlayerNotificationManager.MediaDescriptionAdapter {
|
||||
private val descriptionAdapter = object : PlayerNotificationManager.MediaDescriptionAdapter {
|
||||
/**
|
||||
* sets the title of the notification
|
||||
*/
|
||||
@ -71,23 +74,19 @@ class NowPlayingNotification(
|
||||
// starts a new MainActivity Intent when the player notification is clicked
|
||||
// it doesn't start a completely new MainActivity because the MainActivity's launchMode
|
||||
// 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 {
|
||||
if (isBackgroundPlayerNotification) {
|
||||
putExtra(IntentData.openAudioPlayer, true)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntentCompat.updateCurrentFlags
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,27 +110,71 @@ class NowPlayingNotification(
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(streams?.thumbnailUrl)
|
||||
.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()
|
||||
|
||||
// enqueue the thumbnail loading request
|
||||
ImageHelper.imageLoader.enqueue(request)
|
||||
|
||||
// returns the bitmap on Android 13+, for everything below scaled down to a square
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSquareBitmap(bitmap) else bitmap
|
||||
return bitmap
|
||||
}
|
||||
|
||||
override fun getCurrentSubText(player: Player): CharSequence? {
|
||||
return streams?.uploader
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
private val customActionReceiver = object : CustomActionReceiver {
|
||||
override fun createCustomActions(
|
||||
context: Context,
|
||||
instanceId: Int
|
||||
): MutableMap<String, NotificationCompat.Action> {
|
||||
return mutableMapOf(
|
||||
PREV to createNotificationAction(R.drawable.ic_prev_outlined, PREV, instanceId),
|
||||
NEXT to createNotificationAction(R.drawable.ic_next_outlined, NEXT, instanceId),
|
||||
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() {
|
||||
if (this::mediaSession.isInitialized) return
|
||||
mediaSession = MediaSessionCompat(context, this.javaClass.name)
|
||||
mediaSession.isActive = true
|
||||
mediaSession = MediaSessionCompat(context, this.javaClass.name).apply {
|
||||
isActive = true
|
||||
}
|
||||
|
||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
|
||||
override fun getMediaDescription(
|
||||
player: Player,
|
||||
windowIndex: Int
|
||||
): MediaDescriptionCompat {
|
||||
return MediaDescriptionCompat.Builder().apply {
|
||||
setTitle(streams?.title!!)
|
||||
setSubtitle(streams?.uploader)
|
||||
val extras = Bundle()
|
||||
val appIcon = BitmapFactory.decodeResource(
|
||||
Resources.getSystem(),
|
||||
R.drawable.ic_launcher_monochrome
|
||||
mediaSessionConnector = MediaSessionConnector(mediaSession).apply {
|
||||
setPlayer(player)
|
||||
setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
|
||||
override fun getMediaDescription(
|
||||
player: Player,
|
||||
windowIndex: Int
|
||||
): MediaDescriptionCompat {
|
||||
return MediaDescriptionCompat.Builder().apply {
|
||||
setTitle(streams?.title!!)
|
||||
setSubtitle(streams?.uploader)
|
||||
val appIcon = BitmapFactory.decodeResource(
|
||||
context.resources,
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
mediaSessionConnector.setPlayer(player)
|
||||
PREV -> {
|
||||
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
|
||||
.Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
|
||||
// set the description of the notification
|
||||
.setMediaDescriptionAdapter(
|
||||
DescriptionAdapter()
|
||||
)
|
||||
.build()
|
||||
playerNotification?.apply {
|
||||
setPlayer(player)
|
||||
setUseNextAction(false)
|
||||
setUsePreviousAction(false)
|
||||
setUseStopAction(true)
|
||||
setColorized(true)
|
||||
setMediaSessionToken(mediaSession.sessionToken)
|
||||
setSmallIcon(R.drawable.ic_launcher_lockscreen)
|
||||
setUseFastForwardActionInCompactView(true)
|
||||
setUseRewindActionInCompactView(true)
|
||||
}
|
||||
.setMediaDescriptionAdapter(descriptionAdapter)
|
||||
// register the receiver for custom actions, doesn't seem to change anything
|
||||
.setCustomActionReceiver(customActionReceiver)
|
||||
.build().apply {
|
||||
setPlayer(player)
|
||||
setColorized(true)
|
||||
setMediaSessionToken(mediaSession.sessionToken)
|
||||
setSmallIcon(R.drawable.ic_launcher_lockscreen)
|
||||
setUseNextAction(false)
|
||||
setUsePreviousAction(false)
|
||||
setUseStopAction(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -224,4 +302,11 @@ class NowPlayingNotification(
|
||||
) as NotificationManager
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -397,52 +397,47 @@ object PlayerHelper {
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList<RemoteAction> {
|
||||
val actions: ArrayList<RemoteAction> = ArrayList()
|
||||
actions.add(
|
||||
if (!isOfflinePlayer && alternativePiPControls) {
|
||||
getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_headphones,
|
||||
R.string.background_mode,
|
||||
PlayerEvent.Background
|
||||
)
|
||||
} else {
|
||||
getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_rewind,
|
||||
R.string.rewind,
|
||||
PlayerEvent.Rewind
|
||||
)
|
||||
}
|
||||
val audioModeAction = getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_headphones,
|
||||
R.string.background_mode,
|
||||
PlayerEvent.Background
|
||||
)
|
||||
|
||||
actions.add(
|
||||
getRemoteAction(
|
||||
activity,
|
||||
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play,
|
||||
R.string.pause,
|
||||
if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play
|
||||
)
|
||||
val rewindAction = getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_rewind,
|
||||
R.string.rewind,
|
||||
PlayerEvent.Rewind
|
||||
)
|
||||
|
||||
actions.add(
|
||||
if (!isOfflinePlayer && alternativePiPControls) {
|
||||
getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_next,
|
||||
R.string.play_next,
|
||||
PlayerEvent.Next
|
||||
)
|
||||
} else {
|
||||
getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_forward,
|
||||
R.string.forward,
|
||||
PlayerEvent.Forward
|
||||
)
|
||||
}
|
||||
val playPauseAction = getRemoteAction(
|
||||
activity,
|
||||
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play,
|
||||
R.string.pause,
|
||||
if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
10
app/src/main/res/drawable/ic_forward_md.xml
Normal file
10
app/src/main/res/drawable/ic_forward_md.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_next_outlined.xml
Normal file
10
app/src/main/res/drawable/ic_next_outlined.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_prev_outlined.xml
Normal file
10
app/src/main/res/drawable/ic_prev_outlined.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_rewind_md.xml
Normal file
10
app/src/main/res/drawable/ic_rewind_md.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user