diff --git a/app/src/main/java/com/github/libretube/util/ImageHelper.kt b/app/src/main/java/com/github/libretube/util/ImageHelper.kt index f5aaca9b6..ca07d5865 100644 --- a/app/src/main/java/com/github/libretube/util/ImageHelper.kt +++ b/app/src/main/java/com/github/libretube/util/ImageHelper.kt @@ -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 + ) + } } diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt index 471addf10..c9295f314 100644 --- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -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 { + 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 { + 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" + } } diff --git a/app/src/main/java/com/github/libretube/util/PlayerHelper.kt b/app/src/main/java/com/github/libretube/util/PlayerHelper.kt index 560021075..008e92963 100644 --- a/app/src/main/java/com/github/libretube/util/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/util/PlayerHelper.kt @@ -397,52 +397,47 @@ object PlayerHelper { */ @RequiresApi(Build.VERSION_CODES.O) fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList { - val actions: ArrayList = 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) + } } /** diff --git a/app/src/main/res/drawable/ic_forward_md.xml b/app/src/main/res/drawable/ic_forward_md.xml new file mode 100644 index 000000000..bf1528a53 --- /dev/null +++ b/app/src/main/res/drawable/ic_forward_md.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_next_outlined.xml b/app/src/main/res/drawable/ic_next_outlined.xml new file mode 100644 index 000000000..1a8af1d37 --- /dev/null +++ b/app/src/main/res/drawable/ic_next_outlined.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_prev_outlined.xml b/app/src/main/res/drawable/ic_prev_outlined.xml new file mode 100644 index 000000000..9152dd34c --- /dev/null +++ b/app/src/main/res/drawable/ic_prev_outlined.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_rewind_md.xml b/app/src/main/res/drawable/ic_rewind_md.xml new file mode 100644 index 000000000..1e3b79b2c --- /dev/null +++ b/app/src/main/res/drawable/ic_rewind_md.xml @@ -0,0 +1,10 @@ + + +