Skip to content

Commit

Permalink
Allow customizing notification actions on Android 13+
Browse files Browse the repository at this point in the history
Use a workaround initially suggested in TeamNewPipe#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
  • Loading branch information
Stypox committed Nov 16, 2023
1 parent 82bf644 commit 126b430
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<SessionConnectorActionProvider> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer> 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<Integer> 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)
Expand Down Expand Up @@ -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]),
Expand All @@ -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]);
Expand Down

0 comments on commit 126b430

Please sign in to comment.