From 126b43037fb5c0dc514d06d72f7d53c6d26ff11a Mon Sep 17 00:00:00 2001 From: Stypox Date: Thu, 16 Nov 2023 11:04:01 +0100 Subject: [PATCH] Allow customizing notification actions on Android 13+ Use a workaround initially suggested in https://github.com/TeamNewPipe/NewPipe/pull/10567 Basically tell the system that we're not able to handle "prev" and "next", in order to have 4 customizable action slots instead of just 2 On Android 13+ normal notification actions are useless, so they are not set into the notification builder anymore, to save battery The opposite happens on Android 12-, where media session actions are not set because they don't seem to do anything --- .../mediasession/MediaSessionPlayerUi.java | 105 ++++++++++++++++++ .../mediasession/PlayQueueNavigator.java | 12 +- .../SessionConnectorActionProvider.java | 51 +++++++++ .../player/notification/NotificationUtil.java | 51 ++++++--- 4 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/SessionConnectorActionProvider.java diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java index 6f76a91d1c0..e2531ea29fe 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.player.mediasession; import static org.schabi.newpipe.MainActivity.DEBUG; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.os.Build; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.util.Log; @@ -14,14 +16,20 @@ import androidx.media.session.MediaButtonReceiver; import com.google.android.exoplayer2.ForwardingPlayer; +import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.notification.NotificationActionData; +import org.schabi.newpipe.player.notification.NotificationConstants; import org.schabi.newpipe.player.ui.PlayerUi; import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.util.StreamTypeUtil; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; public class MediaSessionPlayerUi extends PlayerUi @@ -163,4 +171,101 @@ private MediaMetadataCompat buildMediaMetadata() { return builder.build(); } + + + private void updateMediaSessionActions() { + // On Android 13+ (or Android T or API 33+) the actions in the player notification can't be + // controlled directly anymore, but are instead derived from custom media session actions. + // However the system allows customizing only two of these actions, since the other three + // are fixed to play-pause-buffering, previous, next. In order to allow customizing 4 + // actions instead of just 2, we tell the system that the player cannot handle "previous" + // and "next" in PlayQueueNavigator.getSupportedQueueNavigatorActions(), as a workaround. + // The play-pause-buffering action instead cannot be replaced by a custom action even with + // workarounds, so we'll not be able to customize that. + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + // Although setting media session actions on older android versions doesn't seem to + // cause any trouble, it also doesn't seem to do anything, so we don't do anything to + // save battery. Check out NotificationUtil.updateActions() to see what happens on + // older android versions. + return; + } + + final List actions = new ArrayList<>(5); + for (int i = 0; i < 5; ++i) { + final int action = player.getPrefs().getInt( + player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]), + NotificationConstants.SLOT_DEFAULTS[i]); + if (action == NotificationConstants.PLAY_PAUSE + || action == NotificationConstants.PLAY_PAUSE_BUFFERING) { + // play-pause and play-pause-buffering actions are already shown by the system + // in the notification on + continue; + } + + @Nullable final NotificationActionData data = + NotificationActionData.fromNotificationActionEnum(player, action); + + if (data != null) { + actions.add(new SessionConnectorActionProvider(data, context)); + } + } + + sessionConnector.setCustomActionProviders( + actions.toArray(new MediaSessionConnector.CustomActionProvider[0])); + } + + // no need to override onPlaying, onBuffered and onPaused, since the play-pause and + // play-pause-buffering actions are skipped by updateMediaSessionActions anyway + + @Override + public void onBlocked() { + super.onBlocked(); + updateMediaSessionActions(); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + updateMediaSessionActions(); + } + + @Override + public void onCompleted() { + super.onCompleted(); + updateMediaSessionActions(); + } + + @Override + public void onRepeatModeChanged(@RepeatMode final int repeatMode) { + super.onRepeatModeChanged(repeatMode); + updateMediaSessionActions(); + } + + @Override + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled); + updateMediaSessionActions(); + } + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) { + // the notification actions changed + updateMediaSessionActions(); + } + } + + @Override + public void onMetadataChanged(@NonNull final StreamInfo info) { + super.onMetadataChanged(info); + updateMediaSessionActions(); + } + + @Override + public void onPlayQueueEdited() { + super.onPlayQueueEdited(); + updateMediaSessionActions(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index 3339869c129..9a40ae9203f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -5,6 +5,7 @@ import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.ResultReceiver; import android.support.v4.media.MediaDescriptionCompat; @@ -46,7 +47,16 @@ public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, @Override public long getSupportedQueueNavigatorActions( @Nullable final com.google.android.exoplayer2.Player exoPlayer) { - return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; + // As documented in + // https://developer.android.com/about/versions/13/behavior-changes-13#playback-controls + // starting with android 13, setting ACTION_SKIP_TO_PREVIOUS and ACTION_SKIP_TO_NEXT forces + // buttons 2 and 3 to be the system provided "Previous" and "Next". + // Thus, we pretend to not support those actions to have the ability to customize them in + // MediaSessionPlayerUi.updateMediaSessionActions(). + return ACTION_SKIP_TO_QUEUE_ITEM + | (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + ? ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS + : 0); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/SessionConnectorActionProvider.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/SessionConnectorActionProvider.java new file mode 100644 index 00000000000..7b109c149ff --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/SessionConnectorActionProvider.java @@ -0,0 +1,51 @@ +package org.schabi.newpipe.player.mediasession; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.media.session.PlaybackStateCompat; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.player.notification.NotificationActionData; + +import java.lang.ref.WeakReference; + +public class SessionConnectorActionProvider implements MediaSessionConnector.CustomActionProvider { + + private final NotificationActionData data; + @NonNull + private final WeakReference context; + + public SessionConnectorActionProvider(final NotificationActionData notificationActionData, + @NonNull final Context context) { + this.data = notificationActionData; + this.context = new WeakReference<>(context); + } + + @Override + public void onCustomAction(@NonNull final Player player, + @NonNull final String action, + @Nullable final Bundle extras) { + final Context actualContext = context.get(); + if (actualContext != null) { + actualContext.sendBroadcast(new Intent(action)); + } + } + + @Nullable + @Override + public PlaybackStateCompat.CustomAction getCustomAction(@NonNull final Player player) { + if (data.action() == null) { + return null; + } else { + return new PlaybackStateCompat.CustomAction.Builder( + data.action(), data.name(), data.icon() + ).build(); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java index 895ce67edde..c31aa12cd11 100644 --- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java @@ -91,23 +91,34 @@ private synchronized NotificationCompat.Builder createNotification() { new NotificationCompat.Builder(player.getContext(), player.getContext().getString(R.string.notification_channel_id)); - initializeNotificationSlots(); + MediaStyle mediaStyle = new MediaStyle(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + // notification actions are ignored on Android 13+, and are replaced by code in + // MediaSessionPlayerUi, so don't play around with compact actions + + initializeNotificationSlots(); + + // count the number of real slots, to make sure compact slots indices are not out of + // bound + int nonNothingSlotCount = 5; + if (notificationSlots[3] == NotificationConstants.NOTHING) { + --nonNothingSlotCount; + } + if (notificationSlots[4] == NotificationConstants.NOTHING) { + --nonNothingSlotCount; + } - // count the number of real slots, to make sure compact slots indices are not out of bound - int nonNothingSlotCount = 5; - if (notificationSlots[3] == NotificationConstants.NOTHING) { - --nonNothingSlotCount; - } - if (notificationSlots[4] == NotificationConstants.NOTHING) { - --nonNothingSlotCount; - } + // build the compact slot indices array (need code to convert from Integer... because + // Java) + final List compactSlotList = + NotificationConstants.getCompactSlotsFromPreferences( + player.getContext(), player.getPrefs(), nonNothingSlotCount); + final int[] compactSlots = + compactSlotList.stream().mapToInt(Integer::intValue).toArray(); - // build the compact slot indices array (need code to convert from Integer... because Java) - final List compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( - player.getContext(), player.getPrefs(), nonNothingSlotCount); - final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray(); + mediaStyle = mediaStyle.setShowActionsInCompactView(compactSlots); + } - final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots); player.UIs() .get(MediaSessionPlayerUi.class) .flatMap(MediaSessionPlayerUi::getSessionToken) @@ -200,6 +211,12 @@ public void cancelNotificationAndStopForeground() { ///////////////////////////////////////////////////// private void initializeNotificationSlots() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // notification actions are ignored on Android 13+, and are replaced by code in + // MediaSessionPlayerUi + return; + } + for (int i = 0; i < 5; ++i) { notificationSlots[i] = player.getPrefs().getInt( player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]), @@ -209,6 +226,12 @@ private void initializeNotificationSlots() { @SuppressLint("RestrictedApi") private void updateActions(final NotificationCompat.Builder builder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // notification actions are ignored on Android 13+, and are replaced by code in + // MediaSessionPlayerUi + return; + } + builder.mActions.clear(); for (int i = 0; i < 5; ++i) { addAction(builder, notificationSlots[i]);