mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-11-04 01:03:00 +00:00 
			
		
		
		
	Merge pull request #10712 from Stypox/notification-actions-api-33-2
[Android 13+] Restore support of custom notification actions
This commit is contained in:
		@@ -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,15 +16,23 @@ import androidx.annotation.Nullable;
 | 
			
		||||
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.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import java.util.stream.IntStream;
 | 
			
		||||
 | 
			
		||||
public class MediaSessionPlayerUi extends PlayerUi
 | 
			
		||||
        implements SharedPreferences.OnSharedPreferenceChangeListener {
 | 
			
		||||
@@ -34,6 +44,10 @@ public class MediaSessionPlayerUi extends PlayerUi
 | 
			
		||||
    private final String ignoreHardwareMediaButtonsKey;
 | 
			
		||||
    private boolean shouldIgnoreHardwareMediaButtons = false;
 | 
			
		||||
 | 
			
		||||
    // used to check whether any notification action changed, before sending costly updates
 | 
			
		||||
    private List<NotificationActionData> prevNotificationActions = List.of();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public MediaSessionPlayerUi(@NonNull final Player player) {
 | 
			
		||||
        super(player);
 | 
			
		||||
        ignoreHardwareMediaButtonsKey =
 | 
			
		||||
@@ -63,6 +77,10 @@ public class MediaSessionPlayerUi extends PlayerUi
 | 
			
		||||
 | 
			
		||||
        sessionConnector.setMetadataDeduplicationEnabled(true);
 | 
			
		||||
        sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
 | 
			
		||||
 | 
			
		||||
        // force updating media session actions by resetting the previous ones
 | 
			
		||||
        prevNotificationActions = List.of();
 | 
			
		||||
        updateMediaSessionActions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -80,6 +98,7 @@ public class MediaSessionPlayerUi extends PlayerUi
 | 
			
		||||
            mediaSession.release();
 | 
			
		||||
            mediaSession = null;
 | 
			
		||||
        }
 | 
			
		||||
        prevNotificationActions = List.of();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -163,4 +182,109 @@ public class MediaSessionPlayerUi extends PlayerUi
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // only use the fourth and fifth actions (the settings page also shows only the last 2 on
 | 
			
		||||
        // Android 13+)
 | 
			
		||||
        final List<NotificationActionData> newNotificationActions = IntStream.of(3, 4)
 | 
			
		||||
                .map(i -> player.getPrefs().getInt(
 | 
			
		||||
                        player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
 | 
			
		||||
                        NotificationConstants.SLOT_DEFAULTS[i]))
 | 
			
		||||
                .mapToObj(action -> NotificationActionData
 | 
			
		||||
                        .fromNotificationActionEnum(player, action))
 | 
			
		||||
                .filter(Objects::nonNull)
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
 | 
			
		||||
        // avoid costly notification actions update, if nothing changed from last time
 | 
			
		||||
        if (!newNotificationActions.equals(prevNotificationActions)) {
 | 
			
		||||
            prevNotificationActions = newNotificationActions;
 | 
			
		||||
            sessionConnector.setCustomActionProviders(
 | 
			
		||||
                    newNotificationActions.stream()
 | 
			
		||||
                            .map(data -> new SessionConnectorActionProvider(data, context))
 | 
			
		||||
                            .toArray(SessionConnectorActionProvider[]::new));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBlocked() {
 | 
			
		||||
        super.onBlocked();
 | 
			
		||||
        updateMediaSessionActions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPlaying() {
 | 
			
		||||
        super.onPlaying();
 | 
			
		||||
        updateMediaSessionActions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBuffering() {
 | 
			
		||||
        super.onBuffering();
 | 
			
		||||
        updateMediaSessionActions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPaused() {
 | 
			
		||||
        super.onPaused();
 | 
			
		||||
        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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
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) {
 | 
			
		||||
        return new PlaybackStateCompat.CustomAction.Builder(
 | 
			
		||||
                data.action(), data.name(), data.icon()
 | 
			
		||||
        ).build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,187 @@
 | 
			
		||||
package org.schabi.newpipe.player.notification;
 | 
			
		||||
 | 
			
		||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
 | 
			
		||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.DrawableRes;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import org.schabi.newpipe.R;
 | 
			
		||||
import org.schabi.newpipe.player.Player;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
public final class NotificationActionData {
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    private final String action;
 | 
			
		||||
    @NonNull
 | 
			
		||||
    private final String name;
 | 
			
		||||
    @DrawableRes
 | 
			
		||||
    private final int icon;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public NotificationActionData(@NonNull final String action, @NonNull final String name,
 | 
			
		||||
                                  @DrawableRes final int icon) {
 | 
			
		||||
        this.action = action;
 | 
			
		||||
        this.name = name;
 | 
			
		||||
        this.icon = icon;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public String action() {
 | 
			
		||||
        return action;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public String name() {
 | 
			
		||||
        return name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @DrawableRes
 | 
			
		||||
    public int icon() {
 | 
			
		||||
        return icon;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("PrivateResource") // we currently use Exoplayer's internal strings and icons
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static NotificationActionData fromNotificationActionEnum(
 | 
			
		||||
            @NonNull final Player player,
 | 
			
		||||
            @NotificationConstants.Action final int selectedAction
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
 | 
			
		||||
        final Context ctx = player.getContext();
 | 
			
		||||
 | 
			
		||||
        switch (selectedAction) {
 | 
			
		||||
            case NotificationConstants.PREVIOUS:
 | 
			
		||||
                return new NotificationActionData(ACTION_PLAY_PREVIOUS,
 | 
			
		||||
                        ctx.getString(R.string.exo_controls_previous_description), baseActionIcon);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.NEXT:
 | 
			
		||||
                return new NotificationActionData(ACTION_PLAY_NEXT,
 | 
			
		||||
                        ctx.getString(R.string.exo_controls_next_description), baseActionIcon);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.REWIND:
 | 
			
		||||
                return new NotificationActionData(ACTION_FAST_REWIND,
 | 
			
		||||
                        ctx.getString(R.string.exo_controls_rewind_description), baseActionIcon);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.FORWARD:
 | 
			
		||||
                return new NotificationActionData(ACTION_FAST_FORWARD,
 | 
			
		||||
                        ctx.getString(R.string.exo_controls_fastforward_description),
 | 
			
		||||
                        baseActionIcon);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.SMART_REWIND_PREVIOUS:
 | 
			
		||||
                if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_PLAY_PREVIOUS,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_previous_description),
 | 
			
		||||
                            R.drawable.exo_notification_previous);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return new NotificationActionData(ACTION_FAST_REWIND,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_rewind_description),
 | 
			
		||||
                            R.drawable.exo_controls_rewind);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.SMART_FORWARD_NEXT:
 | 
			
		||||
                if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_PLAY_NEXT,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_next_description),
 | 
			
		||||
                            R.drawable.exo_notification_next);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return new NotificationActionData(ACTION_FAST_FORWARD,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_fastforward_description),
 | 
			
		||||
                            R.drawable.exo_controls_fastforward);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.PLAY_PAUSE_BUFFERING:
 | 
			
		||||
                if (player.getCurrentState() == Player.STATE_PREFLIGHT
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BLOCKED
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BUFFERING) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_PLAY_PAUSE,
 | 
			
		||||
                            ctx.getString(R.string.notification_action_buffering),
 | 
			
		||||
                            R.drawable.ic_hourglass_top);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // fallthrough
 | 
			
		||||
            case NotificationConstants.PLAY_PAUSE:
 | 
			
		||||
                if (player.getCurrentState() == Player.STATE_COMPLETED) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_PLAY_PAUSE,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_pause_description),
 | 
			
		||||
                            R.drawable.ic_replay);
 | 
			
		||||
                } else if (player.isPlaying()
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_PREFLIGHT
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BLOCKED
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BUFFERING) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_PLAY_PAUSE,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_pause_description),
 | 
			
		||||
                            R.drawable.exo_notification_pause);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return new NotificationActionData(ACTION_PLAY_PAUSE,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_play_description),
 | 
			
		||||
                            R.drawable.exo_notification_play);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.REPEAT:
 | 
			
		||||
                if (player.getRepeatMode() == REPEAT_MODE_ALL) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_REPEAT,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_repeat_all_description),
 | 
			
		||||
                            R.drawable.exo_media_action_repeat_all);
 | 
			
		||||
                } else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_REPEAT,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_repeat_one_description),
 | 
			
		||||
                            R.drawable.exo_media_action_repeat_one);
 | 
			
		||||
                } else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
 | 
			
		||||
                    return new NotificationActionData(ACTION_REPEAT,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_repeat_off_description),
 | 
			
		||||
                            R.drawable.exo_media_action_repeat_off);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.SHUFFLE:
 | 
			
		||||
                if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
 | 
			
		||||
                    return new NotificationActionData(ACTION_SHUFFLE,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_shuffle_on_description),
 | 
			
		||||
                            R.drawable.exo_controls_shuffle_on);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return new NotificationActionData(ACTION_SHUFFLE,
 | 
			
		||||
                            ctx.getString(R.string.exo_controls_shuffle_off_description),
 | 
			
		||||
                            R.drawable.exo_controls_shuffle_off);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.CLOSE:
 | 
			
		||||
                return new NotificationActionData(ACTION_CLOSE, ctx.getString(R.string.close),
 | 
			
		||||
                        R.drawable.ic_close);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.NOTHING:
 | 
			
		||||
            default:
 | 
			
		||||
                // do nothing
 | 
			
		||||
                return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean equals(@Nullable final Object obj) {
 | 
			
		||||
        return (obj instanceof NotificationActionData other)
 | 
			
		||||
                && this.action.equals(other.action)
 | 
			
		||||
                && this.name.equals(other.name)
 | 
			
		||||
                && this.icon == other.icon;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int hashCode() {
 | 
			
		||||
        return Objects.hash(action, name, icon);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,7 @@ import org.schabi.newpipe.util.Localization;
 | 
			
		||||
 | 
			
		||||
import java.lang.annotation.Retention;
 | 
			
		||||
import java.lang.annotation.RetentionPolicy;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.SortedSet;
 | 
			
		||||
import java.util.TreeSet;
 | 
			
		||||
@@ -65,10 +65,16 @@ public final class NotificationConstants {
 | 
			
		||||
    public static final int CLOSE = 11;
 | 
			
		||||
 | 
			
		||||
    @Retention(RetentionPolicy.SOURCE)
 | 
			
		||||
    @IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
 | 
			
		||||
            PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
 | 
			
		||||
    @IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
 | 
			
		||||
            SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
 | 
			
		||||
            SHUFFLE, CLOSE})
 | 
			
		||||
    public @interface Action { }
 | 
			
		||||
 | 
			
		||||
    @Action
 | 
			
		||||
    public static final int[] ALL_ACTIONS = {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
 | 
			
		||||
            SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
 | 
			
		||||
            SHUFFLE, CLOSE};
 | 
			
		||||
 | 
			
		||||
    @DrawableRes
 | 
			
		||||
    public static final int[] ACTION_ICONS = {
 | 
			
		||||
            0,
 | 
			
		||||
@@ -95,16 +101,6 @@ public final class NotificationConstants {
 | 
			
		||||
            CLOSE,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    @Action
 | 
			
		||||
    public static final int[][] SLOT_ALLOWED_ACTIONS = {
 | 
			
		||||
            new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
 | 
			
		||||
            new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
 | 
			
		||||
            new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
 | 
			
		||||
            new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
 | 
			
		||||
                    SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
 | 
			
		||||
            new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public static final int[] SLOT_PREF_KEYS = {
 | 
			
		||||
            R.string.notification_slot_0_key,
 | 
			
		||||
            R.string.notification_slot_1_key,
 | 
			
		||||
@@ -165,14 +161,11 @@ public final class NotificationConstants {
 | 
			
		||||
    /**
 | 
			
		||||
     * @param context the context to use
 | 
			
		||||
     * @param sharedPreferences the shared preferences to query values from
 | 
			
		||||
     * @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
 | 
			
		||||
     *                  it lower if there are slots with empty actions)
 | 
			
		||||
     * @return a sorted list of the indices of the slots to use as compact slots
 | 
			
		||||
     */
 | 
			
		||||
    public static List<Integer> getCompactSlotsFromPreferences(
 | 
			
		||||
    public static Collection<Integer> getCompactSlotsFromPreferences(
 | 
			
		||||
            @NonNull final Context context,
 | 
			
		||||
            final SharedPreferences sharedPreferences,
 | 
			
		||||
            final int slotCount) {
 | 
			
		||||
            final SharedPreferences sharedPreferences) {
 | 
			
		||||
        final SortedSet<Integer> compactSlots = new TreeSet<>();
 | 
			
		||||
        for (int i = 0; i < 3; i++) {
 | 
			
		||||
            final int compactSlot = sharedPreferences.getInt(
 | 
			
		||||
@@ -180,14 +173,14 @@ public final class NotificationConstants {
 | 
			
		||||
 | 
			
		||||
            if (compactSlot == Integer.MAX_VALUE) {
 | 
			
		||||
                // settings not yet populated, return default values
 | 
			
		||||
                return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
 | 
			
		||||
                return SLOT_COMPACT_DEFAULTS;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // a negative value (-1) is set when the user does not want a particular compact slot
 | 
			
		||||
            if (compactSlot >= 0 && compactSlot < slotCount) {
 | 
			
		||||
            if (compactSlot >= 0) {
 | 
			
		||||
                // compact slot is < 0 if there are less than 3 checked checkboxes
 | 
			
		||||
                compactSlots.add(compactSlot);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return new ArrayList<>(compactSlots);
 | 
			
		||||
        return compactSlots;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,19 @@
 | 
			
		||||
package org.schabi.newpipe.player.notification;
 | 
			
		||||
 | 
			
		||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 | 
			
		||||
import static androidx.media.app.NotificationCompat.MediaStyle;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint;
 | 
			
		||||
import android.app.PendingIntent;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.pm.ServiceInfo;
 | 
			
		||||
import android.graphics.Bitmap;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.DrawableRes;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.StringRes;
 | 
			
		||||
import androidx.core.app.NotificationCompat;
 | 
			
		||||
import androidx.core.app.NotificationManagerCompat;
 | 
			
		||||
import androidx.core.app.PendingIntentCompat;
 | 
			
		||||
@@ -23,23 +26,12 @@ import org.schabi.newpipe.player.Player;
 | 
			
		||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
 | 
			
		||||
import org.schabi.newpipe.util.NavigationHelper;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 | 
			
		||||
import static androidx.media.app.NotificationCompat.MediaStyle;
 | 
			
		||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
 | 
			
		||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
 | 
			
		||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This is a utility class for player notifications.
 | 
			
		||||
 */
 | 
			
		||||
@@ -100,29 +92,21 @@ public final class NotificationUtil {
 | 
			
		||||
        final NotificationCompat.Builder builder =
 | 
			
		||||
                new NotificationCompat.Builder(player.getContext(),
 | 
			
		||||
                player.getContext().getString(R.string.notification_channel_id));
 | 
			
		||||
        final MediaStyle mediaStyle = new MediaStyle();
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
        // setup media style (compact notification slots and media session)
 | 
			
		||||
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
 | 
			
		||||
            // notification actions are ignored on Android 13+, and are replaced by code in
 | 
			
		||||
            // MediaSessionPlayerUi
 | 
			
		||||
            final int[] compactSlots = initializeNotificationSlots();
 | 
			
		||||
            mediaStyle.setShowActionsInCompactView(compactSlots);
 | 
			
		||||
        }
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
        final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
 | 
			
		||||
        player.UIs()
 | 
			
		||||
                .get(MediaSessionPlayerUi.class)
 | 
			
		||||
                .flatMap(MediaSessionPlayerUi::getSessionToken)
 | 
			
		||||
                .ifPresent(mediaStyle::setMediaSession);
 | 
			
		||||
 | 
			
		||||
        // setup notification builder
 | 
			
		||||
        builder.setStyle(mediaStyle)
 | 
			
		||||
                .setPriority(NotificationCompat.PRIORITY_HIGH)
 | 
			
		||||
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
 | 
			
		||||
@@ -157,7 +141,11 @@ public final class NotificationUtil {
 | 
			
		||||
        notificationBuilder.setContentText(player.getUploaderName());
 | 
			
		||||
        notificationBuilder.setTicker(player.getVideoTitle());
 | 
			
		||||
 | 
			
		||||
        updateActions(notificationBuilder);
 | 
			
		||||
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
 | 
			
		||||
            // notification actions are ignored on Android 13+, and are replaced by code in
 | 
			
		||||
            // MediaSessionPlayerUi
 | 
			
		||||
            updateActions(notificationBuilder);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -209,12 +197,35 @@ public final class NotificationUtil {
 | 
			
		||||
    // ACTIONS
 | 
			
		||||
    /////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
    private void initializeNotificationSlots() {
 | 
			
		||||
    /**
 | 
			
		||||
     * The compact slots array from settings contains indices from 0 to 4, each referring to one of
 | 
			
		||||
     * the five actions configurable by the user. However, if the user sets an action to "Nothing",
 | 
			
		||||
     * then all of the actions coming after will have a "settings index" different than the index
 | 
			
		||||
     * of the corresponding action when sent to the system.
 | 
			
		||||
     *
 | 
			
		||||
     * @return the indices of compact slots referred to the list of non-nothing actions that will be
 | 
			
		||||
     *         sent to the system
 | 
			
		||||
     */
 | 
			
		||||
    private int[] initializeNotificationSlots() {
 | 
			
		||||
        final Collection<Integer> settingsCompactSlots = NotificationConstants
 | 
			
		||||
                .getCompactSlotsFromPreferences(player.getContext(), player.getPrefs());
 | 
			
		||||
        final List<Integer> adjustedCompactSlots = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        int nonNothingIndex = 0;
 | 
			
		||||
        for (int i = 0; i < 5; ++i) {
 | 
			
		||||
            notificationSlots[i] = player.getPrefs().getInt(
 | 
			
		||||
                    player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
 | 
			
		||||
                    NotificationConstants.SLOT_DEFAULTS[i]);
 | 
			
		||||
 | 
			
		||||
            if (notificationSlots[i] != NotificationConstants.NOTHING) {
 | 
			
		||||
                if (settingsCompactSlots.contains(i)) {
 | 
			
		||||
                    adjustedCompactSlots.add(nonNothingIndex);
 | 
			
		||||
                }
 | 
			
		||||
                nonNothingIndex += 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return adjustedCompactSlots.stream().mapToInt(Integer::intValue).toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("RestrictedApi")
 | 
			
		||||
@@ -227,115 +238,15 @@ public final class NotificationUtil {
 | 
			
		||||
 | 
			
		||||
    private void addAction(final NotificationCompat.Builder builder,
 | 
			
		||||
                           @NotificationConstants.Action final int slot) {
 | 
			
		||||
        final NotificationCompat.Action action = getAction(slot);
 | 
			
		||||
        if (action != null) {
 | 
			
		||||
            builder.addAction(action);
 | 
			
		||||
        @Nullable final NotificationActionData data =
 | 
			
		||||
                NotificationActionData.fromNotificationActionEnum(player, slot);
 | 
			
		||||
        if (data == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private NotificationCompat.Action getAction(
 | 
			
		||||
            @NotificationConstants.Action final int selectedAction) {
 | 
			
		||||
        final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
 | 
			
		||||
        switch (selectedAction) {
 | 
			
		||||
            case NotificationConstants.PREVIOUS:
 | 
			
		||||
                return getAction(baseActionIcon,
 | 
			
		||||
                        R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.NEXT:
 | 
			
		||||
                return getAction(baseActionIcon,
 | 
			
		||||
                        R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.REWIND:
 | 
			
		||||
                return getAction(baseActionIcon,
 | 
			
		||||
                        R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.FORWARD:
 | 
			
		||||
                return getAction(baseActionIcon,
 | 
			
		||||
                        R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.SMART_REWIND_PREVIOUS:
 | 
			
		||||
                if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
 | 
			
		||||
                    return getAction(R.drawable.exo_notification_previous,
 | 
			
		||||
                            R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return getAction(R.drawable.exo_controls_rewind,
 | 
			
		||||
                            R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.SMART_FORWARD_NEXT:
 | 
			
		||||
                if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
 | 
			
		||||
                    return getAction(R.drawable.exo_notification_next,
 | 
			
		||||
                            R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return getAction(R.drawable.exo_controls_fastforward,
 | 
			
		||||
                            R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.PLAY_PAUSE_BUFFERING:
 | 
			
		||||
                if (player.getCurrentState() == Player.STATE_PREFLIGHT
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BLOCKED
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BUFFERING) {
 | 
			
		||||
                    // null intent -> show hourglass icon that does nothing when clicked
 | 
			
		||||
                    return new NotificationCompat.Action(R.drawable.ic_hourglass_top,
 | 
			
		||||
                            player.getContext().getString(R.string.notification_action_buffering),
 | 
			
		||||
                            null);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // fallthrough
 | 
			
		||||
            case NotificationConstants.PLAY_PAUSE:
 | 
			
		||||
                if (player.getCurrentState() == Player.STATE_COMPLETED) {
 | 
			
		||||
                    return getAction(R.drawable.ic_replay,
 | 
			
		||||
                            R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
 | 
			
		||||
                } else if (player.isPlaying()
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_PREFLIGHT
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BLOCKED
 | 
			
		||||
                        || player.getCurrentState() == Player.STATE_BUFFERING) {
 | 
			
		||||
                    return getAction(R.drawable.exo_notification_pause,
 | 
			
		||||
                            R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return getAction(R.drawable.exo_notification_play,
 | 
			
		||||
                            R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.REPEAT:
 | 
			
		||||
                if (player.getRepeatMode() == REPEAT_MODE_ALL) {
 | 
			
		||||
                    return getAction(R.drawable.exo_media_action_repeat_all,
 | 
			
		||||
                            R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
 | 
			
		||||
                } else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
 | 
			
		||||
                    return getAction(R.drawable.exo_media_action_repeat_one,
 | 
			
		||||
                            R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
 | 
			
		||||
                } else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
 | 
			
		||||
                    return getAction(R.drawable.exo_media_action_repeat_off,
 | 
			
		||||
                            R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.SHUFFLE:
 | 
			
		||||
                if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
 | 
			
		||||
                    return getAction(R.drawable.exo_controls_shuffle_on,
 | 
			
		||||
                            R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return getAction(R.drawable.exo_controls_shuffle_off,
 | 
			
		||||
                            R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.CLOSE:
 | 
			
		||||
                return getAction(R.drawable.ic_close,
 | 
			
		||||
                        R.string.close, ACTION_CLOSE);
 | 
			
		||||
 | 
			
		||||
            case NotificationConstants.NOTHING:
 | 
			
		||||
            default:
 | 
			
		||||
                // do nothing
 | 
			
		||||
                return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private NotificationCompat.Action getAction(@DrawableRes final int drawable,
 | 
			
		||||
                                                @StringRes final int title,
 | 
			
		||||
                                                final String intentAction) {
 | 
			
		||||
        return new NotificationCompat.Action(drawable, player.getContext().getString(title),
 | 
			
		||||
                PendingIntentCompat.getBroadcast(player.getContext(), NOTIFICATION_ID,
 | 
			
		||||
                        new Intent(intentAction), FLAG_UPDATE_CURRENT, false));
 | 
			
		||||
        final PendingIntent intent = PendingIntentCompat.getBroadcast(player.getContext(),
 | 
			
		||||
                NOTIFICATION_ID, new Intent(data.action()), FLAG_UPDATE_CURRENT, false);
 | 
			
		||||
        builder.addAction(new NotificationCompat.Action(data.icon(), data.name(), intent));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Intent getIntentForNotification() {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,35 +5,22 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.SharedPreferences;
 | 
			
		||||
import android.content.res.ColorStateList;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.util.AttributeSet;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.CheckBox;
 | 
			
		||||
import android.widget.ImageView;
 | 
			
		||||
import android.widget.RadioButton;
 | 
			
		||||
import android.widget.RadioGroup;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
import android.widget.Toast;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.appcompat.content.res.AppCompatResources;
 | 
			
		||||
import androidx.core.widget.TextViewCompat;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceViewHolder;
 | 
			
		||||
 | 
			
		||||
import org.schabi.newpipe.App;
 | 
			
		||||
import org.schabi.newpipe.R;
 | 
			
		||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
 | 
			
		||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
 | 
			
		||||
import org.schabi.newpipe.player.notification.NotificationConstants;
 | 
			
		||||
import org.schabi.newpipe.util.DeviceUtils;
 | 
			
		||||
import org.schabi.newpipe.util.ThemeHelper;
 | 
			
		||||
import org.schabi.newpipe.views.FocusOverlayView;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.stream.IntStream;
 | 
			
		||||
 | 
			
		||||
@@ -45,8 +32,9 @@ public class NotificationActionsPreference extends Preference {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Nullable private NotificationSlot[] notificationSlots = null;
 | 
			
		||||
    @Nullable private List<Integer> compactSlots = null;
 | 
			
		||||
    private NotificationSlot[] notificationSlots;
 | 
			
		||||
    private List<Integer> compactSlots;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
    // Lifecycle
 | 
			
		||||
@@ -56,6 +44,11 @@ public class NotificationActionsPreference extends Preference {
 | 
			
		||||
    public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
 | 
			
		||||
        super.onBindViewHolder(holder);
 | 
			
		||||
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 | 
			
		||||
            ((TextView) holder.itemView.findViewById(R.id.summary))
 | 
			
		||||
                    .setText(R.string.notification_actions_summary_android13);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.itemView.setClickable(false);
 | 
			
		||||
        setupActions(holder.itemView);
 | 
			
		||||
    }
 | 
			
		||||
@@ -75,13 +68,29 @@ public class NotificationActionsPreference extends Preference {
 | 
			
		||||
    ////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
    private void setupActions(@NonNull final View view) {
 | 
			
		||||
        compactSlots = NotificationConstants.getCompactSlotsFromPreferences(getContext(),
 | 
			
		||||
                getSharedPreferences(), 5);
 | 
			
		||||
        compactSlots = new ArrayList<>(NotificationConstants.getCompactSlotsFromPreferences(
 | 
			
		||||
                getContext(), getSharedPreferences()));
 | 
			
		||||
        notificationSlots = IntStream.range(0, 5)
 | 
			
		||||
                .mapToObj(i -> new NotificationSlot(i, view))
 | 
			
		||||
                .mapToObj(i -> new NotificationSlot(getContext(), getSharedPreferences(), i, view,
 | 
			
		||||
                        compactSlots.contains(i), this::onToggleCompactSlot))
 | 
			
		||||
                .toArray(NotificationSlot[]::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onToggleCompactSlot(final int i, final CheckBox checkBox) {
 | 
			
		||||
        if (checkBox.isChecked()) {
 | 
			
		||||
            compactSlots.remove((Integer) i);
 | 
			
		||||
        } else if (compactSlots.size() < 3) {
 | 
			
		||||
            compactSlots.add(i);
 | 
			
		||||
        } else {
 | 
			
		||||
            Toast.makeText(getContext(),
 | 
			
		||||
                    R.string.notification_actions_at_most_three,
 | 
			
		||||
                    Toast.LENGTH_SHORT).show();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        checkBox.toggle();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
    // Saving
 | 
			
		||||
@@ -99,143 +108,10 @@ public class NotificationActionsPreference extends Preference {
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < 5; i++) {
 | 
			
		||||
                editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
 | 
			
		||||
                        notificationSlots[i].selectedAction);
 | 
			
		||||
                        notificationSlots[i].getSelectedAction());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            editor.apply();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
    // Notification action
 | 
			
		||||
    ////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
    private static final int[] SLOT_ITEMS = {
 | 
			
		||||
            R.id.notificationAction0,
 | 
			
		||||
            R.id.notificationAction1,
 | 
			
		||||
            R.id.notificationAction2,
 | 
			
		||||
            R.id.notificationAction3,
 | 
			
		||||
            R.id.notificationAction4,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private static final int[] SLOT_TITLES = {
 | 
			
		||||
            R.string.notification_action_0_title,
 | 
			
		||||
            R.string.notification_action_1_title,
 | 
			
		||||
            R.string.notification_action_2_title,
 | 
			
		||||
            R.string.notification_action_3_title,
 | 
			
		||||
            R.string.notification_action_4_title,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private class NotificationSlot {
 | 
			
		||||
 | 
			
		||||
        final int i;
 | 
			
		||||
        @NotificationConstants.Action int selectedAction;
 | 
			
		||||
 | 
			
		||||
        ImageView icon;
 | 
			
		||||
        TextView summary;
 | 
			
		||||
 | 
			
		||||
        NotificationSlot(final int actionIndex, final View parentView) {
 | 
			
		||||
            this.i = actionIndex;
 | 
			
		||||
 | 
			
		||||
            final View view = parentView.findViewById(SLOT_ITEMS[i]);
 | 
			
		||||
            setupSelectedAction(view);
 | 
			
		||||
            setupTitle(view);
 | 
			
		||||
            setupCheckbox(view);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void setupTitle(final View view) {
 | 
			
		||||
            ((TextView) view.findViewById(R.id.notificationActionTitle))
 | 
			
		||||
                    .setText(SLOT_TITLES[i]);
 | 
			
		||||
            view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
 | 
			
		||||
                    v -> openActionChooserDialog());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void setupCheckbox(final View view) {
 | 
			
		||||
            final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
 | 
			
		||||
            compactSlotCheckBox.setChecked(compactSlots.contains(i));
 | 
			
		||||
            view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
 | 
			
		||||
                    v -> {
 | 
			
		||||
                        if (compactSlotCheckBox.isChecked()) {
 | 
			
		||||
                            compactSlots.remove((Integer) i);
 | 
			
		||||
                        } else if (compactSlots.size() < 3) {
 | 
			
		||||
                            compactSlots.add(i);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Toast.makeText(getContext(),
 | 
			
		||||
                                    R.string.notification_actions_at_most_three,
 | 
			
		||||
                                    Toast.LENGTH_SHORT).show();
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        compactSlotCheckBox.toggle();
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void setupSelectedAction(final View view) {
 | 
			
		||||
            icon = view.findViewById(R.id.notificationActionIcon);
 | 
			
		||||
            summary = view.findViewById(R.id.notificationActionSummary);
 | 
			
		||||
            selectedAction = getSharedPreferences().getInt(
 | 
			
		||||
                    getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
 | 
			
		||||
                    NotificationConstants.SLOT_DEFAULTS[i]);
 | 
			
		||||
            updateInfo();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void updateInfo() {
 | 
			
		||||
            if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
 | 
			
		||||
                icon.setImageDrawable(null);
 | 
			
		||||
            } else {
 | 
			
		||||
                icon.setImageDrawable(AppCompatResources.getDrawable(getContext(),
 | 
			
		||||
                        NotificationConstants.ACTION_ICONS[selectedAction]));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            summary.setText(NotificationConstants.getActionName(getContext(), selectedAction));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void openActionChooserDialog() {
 | 
			
		||||
            final LayoutInflater inflater = LayoutInflater.from(getContext());
 | 
			
		||||
            final SingleChoiceDialogViewBinding binding =
 | 
			
		||||
                    SingleChoiceDialogViewBinding.inflate(inflater);
 | 
			
		||||
 | 
			
		||||
            final AlertDialog alertDialog = new AlertDialog.Builder(getContext())
 | 
			
		||||
                    .setTitle(SLOT_TITLES[i])
 | 
			
		||||
                    .setView(binding.getRoot())
 | 
			
		||||
                    .setCancelable(true)
 | 
			
		||||
                    .create();
 | 
			
		||||
 | 
			
		||||
            final View.OnClickListener radioButtonsClickListener = v -> {
 | 
			
		||||
                selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
 | 
			
		||||
                updateInfo();
 | 
			
		||||
                alertDialog.dismiss();
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
 | 
			
		||||
                final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
 | 
			
		||||
                final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
 | 
			
		||||
                        .getRoot();
 | 
			
		||||
 | 
			
		||||
                // if present set action icon with correct color
 | 
			
		||||
                final int iconId = NotificationConstants.ACTION_ICONS[action];
 | 
			
		||||
                if (iconId != 0) {
 | 
			
		||||
                    radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
 | 
			
		||||
 | 
			
		||||
                    final var color = ColorStateList.valueOf(ThemeHelper
 | 
			
		||||
                            .resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary));
 | 
			
		||||
                    TextViewCompat.setCompoundDrawableTintList(radioButton, color);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                radioButton.setText(NotificationConstants.getActionName(getContext(), action));
 | 
			
		||||
                radioButton.setChecked(action == selectedAction);
 | 
			
		||||
                radioButton.setId(id);
 | 
			
		||||
                radioButton.setLayoutParams(new RadioGroup.LayoutParams(
 | 
			
		||||
                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
 | 
			
		||||
                radioButton.setOnClickListener(radioButtonsClickListener);
 | 
			
		||||
                binding.list.addView(radioButton);
 | 
			
		||||
            }
 | 
			
		||||
            alertDialog.show();
 | 
			
		||||
 | 
			
		||||
            if (DeviceUtils.isTv(getContext())) {
 | 
			
		||||
                FocusOverlayView.setupFocusObserver(alertDialog);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,172 @@
 | 
			
		||||
package org.schabi.newpipe.settings.custom;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.SharedPreferences;
 | 
			
		||||
import android.content.res.ColorStateList;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.CheckBox;
 | 
			
		||||
import android.widget.ImageView;
 | 
			
		||||
import android.widget.RadioButton;
 | 
			
		||||
import android.widget.RadioGroup;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.appcompat.content.res.AppCompatResources;
 | 
			
		||||
import androidx.core.widget.TextViewCompat;
 | 
			
		||||
 | 
			
		||||
import org.schabi.newpipe.R;
 | 
			
		||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
 | 
			
		||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
 | 
			
		||||
import org.schabi.newpipe.player.notification.NotificationConstants;
 | 
			
		||||
import org.schabi.newpipe.util.DeviceUtils;
 | 
			
		||||
import org.schabi.newpipe.util.ThemeHelper;
 | 
			
		||||
import org.schabi.newpipe.views.FocusOverlayView;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
 | 
			
		||||
class NotificationSlot {
 | 
			
		||||
 | 
			
		||||
    private static final int[] SLOT_ITEMS = {
 | 
			
		||||
            R.id.notificationAction0,
 | 
			
		||||
            R.id.notificationAction1,
 | 
			
		||||
            R.id.notificationAction2,
 | 
			
		||||
            R.id.notificationAction3,
 | 
			
		||||
            R.id.notificationAction4,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private static final int[] SLOT_TITLES = {
 | 
			
		||||
            R.string.notification_action_0_title,
 | 
			
		||||
            R.string.notification_action_1_title,
 | 
			
		||||
            R.string.notification_action_2_title,
 | 
			
		||||
            R.string.notification_action_3_title,
 | 
			
		||||
            R.string.notification_action_4_title,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private final int i;
 | 
			
		||||
    private @NotificationConstants.Action int selectedAction;
 | 
			
		||||
    private final Context context;
 | 
			
		||||
    private final BiConsumer<Integer, CheckBox> onToggleCompactSlot;
 | 
			
		||||
 | 
			
		||||
    private ImageView icon;
 | 
			
		||||
    private TextView summary;
 | 
			
		||||
 | 
			
		||||
    NotificationSlot(final Context context,
 | 
			
		||||
                     final SharedPreferences prefs,
 | 
			
		||||
                     final int actionIndex,
 | 
			
		||||
                     final View parentView,
 | 
			
		||||
                     final boolean isCompactSlotChecked,
 | 
			
		||||
                     final BiConsumer<Integer, CheckBox> onToggleCompactSlot) {
 | 
			
		||||
        this.context = context;
 | 
			
		||||
        this.i = actionIndex;
 | 
			
		||||
        this.onToggleCompactSlot = onToggleCompactSlot;
 | 
			
		||||
 | 
			
		||||
        selectedAction = Objects.requireNonNull(prefs).getInt(
 | 
			
		||||
                context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
 | 
			
		||||
                NotificationConstants.SLOT_DEFAULTS[i]);
 | 
			
		||||
        final View view = parentView.findViewById(SLOT_ITEMS[i]);
 | 
			
		||||
 | 
			
		||||
        // only show the last two notification slots on Android 13+
 | 
			
		||||
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || i >= 3) {
 | 
			
		||||
            setupSelectedAction(view);
 | 
			
		||||
            setupTitle(view);
 | 
			
		||||
            setupCheckbox(view, isCompactSlotChecked);
 | 
			
		||||
        } else {
 | 
			
		||||
            view.setVisibility(View.GONE);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setupTitle(final View view) {
 | 
			
		||||
        ((TextView) view.findViewById(R.id.notificationActionTitle))
 | 
			
		||||
                .setText(SLOT_TITLES[i]);
 | 
			
		||||
        view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
 | 
			
		||||
                v -> openActionChooserDialog());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setupCheckbox(final View view, final boolean isCompactSlotChecked) {
 | 
			
		||||
        final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 | 
			
		||||
            // there are no compact slots to customize on Android 13+
 | 
			
		||||
            compactSlotCheckBox.setVisibility(View.GONE);
 | 
			
		||||
            view.findViewById(R.id.notificationActionCheckBoxClickableArea)
 | 
			
		||||
                    .setVisibility(View.GONE);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        compactSlotCheckBox.setChecked(isCompactSlotChecked);
 | 
			
		||||
        view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
 | 
			
		||||
                v -> onToggleCompactSlot.accept(i, compactSlotCheckBox));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setupSelectedAction(final View view) {
 | 
			
		||||
        icon = view.findViewById(R.id.notificationActionIcon);
 | 
			
		||||
        summary = view.findViewById(R.id.notificationActionSummary);
 | 
			
		||||
        updateInfo();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void updateInfo() {
 | 
			
		||||
        if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
 | 
			
		||||
            icon.setImageDrawable(null);
 | 
			
		||||
        } else {
 | 
			
		||||
            icon.setImageDrawable(AppCompatResources.getDrawable(context,
 | 
			
		||||
                    NotificationConstants.ACTION_ICONS[selectedAction]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        summary.setText(NotificationConstants.getActionName(context, selectedAction));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void openActionChooserDialog() {
 | 
			
		||||
        final LayoutInflater inflater = LayoutInflater.from(context);
 | 
			
		||||
        final SingleChoiceDialogViewBinding binding =
 | 
			
		||||
                SingleChoiceDialogViewBinding.inflate(inflater);
 | 
			
		||||
 | 
			
		||||
        final AlertDialog alertDialog = new AlertDialog.Builder(context)
 | 
			
		||||
                .setTitle(SLOT_TITLES[i])
 | 
			
		||||
                .setView(binding.getRoot())
 | 
			
		||||
                .setCancelable(true)
 | 
			
		||||
                .create();
 | 
			
		||||
 | 
			
		||||
        final View.OnClickListener radioButtonsClickListener = v -> {
 | 
			
		||||
            selectedAction = NotificationConstants.ALL_ACTIONS[v.getId()];
 | 
			
		||||
            updateInfo();
 | 
			
		||||
            alertDialog.dismiss();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        for (int id = 0; id < NotificationConstants.ALL_ACTIONS.length; ++id) {
 | 
			
		||||
            final int action = NotificationConstants.ALL_ACTIONS[id];
 | 
			
		||||
            final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
 | 
			
		||||
                    .getRoot();
 | 
			
		||||
 | 
			
		||||
            // if present set action icon with correct color
 | 
			
		||||
            final int iconId = NotificationConstants.ACTION_ICONS[action];
 | 
			
		||||
            if (iconId != 0) {
 | 
			
		||||
                radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
 | 
			
		||||
 | 
			
		||||
                final var color = ColorStateList.valueOf(ThemeHelper
 | 
			
		||||
                        .resolveColorFromAttr(context, android.R.attr.textColorPrimary));
 | 
			
		||||
                TextViewCompat.setCompoundDrawableTintList(radioButton, color);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            radioButton.setText(NotificationConstants.getActionName(context, action));
 | 
			
		||||
            radioButton.setChecked(action == selectedAction);
 | 
			
		||||
            radioButton.setId(id);
 | 
			
		||||
            radioButton.setLayoutParams(new RadioGroup.LayoutParams(
 | 
			
		||||
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
 | 
			
		||||
            radioButton.setOnClickListener(radioButtonsClickListener);
 | 
			
		||||
            binding.list.addView(radioButton);
 | 
			
		||||
        }
 | 
			
		||||
        alertDialog.show();
 | 
			
		||||
 | 
			
		||||
        if (DeviceUtils.isTv(context)) {
 | 
			
		||||
            FocusOverlayView.setupFocusObserver(alertDialog);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NotificationConstants.Action
 | 
			
		||||
    public int getSelectedAction() {
 | 
			
		||||
        return selectedAction;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
 | 
			
		||||
<androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
        xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
@@ -8,64 +7,64 @@
 | 
			
		||||
        android:paddingTop="16dp">
 | 
			
		||||
 | 
			
		||||
    <org.schabi.newpipe.views.NewPipeTextView
 | 
			
		||||
            android:id="@+id/textView"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginStart="16dp"
 | 
			
		||||
            android:layout_marginEnd="16dp"
 | 
			
		||||
            android:clickable="false"
 | 
			
		||||
            android:focusable="false"
 | 
			
		||||
            android:gravity="center"
 | 
			
		||||
            android:text="@string/notification_actions_summary"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintHorizontal_bias="0.0"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toTopOf="parent" />
 | 
			
		||||
        android:id="@+id/summary"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginStart="16dp"
 | 
			
		||||
        android:layout_marginEnd="16dp"
 | 
			
		||||
        android:clickable="false"
 | 
			
		||||
        android:focusable="false"
 | 
			
		||||
        android:gravity="center"
 | 
			
		||||
        android:text="@string/notification_actions_summary"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintHorizontal_bias="0.0"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent" />
 | 
			
		||||
 | 
			
		||||
        <include
 | 
			
		||||
            android:id="@+id/notificationAction0"
 | 
			
		||||
            layout="@layout/settings_notification_action"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginTop="8dp"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/textView" />
 | 
			
		||||
    <include
 | 
			
		||||
        android:id="@+id/notificationAction0"
 | 
			
		||||
        layout="@layout/settings_notification_action"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginTop="8dp"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/summary" />
 | 
			
		||||
 | 
			
		||||
        <include
 | 
			
		||||
            android:id="@+id/notificationAction1"
 | 
			
		||||
            layout="@layout/settings_notification_action"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/notificationAction0" />
 | 
			
		||||
    <include
 | 
			
		||||
        android:id="@+id/notificationAction1"
 | 
			
		||||
        layout="@layout/settings_notification_action"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/notificationAction0" />
 | 
			
		||||
 | 
			
		||||
        <include
 | 
			
		||||
            android:id="@+id/notificationAction2"
 | 
			
		||||
            layout="@layout/settings_notification_action"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/notificationAction1" />
 | 
			
		||||
    <include
 | 
			
		||||
        android:id="@+id/notificationAction2"
 | 
			
		||||
        layout="@layout/settings_notification_action"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/notificationAction1" />
 | 
			
		||||
 | 
			
		||||
        <include
 | 
			
		||||
            android:id="@+id/notificationAction3"
 | 
			
		||||
            layout="@layout/settings_notification_action"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/notificationAction2" />
 | 
			
		||||
    <include
 | 
			
		||||
        android:id="@+id/notificationAction3"
 | 
			
		||||
        layout="@layout/settings_notification_action"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/notificationAction2" />
 | 
			
		||||
 | 
			
		||||
        <include
 | 
			
		||||
            android:id="@+id/notificationAction4"
 | 
			
		||||
            layout="@layout/settings_notification_action"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/notificationAction3" />
 | 
			
		||||
    <include
 | 
			
		||||
        android:id="@+id/notificationAction4"
 | 
			
		||||
        layout="@layout/settings_notification_action"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/notificationAction3" />
 | 
			
		||||
 | 
			
		||||
    </androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
</androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,8 @@
 | 
			
		||||
    <string name="notification_action_2_title">Third action button</string>
 | 
			
		||||
    <string name="notification_action_3_title">Fourth action button</string>
 | 
			
		||||
    <string name="notification_action_4_title">Fifth action button</string>
 | 
			
		||||
    <string name="notification_actions_summary">Edit each notification action below by tapping on it. Select up to three of them to be shown in the compact notification by using the checkboxes on the right</string>
 | 
			
		||||
    <string name="notification_actions_summary">Edit each notification action below by tapping on it. Select up to three of them to be shown in the compact notification by using the checkboxes on the right.</string>
 | 
			
		||||
    <string name="notification_actions_summary_android13">Edit each notification action below by tapping on it. The first three actions (play/pause, previous and next) are set by the system and cannot be customized.</string>
 | 
			
		||||
    <string name="notification_actions_at_most_three">You can select at most three actions to show in the compact notification!</string>
 | 
			
		||||
    <string name="notification_action_repeat">Repeat</string>
 | 
			
		||||
    <string name="notification_action_shuffle">Shuffle</string>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user