mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-25 12:32:31 +01: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:
commit
1d8850d1b2
@ -1,10 +1,12 @@
|
|||||||
package org.schabi.newpipe.player.mediasession;
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
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.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Build;
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -14,15 +16,23 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.media.session.MediaButtonReceiver;
|
import androidx.media.session.MediaButtonReceiver;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ForwardingPlayer;
|
import com.google.android.exoplayer2.ForwardingPlayer;
|
||||||
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.Player;
|
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.PlayerUi;
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class MediaSessionPlayerUi extends PlayerUi
|
public class MediaSessionPlayerUi extends PlayerUi
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
@ -34,6 +44,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
private final String ignoreHardwareMediaButtonsKey;
|
private final String ignoreHardwareMediaButtonsKey;
|
||||||
private boolean shouldIgnoreHardwareMediaButtons = false;
|
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) {
|
public MediaSessionPlayerUi(@NonNull final Player player) {
|
||||||
super(player);
|
super(player);
|
||||||
ignoreHardwareMediaButtonsKey =
|
ignoreHardwareMediaButtonsKey =
|
||||||
@ -63,6 +77,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
|
|
||||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||||
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
||||||
|
|
||||||
|
// force updating media session actions by resetting the previous ones
|
||||||
|
prevNotificationActions = List.of();
|
||||||
|
updateMediaSessionActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -80,6 +98,7 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
mediaSession.release();
|
mediaSession.release();
|
||||||
mediaSession = null;
|
mediaSession = null;
|
||||||
}
|
}
|
||||||
|
prevNotificationActions = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -163,4 +182,109 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
|
|
||||||
return builder.build();
|
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.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
@ -65,10 +65,16 @@ public final class NotificationConstants {
|
|||||||
public static final int CLOSE = 11;
|
public static final int CLOSE = 11;
|
||||||
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
|
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
|
||||||
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
|
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
|
||||||
|
SHUFFLE, CLOSE})
|
||||||
public @interface Action { }
|
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
|
@DrawableRes
|
||||||
public static final int[] ACTION_ICONS = {
|
public static final int[] ACTION_ICONS = {
|
||||||
0,
|
0,
|
||||||
@ -95,16 +101,6 @@ public final class NotificationConstants {
|
|||||||
CLOSE,
|
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 = {
|
public static final int[] SLOT_PREF_KEYS = {
|
||||||
R.string.notification_slot_0_key,
|
R.string.notification_slot_0_key,
|
||||||
R.string.notification_slot_1_key,
|
R.string.notification_slot_1_key,
|
||||||
@ -165,14 +161,11 @@ public final class NotificationConstants {
|
|||||||
/**
|
/**
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param sharedPreferences the shared preferences to query values from
|
* @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
|
* @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,
|
@NonNull final Context context,
|
||||||
final SharedPreferences sharedPreferences,
|
final SharedPreferences sharedPreferences) {
|
||||||
final int slotCount) {
|
|
||||||
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
final int compactSlot = sharedPreferences.getInt(
|
final int compactSlot = sharedPreferences.getInt(
|
||||||
@ -180,14 +173,14 @@ public final class NotificationConstants {
|
|||||||
|
|
||||||
if (compactSlot == Integer.MAX_VALUE) {
|
if (compactSlot == Integer.MAX_VALUE) {
|
||||||
// settings not yet populated, return default values
|
// 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) {
|
||||||
if (compactSlot >= 0 && compactSlot < slotCount) {
|
// compact slot is < 0 if there are less than 3 checked checkboxes
|
||||||
compactSlots.add(compactSlot);
|
compactSlots.add(compactSlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ArrayList<>(compactSlots);
|
return compactSlots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
package org.schabi.newpipe.player.notification;
|
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.annotation.SuppressLint;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ServiceInfo;
|
import android.content.pm.ServiceInfo;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.core.app.PendingIntentCompat;
|
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.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
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.
|
* This is a utility class for player notifications.
|
||||||
*/
|
*/
|
||||||
@ -100,29 +92,21 @@ public final class NotificationUtil {
|
|||||||
final NotificationCompat.Builder builder =
|
final NotificationCompat.Builder builder =
|
||||||
new NotificationCompat.Builder(player.getContext(),
|
new NotificationCompat.Builder(player.getContext(),
|
||||||
player.getContext().getString(R.string.notification_channel_id));
|
player.getContext().getString(R.string.notification_channel_id));
|
||||||
|
final MediaStyle mediaStyle = new MediaStyle();
|
||||||
|
|
||||||
initializeNotificationSlots();
|
// setup media style (compact notification slots and media session)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
// notification actions are ignored on Android 13+, and are replaced by code in
|
||||||
int nonNothingSlotCount = 5;
|
// MediaSessionPlayerUi
|
||||||
if (notificationSlots[3] == NotificationConstants.NOTHING) {
|
final int[] compactSlots = initializeNotificationSlots();
|
||||||
--nonNothingSlotCount;
|
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()
|
player.UIs()
|
||||||
.get(MediaSessionPlayerUi.class)
|
.get(MediaSessionPlayerUi.class)
|
||||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||||
.ifPresent(mediaStyle::setMediaSession);
|
.ifPresent(mediaStyle::setMediaSession);
|
||||||
|
|
||||||
|
// setup notification builder
|
||||||
builder.setStyle(mediaStyle)
|
builder.setStyle(mediaStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
@ -157,8 +141,12 @@ public final class NotificationUtil {
|
|||||||
notificationBuilder.setContentText(player.getUploaderName());
|
notificationBuilder.setContentText(player.getUploaderName());
|
||||||
notificationBuilder.setTicker(player.getVideoTitle());
|
notificationBuilder.setTicker(player.getVideoTitle());
|
||||||
|
|
||||||
|
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);
|
updateActions(notificationBuilder);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -209,12 +197,35 @@ public final class NotificationUtil {
|
|||||||
// ACTIONS
|
// 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) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
notificationSlots[i] = player.getPrefs().getInt(
|
notificationSlots[i] = player.getPrefs().getInt(
|
||||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
NotificationConstants.SLOT_DEFAULTS[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")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -227,115 +238,15 @@ public final class NotificationUtil {
|
|||||||
|
|
||||||
private void addAction(final NotificationCompat.Builder builder,
|
private void addAction(final NotificationCompat.Builder builder,
|
||||||
@NotificationConstants.Action final int slot) {
|
@NotificationConstants.Action final int slot) {
|
||||||
final NotificationCompat.Action action = getAction(slot);
|
@Nullable final NotificationActionData data =
|
||||||
if (action != null) {
|
NotificationActionData.fromNotificationActionEnum(player, slot);
|
||||||
builder.addAction(action);
|
if (data == null) {
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
final PendingIntent intent = PendingIntentCompat.getBroadcast(player.getContext(),
|
||||||
private NotificationCompat.Action getAction(
|
NOTIFICATION_ID, new Intent(data.action()), FLAG_UPDATE_CURRENT, false);
|
||||||
@NotificationConstants.Action final int selectedAction) {
|
builder.addAction(new NotificationCompat.Action(data.icon(), data.name(), intent));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getIntentForNotification() {
|
private Intent getIntentForNotification() {
|
||||||
|
@ -5,35 +5,22 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.ColorStateList;
|
import android.os.Build;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.Preference;
|
||||||
import androidx.preference.PreferenceViewHolder;
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
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.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.List;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
@ -45,8 +32,9 @@ public class NotificationActionsPreference extends Preference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Nullable private NotificationSlot[] notificationSlots = null;
|
private NotificationSlot[] notificationSlots;
|
||||||
@Nullable private List<Integer> compactSlots = null;
|
private List<Integer> compactSlots;
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@ -56,6 +44,11 @@ public class NotificationActionsPreference extends Preference {
|
|||||||
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
|
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
|
||||||
super.onBindViewHolder(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);
|
holder.itemView.setClickable(false);
|
||||||
setupActions(holder.itemView);
|
setupActions(holder.itemView);
|
||||||
}
|
}
|
||||||
@ -75,13 +68,29 @@ public class NotificationActionsPreference extends Preference {
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void setupActions(@NonNull final View view) {
|
private void setupActions(@NonNull final View view) {
|
||||||
compactSlots = NotificationConstants.getCompactSlotsFromPreferences(getContext(),
|
compactSlots = new ArrayList<>(NotificationConstants.getCompactSlotsFromPreferences(
|
||||||
getSharedPreferences(), 5);
|
getContext(), getSharedPreferences()));
|
||||||
notificationSlots = IntStream.range(0, 5)
|
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);
|
.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
|
// Saving
|
||||||
@ -99,143 +108,10 @@ public class NotificationActionsPreference extends Preference {
|
|||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
notificationSlots[i].selectedAction);
|
notificationSlots[i].getSelectedAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.apply();
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
@ -8,7 +7,7 @@
|
|||||||
android:paddingTop="16dp">
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/summary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
@ -30,7 +29,7 @@
|
|||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView" />
|
app:layout_constraintTop_toBottomOf="@+id/summary" />
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/notificationAction1"
|
android:id="@+id/notificationAction1"
|
||||||
@ -68,4 +67,4 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/notificationAction3" />
|
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_2_title">Third action button</string>
|
||||||
<string name="notification_action_3_title">Fourth 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_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_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_repeat">Repeat</string>
|
||||||
<string name="notification_action_shuffle">Shuffle</string>
|
<string name="notification_action_shuffle">Shuffle</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user