mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-22 02:53:09 +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;
|
||||
|
||||
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>
|
||||
|
Loading…
Reference in New Issue
Block a user