Merged 'dev' branch

This commit is contained in:
Avently 2020-09-27 15:04:20 +03:00
commit 609bf64856
66 changed files with 1863 additions and 784 deletions

View file

@ -33,7 +33,7 @@ android {
// suffix the app id and the app name with git branch name // suffix the app id and the app name with git branch name
def workingBranch = getGitWorkingBranch() def workingBranch = getGitWorkingBranch()
def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase() def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "")
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") { if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
// default values when branch name could not be determined or is master or dev // default values when branch name could not be determined or is master or dev
applicationIdSuffix ".debug" applicationIdSuffix ".debug"

View file

@ -44,8 +44,9 @@
</receiver> </receiver>
<service <service
android:name=".player.MainPlayer" android:name=".player.MainPlayer"
android:exported="false"> android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>

View file

@ -39,6 +39,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
@ -268,7 +269,7 @@ public class RouterActivity extends AppCompatActivity {
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
final LinearLayout rootLayout = (LinearLayout) inflater.inflate( final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.preferred_player_dialog_view, null, false); R.layout.single_choice_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
@ -278,6 +279,7 @@ public class RouterActivity extends AppCompatActivity {
handleChoice(choice.key); handleChoice(choice.key);
// open future streams always like this one, because "always" button was used by user
if (which == DialogInterface.BUTTON_POSITIVE) { if (which == DialogInterface.BUTTON_POSITIVE) {
preferences.edit() preferences.edit()
.putString(getString(R.string.preferred_open_action_key), choice.key) .putString(getString(R.string.preferred_open_action_key), choice.key)
@ -377,23 +379,50 @@ public class RouterActivity extends AppCompatActivity {
final boolean isExtAudioEnabled = preferences.getBoolean( final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false); getString(R.string.use_external_audio_player_key), false);
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
getString(R.string.show_info), getString(R.string.video_player_key), getString(R.string.video_player),
resolveResourceIdFromAttr(context, R.attr.ic_info_outline))); resolveResourceIdFromAttr(context, R.attr.ic_play_arrow));
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
getString(R.string.show_info_key), getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.ic_info_outline));
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
getString(R.string.popup_player_key), getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.ic_popup));
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
getString(R.string.background_player_key), getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.ic_headset));
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) { if (linkType == LinkType.STREAM) {
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), if (isExtVideoEnabled) {
getString(R.string.video_player), // show both "show info" and "video player", they are two different activities
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow))); returnList.add(showInfo);
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), returnList.add(videoPlayer);
getString(R.string.popup_player), } else if (capabilities.contains(VIDEO)
resolveResourceIdFromAttr(context, R.attr.ic_popup))); && PlayerHelper.isAutoplayAllowedByUser(context)) {
} // show only "video player" since the details activity will be opened and the video
// will be autoplayed there and "show info" would do the exact same thing
returnList.add(videoPlayer);
} else {
// show only "show info" if video player is not applicable or autoplay is disabled
returnList.add(showInfo);
}
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) { if (capabilities.contains(VIDEO)) {
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), returnList.add(popupPlayer);
getString(R.string.background_player), }
resolveResourceIdFromAttr(context, R.attr.ic_headset))); if (capabilities.contains(AUDIO)) {
returnList.add(backgroundPlayer);
}
} else {
returnList.add(showInfo);
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
returnList.add(videoPlayer);
returnList.add(popupPlayer);
}
if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
returnList.add(backgroundPlayer);
}
} }
returnList.add(new AdapterChoiceItem(getString(R.string.download_key), returnList.add(new AdapterChoiceItem(getString(R.string.download_key),

View file

@ -1257,7 +1257,7 @@ public class VideoDetailFragment
} }
private boolean isExternalPlayerEnabled() { private boolean isExternalPlayerEnabled() {
return PreferenceManager.getDefaultSharedPreferences(getContext()) return PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.use_external_video_player_key), false); .getBoolean(getString(R.string.use_external_video_player_key), false);
} }
@ -1268,23 +1268,7 @@ public class VideoDetailFragment
&& !isExternalPlayerEnabled() && !isExternalPlayerEnabled()
&& (player == null || player.videoPlayerSelected()) && (player == null || player.videoPlayerSelected())
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN && bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
&& isAutoplayAllowedByUser(); && PlayerHelper.isAutoplayAllowedByUser(requireContext());
}
private boolean isAutoplayAllowedByUser() {
if (activity == null) {
return false;
}
switch (PlayerHelper.getAutoplayType(activity)) {
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
return false;
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
return !ListHelper.isMeteredNetwork(activity);
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
default:
return true;
}
} }
private void addVideoPlayerView() { private void addVideoPlayerView() {

View file

@ -180,6 +180,8 @@ public abstract class BasePlayer implements
@NonNull @NonNull
protected final HistoryRecordManager recordManager; protected final HistoryRecordManager recordManager;
@NonNull @NonNull
protected final SharedPreferences sharedPreferences;
@NonNull
protected final CustomTrackSelector trackSelector; protected final CustomTrackSelector trackSelector;
@NonNull @NonNull
protected final PlayerDataSource dataSource; protected final PlayerDataSource dataSource;
@ -211,6 +213,7 @@ public abstract class BasePlayer implements
setupBroadcastReceiver(intentFilter); setupBroadcastReceiver(intentFilter);
this.recordManager = new HistoryRecordManager(context); this.recordManager = new HistoryRecordManager(context);
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
this.progressUpdateReactor = new SerialDisposable(); this.progressUpdateReactor = new SerialDisposable();
this.databaseUpdateReactor = new CompositeDisposable(); this.databaseUpdateReactor = new CompositeDisposable();
@ -1239,7 +1242,15 @@ public abstract class BasePlayer implements
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
} }
if (simpleExoPlayer != null) { if (simpleExoPlayer != null) {
simpleExoPlayer.seekTo(positionMillis); // prevent invalid positions when fast-forwarding/-rewinding
long normalizedPositionMillis = positionMillis;
if (normalizedPositionMillis < 0) {
normalizedPositionMillis = 0;
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
normalizedPositionMillis = simpleExoPlayer.getDuration();
}
simpleExoPlayer.seekTo(normalizedPositionMillis);
} }
} }

View file

@ -19,35 +19,18 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Binder; import android.os.Binder;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import androidx.preference.PreferenceManager;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.RemoteViews; import android.view.ViewGroup;
import android.view.WindowManager;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -64,7 +47,6 @@ public final class MainPlayer extends Service {
private VideoPlayerImpl playerImpl; private VideoPlayerImpl playerImpl;
private WindowManager windowManager; private WindowManager windowManager;
private SharedPreferences sharedPreferences;
private final IBinder mBinder = new MainPlayer.LocalBinder(); private final IBinder mBinder = new MainPlayer.LocalBinder();
@ -78,30 +60,26 @@ public final class MainPlayer extends Service {
// Notification // Notification
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
static final int NOTIFICATION_ID = 123789; static final String ACTION_CLOSE
private NotificationManager notificationManager; = "org.schabi.newpipe.player.MainPlayer.CLOSE";
private NotificationCompat.Builder notBuilder; static final String ACTION_PLAY_PAUSE
private RemoteViews notRemoteView; = "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
private RemoteViews bigNotRemoteView; static final String ACTION_OPEN_CONTROLS
= "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
static final String ACTION_CLOSE = static final String ACTION_REPEAT
"org.schabi.newpipe.player.MainPlayer.CLOSE"; = "org.schabi.newpipe.player.MainPlayer.REPEAT";
static final String ACTION_PLAY_PAUSE = static final String ACTION_PLAY_NEXT
"org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_OPEN_CONTROLS = static final String ACTION_PLAY_PREVIOUS
"org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_REPEAT = static final String ACTION_FAST_REWIND
"org.schabi.newpipe.player.MainPlayer.REPEAT"; = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_PLAY_NEXT = static final String ACTION_FAST_FORWARD
"org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_PLAY_PREVIOUS = static final String ACTION_SHUFFLE
"org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; = "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE";
static final String ACTION_FAST_REWIND = public static final String ACTION_RECREATE_NOTIFICATION
"org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; = "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
static final String ACTION_FAST_FORWARD =
"org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle // Service's LifeCycle
@ -113,9 +91,7 @@ public final class MainPlayer extends Service {
Log.d(TAG, "onCreate() called"); Log.d(TAG, "onCreate() called");
} }
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
createView(); createView();
@ -143,7 +119,7 @@ public final class MainPlayer extends Service {
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { || intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
showNotificationAndStartForeground(); NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
} }
playerImpl.handleIntent(intent); playerImpl.handleIntent(intent);
@ -177,7 +153,7 @@ public final class MainPlayer extends Service {
// So we should hide the notification at all. // So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case // When autoplay enabled such notification flashing is annoying so skip this case
if (!autoplayEnabled) { if (!autoplayEnabled) {
stopForeground(true); NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
} }
} }
} }
@ -232,11 +208,8 @@ public final class MainPlayer extends Service {
playerImpl.removePopupFromView(); playerImpl.removePopupFromView();
playerImpl.destroy(); playerImpl.destroy();
} }
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
stopForeground(true); NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
stopSelf(); stopSelf();
} }
@ -275,206 +248,6 @@ public final class MainPlayer extends Service {
} }
} }
private void showNotificationAndStartForeground() {
resetNotification();
if (getBigNotRemoteView() != null) {
getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
if (getNotRemoteView() != null) {
getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
startForeground(NOTIFICATION_ID, getNotBuilder().build());
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
void resetNotification() {
notBuilder = createNotification();
playerImpl.timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_notification_expanded);
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
final NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
builder.setPriority(NotificationCompat.PRIORITY_MAX);
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(final NotificationCompat.Builder builder) {
final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key), true);
if (isLockScreenThumbnailEnabled) {
playerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
playerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight);
}
private void setupNotification(final RemoteViews remoteViews) {
// Don't show anything until player is playing
if (playerImpl == null) {
return;
}
remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
// Starts VideoDetailFragment or opens BackgroundPlayerActivity.
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getActivity(this, NOTIFICATION_ID,
getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_previous);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_next);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
} else {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_rewind);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_fastforward);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
}
setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode());
}
/**
* Updates the notification, and the play/pause button in it.
* Used for changes on the remoteView
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
synchronized void updateNotification(final int drawableId) {
/*if (DEBUG) {
Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
}*/
if (notBuilder == null) {
return;
}
if (drawableId != -1) {
if (notRemoteView != null) {
notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
playerImpl.timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) {
if (remoteViews == null) {
return;
}
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all);
break;
}
}
private Intent getIntentForNotification() {
final Intent intent;
if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show BackgroundPlayerActivity
intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext());
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
return intent;
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
NotificationCompat.Builder getNotBuilder() {
return notBuilder;
}
RemoteViews getBigNotRemoteView() {
return bigNotRemoteView;
}
RemoteViews getNotRemoteView() {
return notRemoteView;
}
public class LocalBinder extends Binder { public class LocalBinder extends Binder {

View file

@ -0,0 +1,165 @@
package org.schabi.newpipe.player;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public final class NotificationConstants {
private NotificationConstants() { }
public static final int NOTHING = 0;
public static final int PREVIOUS = 1;
public static final int NEXT = 2;
public static final int REWIND = 3;
public static final int FORWARD = 4;
public static final int SMART_REWIND_PREVIOUS = 5;
public static final int SMART_FORWARD_NEXT = 6;
public static final int PLAY_PAUSE = 7;
public static final int PLAY_PAUSE_BUFFERING = 8;
public static final int REPEAT = 9;
public static final int SHUFFLE = 10;
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})
public @interface Action { }
@DrawableRes
public static final int[] ACTION_ICONS = {
0,
R.drawable.exo_icon_previous,
R.drawable.exo_icon_next,
R.drawable.exo_icon_rewind,
R.drawable.exo_icon_fastforward,
R.drawable.exo_icon_previous,
R.drawable.exo_icon_next,
R.drawable.ic_pause_white_24dp,
R.drawable.ic_hourglass_top_white_24dp,
R.drawable.exo_icon_repeat_all,
R.drawable.exo_icon_shuffle_on,
R.drawable.ic_close_white_24dp,
};
@Action
public static final int[] SLOT_DEFAULTS = {
SMART_REWIND_PREVIOUS,
PLAY_PAUSE_BUFFERING,
SMART_FORWARD_NEXT,
REPEAT,
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,
R.string.notification_slot_2_key,
R.string.notification_slot_3_key,
R.string.notification_slot_4_key,
};
public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2};
public static final int[] SLOT_COMPACT_PREF_KEYS = {
R.string.notification_slot_compact_0_key,
R.string.notification_slot_compact_1_key,
R.string.notification_slot_compact_2_key,
};
public static String getActionName(@NonNull final Context context, @Action final int action) {
switch (action) {
case PREVIOUS:
return context.getString(R.string.exo_controls_previous_description);
case NEXT:
return context.getString(R.string.exo_controls_next_description);
case REWIND:
return context.getString(R.string.exo_controls_rewind_description);
case FORWARD:
return context.getString(R.string.exo_controls_fastforward_description);
case SMART_REWIND_PREVIOUS:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_rewind_description),
context.getString(R.string.exo_controls_previous_description));
case SMART_FORWARD_NEXT:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_fastforward_description),
context.getString(R.string.exo_controls_next_description));
case PLAY_PAUSE:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_play_description),
context.getString(R.string.exo_controls_pause_description));
case PLAY_PAUSE_BUFFERING:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_play_description),
context.getString(R.string.exo_controls_pause_description),
context.getString(R.string.notification_action_buffering));
case REPEAT:
return context.getString(R.string.notification_action_repeat);
case SHUFFLE:
return context.getString(R.string.notification_action_shuffle);
case CLOSE:
return context.getString(R.string.close);
case NOTHING: default:
return context.getString(R.string.notification_action_nothing);
}
}
/**
* @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(
@NonNull final Context context,
final SharedPreferences sharedPreferences,
final int slotCount) {
final SortedSet<Integer> compactSlots = new TreeSet<>();
for (int i = 0; i < 3; i++) {
final int compactSlot = sharedPreferences.getInt(
context.getString(SLOT_COMPACT_PREF_KEYS[i]), Integer.MAX_VALUE);
if (compactSlot == Integer.MAX_VALUE) {
// settings not yet populated, return default values
return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS));
}
// a negative value (-1) is set when the user does not want a particular compact slot
if (compactSlot >= 0 && compactSlot < slotCount) {
compactSlots.add(compactSlot);
}
}
return new ArrayList<>(compactSlots);
}
}

View file

@ -0,0 +1,371 @@
package org.schabi.newpipe.player;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.List;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
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.MainPlayer.ACTION_CLOSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
/**
* This is a utility class for player notifications.
*
* @author cool-student
*/
public final class NotificationUtil {
private static final String TAG = NotificationUtil.class.getSimpleName();
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final int NOTIFICATION_ID = 123789;
@Nullable private static NotificationUtil instance = null;
@NotificationConstants.Action
private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder;
private NotificationUtil() {
}
public static NotificationUtil getInstance() {
if (instance == null) {
instance = new NotificationUtil();
}
return instance;
}
/////////////////////////////////////////////////////
// NOTIFICATION
/////////////////////////////////////////////////////
/**
* Creates the notification if it does not exist already and recreates it if forceRecreate is
* true. Updates the notification with the data in the player.
* @param player the player currently open, to take data from
* @param forceRecreate whether to force the recreation of the notification even if it already
* exists
*/
synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player,
final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) {
notificationBuilder = createNotification(player);
}
updateNotification(player);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
private synchronized NotificationCompat.Builder createNotification(
final VideoPlayerImpl player) {
if (DEBUG) {
Log.d(TAG, "createNotification()");
}
notificationManager = NotificationManagerCompat.from(player.context);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context,
player.context.getString(R.string.notification_channel_id));
initializeNotificationSlots(player);
// count the number of real slots, to make sure compact slots indices are not out of bound
int nonNothingSlotCount = 5;
if (notificationSlots[3] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
}
if (notificationSlots[4] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
}
// build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.context, player.sharedPreferences, nonNothingSlotCount);
final int[] compactSlots = new int[compactSlotList.size()];
for (int i = 0; i < compactSlotList.size(); i++) {
compactSlots[i] = compactSlotList.get(i);
}
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(player.mediaSessionManager.getSessionToken())
.setShowActionsInCompactView(compactSlots))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setColor(ContextCompat.getColor(player.context, R.color.gray))
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
return builder;
}
/**
* Updates the notification builder and the button icons depending on the playback state.
* @param player the player currently open, to take data from
*/
private synchronized void updateNotification(final VideoPlayerImpl player) {
if (DEBUG) {
Log.d(TAG, "updateNotification()");
}
// also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context,
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder, player);
setLargeIcon(notificationBuilder, player);
}
@SuppressLint("RestrictedApi")
boolean shouldUpdateBufferingSlot() {
if (notificationBuilder.mActions.size() < 3) {
// this should never happen, but let's make sure notification actions are populated
return true;
}
// only second and third slot could contain PLAY_PAUSE_BUFFERING, update them only if they
// are not already in the buffering state (the only one with a null action intent)
return (notificationSlots[1] == NotificationConstants.PLAY_PAUSE_BUFFERING
&& notificationBuilder.mActions.get(1).actionIntent != null)
|| (notificationSlots[2] == NotificationConstants.PLAY_PAUSE_BUFFERING
&& notificationBuilder.mActions.get(2).actionIntent != null);
}
void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) {
if (notificationBuilder == null) {
notificationBuilder = createNotification(player);
}
updateNotification(player);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
}
void cancelNotificationAndStopForeground(final Service service) {
service.stopForeground(true);
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
notificationManager = null;
notificationBuilder = null;
}
/////////////////////////////////////////////////////
// ACTIONS
/////////////////////////////////////////////////////
private void initializeNotificationSlots(final VideoPlayerImpl player) {
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.sharedPreferences.getInt(
player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
}
}
@SuppressLint("RestrictedApi")
private void updateActions(final NotificationCompat.Builder builder,
final VideoPlayerImpl player) {
builder.mActions.clear();
for (int i = 0; i < 5; ++i) {
addAction(builder, player, notificationSlots[i]);
}
}
private void addAction(final NotificationCompat.Builder builder,
final VideoPlayerImpl player,
@NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(player, slot);
if (action != null) {
builder.addAction(action);
}
}
@Nullable
private NotificationCompat.Action getAction(
final VideoPlayerImpl player,
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
return getAction(player, baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT:
return getAction(player, baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND:
return getAction(player, baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD:
return getAction(player, baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.playQueue != null && player.playQueue.size() > 1) {
return getAction(player, R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else {
return getAction(player, R.drawable.exo_controls_rewind,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.playQueue != null && player.playQueue.size() > 1) {
return getAction(player, R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else {
return getAction(player, R.drawable.exo_controls_fastforward,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
// null intent -> show hourglass icon that does nothing when clicked
return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png,
player.context.getString(R.string.notification_action_buffering),
null);
}
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return getAction(player, R.drawable.ic_replay_white_24dp_png,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying()
|| player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
return getAction(player, R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else {
return getAction(player, 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(player, 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(player, R.drawable.exo_media_action_repeat_one,
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return getAction(player, R.drawable.exo_media_action_repeat_off,
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
}
case NotificationConstants.SHUFFLE:
if (player.playQueue != null && player.playQueue.isShuffled()) {
return getAction(player, R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else {
return getAction(player, R.drawable.exo_controls_shuffle_off,
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
}
case NotificationConstants.CLOSE:
return getAction(player, R.drawable.ic_close_white_24dp_png,
R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING:
default:
// do nothing
return null;
}
}
private NotificationCompat.Action getAction(final VideoPlayerImpl player,
@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.context.getString(title),
PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT));
}
private Intent getIntentForNotification(final VideoPlayerImpl player) {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.context);
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent(
player.context, MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
return intent;
}
}
/////////////////////////////////////////////////////
// BITMAP
/////////////////////////////////////////////////////
private void setLargeIcon(final NotificationCompat.Builder builder,
final VideoPlayerImpl player) {
final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean(
player.context.getString(R.string.scale_to_square_image_in_notifications_key),
false);
if (scaleImageToSquareAspectRatio) {
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
} else {
builder.setLargeIcon(player.getThumbnail());
}
}
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth());
}
private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) {
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
final float scaleWidth = ((float) newWidth) / width;
final float scaleHeight = ((float) newHeight) / height;
final Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
}
}

View file

@ -55,6 +55,7 @@ import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@ -64,6 +65,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
@ -108,10 +110,10 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
import static org.schabi.newpipe.player.MainPlayer.NOTIFICATION_ID; import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
@ -141,7 +143,6 @@ public class VideoPlayerImpl extends VideoPlayer
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private static final float MAX_GESTURE_LENGTH = 0.75f; private static final float MAX_GESTURE_LENGTH = 0.75f;
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
private TextView titleTextView; private TextView titleTextView;
private TextView channelTextView; private TextView channelTextView;
@ -187,7 +188,6 @@ public class VideoPlayerImpl extends VideoPlayer
private boolean isVerticalVideo = false; private boolean isVerticalVideo = false;
private boolean fragmentIsVisible = false; private boolean fragmentIsVisible = false;
boolean shouldUpdateOnProgress; boolean shouldUpdateOnProgress;
int timesNotificationUpdated;
private final MainPlayer service; private final MainPlayer service;
private PlayerServiceEventListener fragmentListener; private PlayerServiceEventListener fragmentListener;
@ -198,9 +198,6 @@ public class VideoPlayerImpl extends VideoPlayer
@NonNull @NonNull
private final AudioPlaybackResolver resolver; private final AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
// Popup // Popup
private WindowManager.LayoutParams popupLayoutParams; private WindowManager.LayoutParams popupLayoutParams;
public WindowManager windowManager; public WindowManager windowManager;
@ -578,29 +575,32 @@ public class VideoPlayerImpl extends VideoPlayer
setupScreenRotationButton(); setupScreenRotationButton();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener // ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
void onShuffleOrRepeatModeChanged() {
updatePlaybackButtons();
updatePlayback();
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override @Override
public void onRepeatModeChanged(final int i) { public void onRepeatModeChanged(final int i) {
super.onRepeatModeChanged(i); super.onRepeatModeChanged(i);
updatePlaybackButtons(); onShuffleOrRepeatModeChanged();
updatePlayback();
service.resetNotification();
service.updateNotification(-1);
} }
@Override @Override
public void onShuffleClicked() { public void onShuffleClicked() {
super.onShuffleClicked(); super.onShuffleClicked();
updatePlaybackButtons(); onShuffleOrRepeatModeChanged();
updatePlayback();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback Listener // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onPlayerError(final ExoPlaybackException error) { public void onPlayerError(final ExoPlaybackException error) {
@ -611,6 +611,13 @@ public class VideoPlayerImpl extends VideoPlayer
} }
} }
@Override
public void onTimelineChanged(final Timeline timeline, final int reason) {
super.onTimelineChanged(timeline, reason);
// force recreate notification to ensure seek bar is shown when preparation finishes
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
}
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag); super.onMetadataChanged(tag);
@ -619,8 +626,7 @@ public class VideoPlayerImpl extends VideoPlayer
titleTextView.setText(tag.getMetadata().getName()); titleTextView.setText(tag.getMetadata().getName());
channelTextView.setText(tag.getMetadata().getUploaderName()); channelTextView.setText(tag.getMetadata().getUploaderName());
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(-1);
updateMetadata(); updateMetadata();
} }
@ -643,35 +649,17 @@ public class VideoPlayerImpl extends VideoPlayer
public void onUpdateProgress(final int currentProgress, public void onUpdateProgress(final int currentProgress,
final int duration, final int bufferPercent) { final int duration, final int bufferPercent) {
super.onUpdateProgress(currentProgress, duration, bufferPercent); super.onUpdateProgress(currentProgress, duration, bufferPercent);
updateProgress(currentProgress, duration, bufferPercent); updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED // setMetadata only updates the metadata when any of the metadata keys are null
|| getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) { mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), getThumbnail(),
return; duration);
} }
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { @Override
service.resetNotification(); public void onPlayQueueEdited() {
} updatePlayback();
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
if (service.getBigNotRemoteView() != null) {
if (cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
service.getBigNotRemoteView()
.setProgressBar(R.id.notificationProgressBar,
duration, currentProgress, false);
service.getBigNotRemoteView()
.setTextViewText(R.id.notificationTime,
getTimeString(currentProgress) + " / " + cachedDurationString);
}
if (service.getNotRemoteView() != null) {
service.getNotRemoteView()
.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
}
service.updateNotification(-1);
} }
@Override @Override
@ -1104,8 +1092,7 @@ public class VideoPlayerImpl extends VideoPlayer
animatePlayButtons(false, 100); animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(false); getRootView().setKeepScreenOn(false);
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(R.drawable.exo_controls_play);
} }
@Override @Override
@ -1113,8 +1100,9 @@ public class VideoPlayerImpl extends VideoPlayer
super.onBuffering(); super.onBuffering();
getRootView().setKeepScreenOn(true); getRootView().setKeepScreenOn(true);
service.resetNotification(); if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
service.updateNotification(R.drawable.exo_controls_play); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
} }
@Override @Override
@ -1132,10 +1120,7 @@ public class VideoPlayerImpl extends VideoPlayer
checkLandscape(); checkLandscape();
getRootView().setKeepScreenOn(true); getRootView().setKeepScreenOn(true);
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(R.drawable.exo_controls_pause);
service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build());
} }
@Override @Override
@ -1151,13 +1136,12 @@ public class VideoPlayerImpl extends VideoPlayer
updateWindowFlags(IDLE_WINDOW_FLAGS); updateWindowFlags(IDLE_WINDOW_FLAGS);
service.resetNotification();
service.updateNotification(R.drawable.exo_controls_play);
// Remove running notification when user don't want music (or video in popup) // Remove running notification when user don't want music (or video in popup)
// to be played in background // to be played in background
if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) { if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) {
service.stopForeground(true); NotificationUtil.getInstance().cancelNotificationAndStopForeground(service);
} else {
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
} }
getRootView().setKeepScreenOn(false); getRootView().setKeepScreenOn(false);
@ -1169,8 +1153,7 @@ public class VideoPlayerImpl extends VideoPlayer
animatePlayButtons(false, 100); animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true); getRootView().setKeepScreenOn(true);
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(R.drawable.exo_controls_play);
} }
@ -1180,23 +1163,20 @@ public class VideoPlayerImpl extends VideoPlayer
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
}); });
getRootView().setKeepScreenOn(false);
getRootView().setKeepScreenOn(false);
updateWindowFlags(IDLE_WINDOW_FLAGS); updateWindowFlags(IDLE_WINDOW_FLAGS);
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(R.drawable.ic_replay_white_24dp);
if (isFullscreen) { if (isFullscreen) {
toggleFullscreen(); toggleFullscreen();
} }
super.onCompleted(); super.onCompleted();
} }
@Override @Override
public void destroy() { public void destroy() {
super.destroy(); super.destroy();
service.getContentResolver().unregisterContentObserver(settingsContentObserver); service.getContentResolver().unregisterContentObserver(settingsContentObserver);
} }
@ -1220,6 +1200,8 @@ public class VideoPlayerImpl extends VideoPlayer
intentFilter.addAction(ACTION_PLAY_NEXT); intentFilter.addAction(ACTION_PLAY_NEXT);
intentFilter.addAction(ACTION_FAST_REWIND); intentFilter.addAction(ACTION_FAST_REWIND);
intentFilter.addAction(ACTION_FAST_FORWARD); intentFilter.addAction(ACTION_FAST_FORWARD);
intentFilter.addAction(ACTION_SHUFFLE);
intentFilter.addAction(ACTION_RECREATE_NOTIFICATION);
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED);
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED);
@ -1269,6 +1251,17 @@ public class VideoPlayerImpl extends VideoPlayer
case ACTION_REPEAT: case ACTION_REPEAT:
onRepeatClicked(); onRepeatClicked();
break; break;
case ACTION_SHUFFLE:
onShuffleClicked();
break;
case ACTION_RECREATE_NOTIFICATION:
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
break;
case Intent.ACTION_HEADSET_PLUG: //FIXME
/*notificationManager.cancel(NOTIFICATION_ID);
mediaSessionManager.dispose();
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
break;
case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED:
fragmentIsVisible = true; fragmentIsVisible = true;
useVideoSource(true); useVideoSource(true);
@ -1308,7 +1301,6 @@ public class VideoPlayerImpl extends VideoPlayer
} }
break; break;
} }
service.resetNotification();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -1320,10 +1312,7 @@ public class VideoPlayerImpl extends VideoPlayer
final View view, final View view,
final Bitmap loadedImage) { final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage); super.onLoadingComplete(imageUri, view, loadedImage);
// rebuild notification here since remote view does not release bitmaps, NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
// causing memory leaks
service.resetNotification();
service.updateNotification(-1);
} }
@Override @Override
@ -1331,20 +1320,18 @@ public class VideoPlayerImpl extends VideoPlayer
final View view, final View view,
final FailReason failReason) { final FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason); super.onLoadingFailed(imageUri, view, failReason);
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(-1);
} }
@Override @Override
public void onLoadingCancelled(final String imageUri, final View view) { public void onLoadingCancelled(final String imageUri, final View view) {
super.onLoadingCancelled(imageUri, view); super.onLoadingCancelled(imageUri, view);
service.resetNotification(); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
service.updateNotification(-1);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void setInitialGestureValues() { private void setInitialGestureValues() {
if (getAudioReactor() != null) { if (getAudioReactor() != null) {

View file

@ -3,44 +3,58 @@ package org.schabi.newpipe.player.helper;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.MediaMetadata;
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.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
import androidx.media.session.MediaButtonReceiver; import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager { public class MediaSessionManager {
private static final String TAG = "MediaSessionManager"; private static final String TAG = MediaSessionManager.class.getSimpleName();
public static final boolean DEBUG = MainActivity.DEBUG;
@NonNull @NonNull
private final MediaSessionCompat mediaSession; private final MediaSessionCompat mediaSession;
@NonNull @NonNull
private final MediaSessionConnector sessionConnector; private final MediaSessionConnector sessionConnector;
private int lastAlbumArtHashCode;
public MediaSessionManager(@NonNull final Context context, public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player, @NonNull final Player player,
@NonNull final MediaSessionCallback callback) { @NonNull final MediaSessionCallback callback) {
this.mediaSession = new MediaSessionCompat(context, TAG); mediaSession = new MediaSessionCompat(context, TAG);
this.mediaSession.setActive(true); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mediaSession.setActive(true);
this.sessionConnector = new MediaSessionConnector(mediaSession); mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
this.sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback)); .setState(PlaybackStateCompat.STATE_NONE, -1, 1)
this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); .setActions(PlaybackStateCompat.ACTION_SEEK_TO
this.sessionConnector.setPlayer(player); | PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_STOP)
.build());
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback));
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
sessionConnector.setPlayer(player);
} }
@Nullable @Nullable
@ -49,46 +63,78 @@ public class MediaSessionManager {
return MediaButtonReceiver.handleIntent(mediaSession, intent); return MediaButtonReceiver.handleIntent(mediaSession, intent);
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public MediaSessionCompat.Token getSessionToken() {
public void setLockScreenArt(final NotificationCompat.Builder builder, return mediaSession.getSessionToken();
@Nullable final Bitmap thumbnailBitmap) { }
if (thumbnailBitmap == null || !mediaSession.isActive()) {
public void setMetadata(final String title,
final String artist,
final Bitmap albumArt,
final long duration) {
if (albumArt == null || !mediaSession.isActive()) {
return; return;
} }
mediaSession.setMetadata( if (DEBUG) {
new MediaMetadataCompat.Builder() if (getMetadataAlbumArt() == null) {
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap) Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
.build() }
); if (getMetadataTitle() == null) {
Log.d(TAG, "N_getMetadataTitle: title == null");
}
if (getMetadataArtist() == null) {
Log.d(TAG, "N_getMetadataArtist: artist == null");
}
if (getMetadataDuration() <= 1) {
Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration());
}
}
final MediaStyle mediaStyle = new MediaStyle() if (getMetadataAlbumArt() == null || getMetadataTitle() == null
.setMediaSession(mediaSession.getSessionToken()); || getMetadataArtist() == null || getMetadataDuration() <= 1
|| albumArt.hashCode() != lastAlbumArtHashCode) {
if (DEBUG) {
Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist
+ " thumb: " + albumArt.hashCode() + " d: " + duration);
}
builder.setStyle(mediaStyle); mediaSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build());
lastAlbumArtHashCode = albumArt.hashCode();
}
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap getMetadataAlbumArt() {
public void clearLockScreenArt(final NotificationCompat.Builder builder) { return mediaSession.getController().getMetadata()
mediaSession.setMetadata( .getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
new MediaMetadataCompat.Builder() }
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
.build()
);
final MediaStyle mediaStyle = new MediaStyle() private String getMetadataTitle() {
.setMediaSession(mediaSession.getSessionToken()); return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
builder.setStyle(mediaStyle); private String getMetadataArtist() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
}
private long getMetadataDuration() {
return mediaSession.getController().getMetadata()
.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
} }
/** /**
* Should be called on player destruction to prevent leakage. * Should be called on player destruction to prevent leakage.
*/ */
public void dispose() { public void dispose() {
this.sessionConnector.setPlayer(null); sessionConnector.setPlayer(null);
this.sessionConnector.setQueueNavigator(null); sessionConnector.setQueueNavigator(null);
this.mediaSession.setActive(false); mediaSession.setActive(false);
this.mediaSession.release(); mediaSession.release();
} }
} }

View file

@ -28,6 +28,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ListHelper;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.text.DecimalFormat; import java.text.DecimalFormat;
@ -248,6 +249,18 @@ public final class PlayerHelper {
} }
} }
public static boolean isAutoplayAllowedByUser(@NonNull final Context context) {
switch (PlayerHelper.getAutoplayType(context)) {
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
return false;
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
return !ListHelper.isMeteredNetwork(context);
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
default:
return true;
}
}
@NonNull @NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) { public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;

View file

@ -255,21 +255,21 @@ public class MediaSourceManager {
// Loading and Syncing // Loading and Syncing
switch (event.type()) { switch (event.type()) {
case INIT: case INIT: case REORDER: case ERROR: case SELECT:
case REORDER:
case ERROR:
case SELECT:
loadImmediate(); // low frequency, critical events loadImmediate(); // low frequency, critical events
break; break;
case APPEND: case APPEND: case REMOVE: case MOVE: case RECOVERY:
case REMOVE:
case MOVE:
case RECOVERY:
default: default:
loadDebounced(); // high frequency or noncritical events loadDebounced(); // high frequency or noncritical events
break; break;
} }
// update ui and notification
switch (event.type()) {
case APPEND: case REMOVE: case MOVE: case REORDER:
playbackListener.onPlayQueueEdited();
}
if (!isPlayQueueReady()) { if (!isPlayQueueReady()) {
maybeBlock(); maybeBlock();
playQueue.fetch(); playQueue.fetch();

View file

@ -69,7 +69,7 @@ public interface PlaybackListener {
MediaSource sourceOf(PlayQueueItem item, StreamInfo info); MediaSource sourceOf(PlayQueueItem item, StreamInfo info);
/** /**
* Called when the play queue can no longer to played or used. * Called when the play queue can no longer be played or used.
* Currently, this means the play queue is empty and complete. * Currently, this means the play queue is empty and complete.
* Signals to the listener that it should shutdown. * Signals to the listener that it should shutdown.
* <p> * <p>
@ -77,4 +77,13 @@ public interface PlaybackListener {
* </p> * </p>
*/ */
void onPlaybackShutdown(); void onPlaybackShutdown();
/**
* Called whenever the play queue was edited (items were added, deleted or moved),
* use this to e.g. update notification buttons or fragment ui.
* <p>
* May be called at any time.
* </p>
*/
void onPlayQueueEdited();
} }

View file

@ -20,7 +20,8 @@ public enum UserAction {
DELETE_FROM_HISTORY("delete from history"), DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"), PLAY_STREAM("Play stream"),
DOWNLOAD_POSTPROCESSING("download post-processing"), DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"); DOWNLOAD_FAILED("download failed"),
PREFERENCES_MIGRATION("migration of preferences");
private final String message; private final String message;

View file

@ -5,12 +5,12 @@ import android.os.Bundle;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.util.ThemeHelper;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@ -25,24 +25,16 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
} }
@Override @Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View rootView,
super.onViewCreated(view, savedInstanceState); @Nullable final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
setDivider(null); setDivider(null);
updateTitle(); ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
updateTitle(); ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
}
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(getPreferenceScreen().getTitle());
}
}
} }
} }

View file

@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import java.io.File; import java.io.File;
import java.util.Set;
/* /*
* Created by k3b on 07.01.2016. * Created by k3b on 07.01.2016.
@ -38,6 +39,22 @@ public final class NewPipeSettings {
private NewPipeSettings() { } private NewPipeSettings() { }
public static void initSettings(final Context context) { public static void initSettings(final Context context) {
// check if there are entries in the prefs to determine whether this is the first app run
Boolean isFirstRun = null;
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
.getAll().keySet();
for (final String key: prefsKeys) {
// ACRA stores some info in the prefs during app initialization
// which happens before this method is called. Therefore ignore ACRA-related keys.
if (!key.toLowerCase().startsWith("acra")) {
isFirstRun = false;
break;
}
}
if (isFirstRun == null) {
isFirstRun = true;
}
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true); PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true); PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true); PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
@ -48,6 +65,8 @@ public final class NewPipeSettings {
getVideoDownloadFolder(context); getVideoDownloadFolder(context);
getAudioDownloadFolder(context); getAudioDownloadFolder(context);
SettingMigrations.initMigrations(context, isFirstRun);
} }
private static void getVideoDownloadFolder(final Context context) { private static void getVideoDownloadFolder(final Context context) {

View file

@ -0,0 +1,270 @@
package org.schabi.newpipe.settings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
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.fragment.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.List;
public class NotificationSettingsFragment extends Fragment {
private Switch scaleSwitch;
private NotificationSlot[] notificationSlots;
private SharedPreferences pref;
private List<Integer> compactSlots;
private String scaleKey;
////////////////////////////////////////////////////////////////////////////
// Lifecycle
////////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pref = PreferenceManager.getDefaultSharedPreferences(requireContext());
scaleKey = getString(R.string.scale_to_square_image_in_notifications_key);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.settings_notification, container, false);
}
@Override
public void onViewCreated(@NonNull final View rootView,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
setupScaleSwitch(rootView);
setupActions(rootView);
}
@Override
public void onResume() {
super.onResume();
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.settings_category_notification_title));
}
@Override
public void onPause() {
super.onPause();
saveChanges();
requireContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
}
////////////////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////////////////
private void setupScaleSwitch(@NonNull final View view) {
scaleSwitch = view.findViewById(R.id.notificationScaleSwitch);
scaleSwitch.setChecked(pref.getBoolean(scaleKey, false));
view.findViewById(R.id.notificationScaleSwitchClickableArea)
.setOnClickListener(v -> scaleSwitch.toggle());
}
private void setupActions(@NonNull final View view) {
compactSlots =
NotificationConstants.getCompactSlotsFromPreferences(requireContext(), pref, 5);
notificationSlots = new NotificationSlot[5];
for (int i = 0; i < 5; i++) {
notificationSlots[i] = new NotificationSlot(i, view);
}
}
////////////////////////////////////////////////////////////////////////////
// Saving
////////////////////////////////////////////////////////////////////////////
private void saveChanges() {
final SharedPreferences.Editor editor = pref.edit();
editor.putBoolean(scaleKey, scaleSwitch.isChecked());
for (int i = 0; i < 3; i++) {
editor.putInt(getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]),
(i < compactSlots.size() ? compactSlots.get(i) : -1));
}
for (int i = 0; i < 5; i++) {
editor.putInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
notificationSlots[i].selectedAction);
}
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(requireContext(),
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 = pref.getInt(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(requireContext(),
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(requireContext(), selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(requireContext());
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.single_choice_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final AlertDialog alertDialog = new AlertDialog.Builder(requireContext())
.setTitle(SLOT_TITLES[i])
.setView(radioGroup)
.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
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
// if present set action icon with correct color
if (NotificationConstants.ACTION_ICONS[action] != 0) {
final Drawable drawable = AppCompatResources.getDrawable(requireContext(),
NotificationConstants.ACTION_ICONS[action]);
if (drawable != null) {
final int color = ThemeHelper.resolveColorFromAttr(requireContext(),
android.R.attr.textColorPrimary);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
drawable.setTint(color);
} else {
drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
radioButton.setCompoundDrawablesWithIntrinsicBounds(
null, null, drawable, null);
}
}
radioButton.setText(NotificationConstants.getActionName(requireContext(), action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
radioGroup.addView(radioButton);
}
alertDialog.show();
if (DeviceUtils.isTv(requireContext())) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
}
}

View file

@ -22,9 +22,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -117,7 +115,8 @@ public class PeertubeInstanceListFragment extends Fragment {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
updateTitle(); ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.peertube_instance_url_title));
} }
@Override @Override
@ -176,15 +175,6 @@ public class PeertubeInstanceListFragment extends Fragment {
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
} }
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.peertube_instance_url_title);
}
}
}
private void saveChanges() { private void saveChanges() {
final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
for (final PeertubeInstance instance : instanceList) { for (final PeertubeInstance instance : instanceList) {

View file

@ -0,0 +1,140 @@
package org.schabi.newpipe.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import static org.schabi.newpipe.MainActivity.DEBUG;
public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 2;
private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@Override
public void migrate(final Context context) {
// We changed the content of the dialog which opens when sharing a link to NewPipe
// by removing the "open detail page" option.
// Therefore, show the dialog once again to ensure users need to choose again and are
// aware of the changed dialog.
final SharedPreferences.Editor editor = sp.edit();
editor.putString(context.getString(R.string.preferred_open_action_key),
context.getString(R.string.always_ask_open_action_key));
editor.apply();
}
};
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
protected void migrate(final Context context) {
// The new application workflow introduced in #2907 allows minimizing videos
// while playing to do other stuff within the app.
// For an even better workflow, we minimize a stream when switching the app to play in
// background.
// Therefore, set default value to background, if it has not been changed yet.
final String minimizeOnExitKey = context.getString(R.string.minimize_on_exit_key);
if (sp.getString(minimizeOnExitKey, "")
.equals(context.getString(R.string.minimize_on_exit_none_key))) {
final SharedPreferences.Editor editor = sp.edit();
editor.putString(minimizeOnExitKey,
context.getString(R.string.minimize_on_exit_background_key));
editor.apply();
}
}
};
/**
* List of all implemented migrations.
* <p>
* <b>Append new migrations to the end of the list</b> to keep it sorted ascending.
* If not sorted correctly, migrations which depend on each other, may fail.
*/
private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1,
MIGRATION_1_2
};
public static void initMigrations(final Context context, final boolean isFirstRun) {
// setup migrations and check if there is something to do
sp = PreferenceManager.getDefaultSharedPreferences(context);
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
// no migration to run, already up to date
if (isFirstRun) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {
return;
}
// run migrations
int currentVersion = lastPrefVersion;
for (final Migration currentMigration : SETTING_MIGRATIONS) {
try {
if (currentMigration.shouldMigrate(currentVersion)) {
if (DEBUG) {
Log.d(TAG, "Migrating preferences from version "
+ currentVersion + " to " + currentMigration.newVersion);
}
currentMigration.migrate(context);
currentVersion = currentMigration.newVersion;
}
} catch (final Exception e) {
// save the version with the last successful migration and report the error
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
final ErrorInfo errorInfo = ErrorInfo.make(
UserAction.PREFERENCES_MIGRATION,
"none",
"Migrating preferences from version " + lastPrefVersion + " to "
+ VERSION + ". "
+ "Error at " + currentVersion + " => " + ++currentVersion,
0
);
ErrorActivity.reportError(context, e, SettingMigrations.class, null, errorInfo);
return;
}
}
// store the current preferences version
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
}
private SettingMigrations() { }
abstract static class Migration {
public final int oldVersion;
public final int newVersion;
protected Migration(final int oldVersion, final int newVersion) {
this.oldVersion = oldVersion;
this.newVersion = newVersion;
}
/**
* @param currentVersion current settings version
* @return Returns whether this migration should be run.
* A migration is necessary if the old version of this migration is lower than or equal to
* the current settings version.
*/
private boolean shouldMigrate(final int currentVersion) {
return oldVersion >= currentVersion;
}
protected abstract void migrate(Context context);
}
}

View file

@ -16,9 +16,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -92,7 +90,8 @@ public class ChooseTabsFragment extends Fragment {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
updateTitle(); ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.main_page_content));
} }
@Override @Override
@ -137,15 +136,6 @@ public class ChooseTabsFragment extends Fragment {
tabList.addAll(tabsManager.getTabs()); tabList.addAll(tabsManager.getTabs());
} }
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.main_page_content);
}
}
}
private void saveChanges() { private void saveChanges() {
tabsManager.saveTabs(tabList); tabsManager.saveTabs(tabList);
} }

View file

@ -57,7 +57,7 @@ import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList; import java.util.ArrayList;
@SuppressWarnings({"unused", "WeakerAccess"}) @SuppressWarnings({"unused"})
public final class NavigationHelper { public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag"; public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
@ -69,16 +69,18 @@ public final class NavigationHelper {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@NonNull @NonNull
public static Intent getPlayerIntent(@NonNull final Context context, public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz, @NonNull final Class<T> targetClazz,
@NonNull final PlayQueue playQueue, @Nullable final PlayQueue playQueue,
@Nullable final String quality, @Nullable final String quality,
final boolean resumePlayback) { final boolean resumePlayback) {
final Intent intent = new Intent(context, targetClazz); final Intent intent = new Intent(context, targetClazz);
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (playQueue != null) {
if (cacheKey != null) { final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); if (cacheKey != null) {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
}
} }
if (quality != null) { if (quality != null) {
intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
@ -90,53 +92,51 @@ public final class NavigationHelper {
} }
@NonNull @NonNull
public static Intent getPlayerIntent(@NonNull final Context context, public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz, @NonNull final Class<T> targetClazz,
@NonNull final PlayQueue playQueue, @Nullable final PlayQueue playQueue,
final boolean resumePlayback) { final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback); return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
} }
@NonNull @NonNull
public static Intent getPlayerEnqueueIntent(@NonNull final Context context, public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context,
@NonNull final Class targetClazz, @NonNull final Class<T> targetClazz,
@NonNull final PlayQueue playQueue, @Nullable final PlayQueue playQueue,
final boolean selectOnAppend, final boolean selectOnAppend,
final boolean resumePlayback) { final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(BasePlayer.APPEND_ONLY, true)
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
} }
@NonNull @NonNull
public static Intent getPlayerIntent(@NonNull final Context context, public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz, @NonNull final Class<T> targetClazz,
@NonNull final PlayQueue playQueue, @Nullable final PlayQueue playQueue,
final int repeatMode, final int repeatMode,
final float playbackSpeed, final float playbackSpeed,
final float playbackPitch, final float playbackPitch,
final boolean playbackSkipSilence, final boolean playbackSkipSilence,
@Nullable final String playbackQuality, @Nullable final String playbackQuality,
final boolean resumePlayback, final boolean resumePlayback,
final boolean startPaused, final boolean startPaused,
final boolean isMuted) { final boolean isMuted) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.START_PAUSED, startPaused) .putExtra(BasePlayer.START_PAUSED, startPaused)
.putExtra(BasePlayer.IS_MUTED, isMuted); .putExtra(BasePlayer.IS_MUTED, isMuted);
} }
public static void playOnMainPlayer( public static void playOnMainPlayer(final AppCompatActivity activity,
final AppCompatActivity activity, final PlayQueue queue,
final PlayQueue queue, final boolean autoPlay) {
final boolean autoPlay) {
playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay); playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay);
} }
public static void playOnMainPlayer( public static void playOnMainPlayer(final FragmentManager fragmentManager,
final FragmentManager fragmentManager, final PlayQueue queue,
final PlayQueue queue, final boolean autoPlay) {
final boolean autoPlay) {
final PlayQueueItem currentStream = queue.getItem(); final PlayQueueItem currentStream = queue.getItem();
openVideoDetailFragment( openVideoDetailFragment(
fragmentManager, fragmentManager,
@ -148,7 +148,7 @@ public final class NavigationHelper {
} }
public static void playOnMainPlayer(@NonNull final Context context, public static void playOnMainPlayer(@NonNull final Context context,
@NonNull final PlayQueue queue, @Nullable final PlayQueue queue,
@NonNull final StreamingService.LinkType linkType, @NonNull final StreamingService.LinkType linkType,
@NonNull final String url, @NonNull final String url,
@NonNull final String title, @NonNull final String title,
@ -553,18 +553,14 @@ public final class NavigationHelper {
return true; return true;
} }
public static Intent getBackgroundPlayerActivityIntent(final Context context) { public static Intent getPlayQueueActivityIntent(final Context context) {
return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class); final Intent intent = new Intent(context, BackgroundPlayerActivity.class);
}
private static Intent getServicePlayerActivityIntent(final Context context,
final Class activityClass) {
final Intent intent = new Intent(context, activityClass);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} }
return intent; return intent;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Link handling // Link handling
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View file

@ -19,6 +19,7 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -26,7 +27,10 @@ import android.util.TypedValue;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import androidx.annotation.AttrRes; import androidx.annotation.AttrRes;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes; import androidx.annotation.StyleRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -231,4 +235,20 @@ public final class ThemeHelper {
return PreferenceManager.getDefaultSharedPreferences(context) return PreferenceManager.getDefaultSharedPreferences(context)
.getString(themeKey, defaultTheme); .getString(themeKey, defaultTheme);
} }
/**
* Sets the title to the activity, if the activity is an {@link AppCompatActivity} and has an
* action bar.
* @param activity the activity to set the title of
* @param title the title to set to the activity
*/
public static void setTitleToAppCompatActivity(@Nullable final Activity activity,
final CharSequence title) {
if (activity instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(title);
}
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,2l0.01,6L10,12l-3.99,4.01L6,22h12v-6l-4,-4l4,-3.99V2H6zM16,16.5V20H8v-3.5l4,-4L16,16.5z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z"/>
</vector>

View file

@ -468,21 +468,20 @@
android:layout_height="60dp" android:layout_height="60dp"
android:id="@+id/playQueueControl"> android:id="@+id/playQueueControl">
<ImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playQueueClose" android:id="@+id/playQueueClose"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="40dp" android:layout_marginEnd="40dp"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_close_white_24dp"
android:background="?android:selectableItemBackground" android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription"/> android:clickable="true"
android:contentDescription="@string/close"
android:focusable="true"
android:padding="10dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_close_white_24dp" />
<ImageButton <ImageButton
android:id="@+id/repeatButton" android:id="@+id/repeatButton"

View file

@ -10,10 +10,8 @@
android:maxLines="2" android:maxLines="2"
android:minHeight="?attr/listPreferredItemHeightSmall" android:minHeight="?attr/listPreferredItemHeightSmall"
android:paddingEnd="?attr/listPreferredItemPaddingRight" android:paddingEnd="?attr/listPreferredItemPaddingRight"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:paddingStart="?attr/listPreferredItemPaddingLeft" android:paddingStart="?attr/listPreferredItemPaddingLeft"
android:background="?attr/checked_selector" android:background="?attr/checked_selector"
android:textColor="?attr/textColorAlertDialogListItem" android:textColor="?attr/textColorAlertDialogListItem"
tools:drawableLeft="?attr/ic_play" tools:drawableLeft="?attr/ic_play_arrow"
tools:text="Lorem ipsum dolor sit amet" /> tools:text="Lorem ipsum dolor sit amet" />

View file

@ -81,155 +81,155 @@
android:paddingEnd="@dimen/player_main_controls_padding" android:paddingEnd="@dimen/player_main_controls_padding"
android:baselineAligned="false"> android:baselineAligned="false">
<LinearLayout <LinearLayout
android:id="@+id/primaryControls" android:id="@+id/primaryControls"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="45dp" android:minHeight="45dp"
android:baselineAligned="false" android:baselineAligned="false"
android:gravity="top" android:gravity="top"
tools:ignore="RtlHardcoded"> tools:ignore="RtlHardcoded">
<ImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playerCloseButton" android:id="@+id/playerCloseButton"
android:layout_width="36dp" android:layout_width="36dp"
android:layout_height="36dp" android:layout_height="36dp"
android:padding="@dimen/player_main_buttons_padding" android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="?attr/ic_close" app:srcCompat="?attr/ic_close"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded" tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone" /> android:visibility="gone" />
<LinearLayout
android:id="@+id/metadataView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="top"
android:orientation="vertical"
android:layout_marginTop="6dp"
android:layout_marginRight="8dp"
tools:ignore="RtlHardcoded"
android:layout_weight="1">
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONG"/>
<TextView
android:id="@+id/channelTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="12sp"
android:clickable="true"
android:focusable="true"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
<TextView
android:id="@+id/qualityTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="720p"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
android:visibility="visible"
tools:ignore="HardcodedText,RtlHardcoded"/>
<TextView
android:id="@+id/playbackSpeed"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="0dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1x"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/queueButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:paddingTop="5dp"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:paddingBottom="3dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_list_white_24dp"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/moreOptionsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_expand_more_white_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/metadataView" android:id="@+id/secondaryControls"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="top" android:gravity="top"
android:orientation="vertical" android:visibility="invisible"
android:layout_marginTop="6dp"
android:layout_marginRight="8dp"
tools:ignore="RtlHardcoded" tools:ignore="RtlHardcoded"
android:layout_weight="1"> tools:visibility="visible">
<TextView <TextView
android:id="@+id/titleTextView" android:id="@+id/resizeTextView"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="35dp"
android:ellipsize="marquee" android:padding="@dimen/player_main_buttons_padding"
android:fadingEdge="horizontal" android:layout_marginEnd="8dp"
android:marqueeRepeatLimit="marquee_forever" android:gravity="center"
android:scrollHorizontally="true" android:minWidth="50dp"
android:singleLine="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold" android:textStyle="bold"
android:clickable="true" android:background="?attr/selectableItemBackground"
android:focusable="true" tools:ignore="HardcodedText,RtlHardcoded"
tools:ignore="RtlHardcoded" tools:text="FIT"/>
tools:text="The Video Title LONG very LONG"/>
<TextView
android:id="@+id/channelTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="12sp"
android:clickable="true"
android:focusable="true"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
<TextView
android:id="@+id/qualityTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="720p"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
android:visibility="visible"
tools:ignore="HardcodedText,RtlHardcoded"/>
<TextView
android:id="@+id/playbackSpeed"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="0dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1x"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/queueButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:paddingTop="5dp"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:paddingBottom="3dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_list_white_24dp"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/moreOptionsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_expand_more_white_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>
<LinearLayout
android:id="@+id/secondaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:visibility="invisible"
tools:ignore="RtlHardcoded"
tools:visibility="visible">
<TextView
android:id="@+id/resizeTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="50dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"
tools:text="FIT"/>
<FrameLayout <FrameLayout
android:layout_width="0dp" android:layout_width="0dp"
@ -255,60 +255,60 @@
</FrameLayout> </FrameLayout>
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playWithKodi" android:id="@+id/playWithKodi"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="35dp" android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding" android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
app:srcCompat="@drawable/ic_cast_white_24dp" app:srcCompat="@drawable/ic_cast_white_24dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:contentDescription="@string/play_with_kodi_title" android:contentDescription="@string/play_with_kodi_title"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded"/>
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/openInBrowser" android:id="@+id/openInBrowser"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="35dp" android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding" android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
app:srcCompat="@drawable/ic_language_white_24dp" app:srcCompat="@drawable/ic_language_white_24dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:contentDescription="@string/open_in_browser" android:contentDescription="@string/open_in_browser"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded"/>
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/share" android:id="@+id/share"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="35dp" android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding" android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
app:srcCompat="@drawable/ic_share_white_24dp" app:srcCompat="@drawable/ic_share_white_24dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:contentDescription="@string/share" android:contentDescription="@string/share"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded"/>
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchMute" android:id="@+id/switchMute"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="37dp" android:layout_height="37dp"
android:padding="@dimen/player_main_buttons_padding" android:padding="@dimen/player_main_buttons_padding"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
app:srcCompat="@drawable/ic_volume_off_white_24dp" app:srcCompat="@drawable/ic_volume_off_white_24dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:contentDescription="@string/mute" android:contentDescription="@string/mute"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton" android:id="@+id/fullScreenButton"
@ -410,42 +410,42 @@
android:orientation="horizontal" android:orientation="horizontal"
android:weightSum="5.5"> android:weightSum="5.5">
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPreviousButton" android:id="@+id/playPreviousButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_previous_white_24dp" app:srcCompat="@drawable/ic_previous_white_24dp"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPauseButton" android:id="@+id/playPauseButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="60dp" android:layout_height="60dp"
android:layout_weight="1" android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_pause_white_24dp" app:srcCompat="@drawable/ic_pause_white_24dp"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playNextButton" android:id="@+id/playNextButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_next_white_24dp" app:srcCompat="@drawable/ic_next_white_24dp"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
</LinearLayout> </LinearLayout>
@ -464,21 +464,20 @@
android:layout_height="60dp" android:layout_height="60dp"
android:id="@+id/playQueueControl"> android:id="@+id/playQueueControl">
<ImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playQueueClose" android:id="@+id/playQueueClose"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="40dp" android:layout_marginEnd="40dp"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_close_white_24dp"
android:background="?android:selectableItemBackground" android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription"/> android:clickable="true"
android:contentDescription="@string/close"
android:focusable="true"
android:padding="10dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_close_white_24dp" />
<ImageButton <ImageButton
android:id="@+id/repeatButton" android:id="@+id/repeatButton"

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp" android:layout_height="64dp"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout <LinearLayout
android:id="@+id/notificationContent" android:id="@+id/notificationContent"
@ -107,8 +107,8 @@
android:focusable="true" android:focusable="true"
android:padding="5dp" android:padding="5dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp" app:srcCompat="@drawable/ic_close_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"/> tools:ignore="ContentDescription,RtlHardcoded" />
</LinearLayout> </LinearLayout>
<ProgressBar <ProgressBar
@ -121,4 +121,4 @@
android:progressDrawable="@drawable/custom_progress_bar" android:progressDrawable="@drawable/custom_progress_bar"
tools:ignore="RtlHardcoded" tools:ignore="RtlHardcoded"
tools:progress="52"/> tools:progress="52"/>
</FrameLayout> </FrameLayout>

View file

@ -29,8 +29,8 @@
android:focusable="true" android:focusable="true"
android:padding="8dp" android:padding="8dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp" app:srcCompat="@drawable/ic_close_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"/> tools:ignore="ContentDescription,RtlHardcoded" />
<LinearLayout <LinearLayout
@ -162,4 +162,4 @@
android:src="@drawable/exo_controls_next" android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
</RelativeLayout> </RelativeLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout 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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -11,8 +10,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal" android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
app:srcCompat="@drawable/ic_close_white_24dp"
app:backgroundTint="@color/light_youtube_primary_color" app:backgroundTint="@color/light_youtube_primary_color"
app:borderWidth="0dp" app:borderWidth="0dp"
app:fabSize="normal"/> app:fabSize="normal"
</FrameLayout> app:srcCompat="@drawable/ic_close_white_24dp" />
</FrameLayout>

View file

@ -12,7 +12,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="12dp" android:layout_marginLeft="12dp"
android:layout_alignBaseline="@+id/autoplay_switch" android:layout_alignBaseline="@+id/autoplay_switch"
android:text="@string/next_video_title" android:text="@string/exo_controls_next_description"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="12sp" android:textSize="12sp"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />

View file

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Switch
android:id="@+id/notificationScaleSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:text="@string/notification_scale_to_square_image_title"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@+id/notificationScaleSwitch"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/notification_scale_to_square_image_summary"
app:layout_constraintEnd_toStartOf="@+id/notificationScaleSwitch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<View
android:id="@+id/notificationScaleSwitchClickableArea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toTopOf="@+id/divider"
app:layout_constraintTop_toTopOf="parent"
android:background="?android:selectableItemBackground" />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:background="?android:attr/listDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<TextView
android:id="@+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:lines="4"
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_toBottomOf="@+id/divider" />
<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/textView4" />
<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/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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,88 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/notificationActionIcon"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:tint="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_previous_white_24dp" />
<TextView
android:id="@+id/notificationActionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/notificationActionSummary"
app:layout_constraintEnd_toEndOf="@id/notificationActionClickableArea"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/notificationActionIcon"
app:layout_constraintTop_toTopOf="@+id/notificationActionIcon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/notification_action_1_title" />
<TextView
android:id="@+id/notificationActionSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@+id/notificationActionIcon"
app:layout_constraintEnd_toEndOf="@+id/notificationActionClickableArea"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/notificationActionTitle"
app:layout_constraintTop_toBottomOf="@+id/notificationActionTitle"
tools:text="@string/notification_action_play_pause_buffering_value" />
<View
android:id="@+id/notificationActionClickableArea"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/notificationActionCheckBoxClickableArea"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/notificationActionCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="@+id/notificationActionCheckBoxClickableArea"
app:layout_constraintEnd_toEndOf="@+id/notificationActionCheckBoxClickableArea"
app:layout_constraintStart_toStartOf="@+id/notificationActionCheckBoxClickableArea"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/notificationActionCheckBoxClickableArea"
android:layout_width="0dp"
android:layout_height="0dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -47,7 +47,7 @@
android:contentDescription="@string/search" android:contentDescription="@string/search"
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="?attr/ic_close" app:srcCompat="?attr/ic_close"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded" />
</FrameLayout> </FrameLayout>
</FrameLayout> </FrameLayout>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources translatable="false"> <resources translatable="false">
<!-- App versioning -->
<string name="last_used_version" translatable="false">last_used_version</string>
<string name="last_used_preferences_version" translatable="false">last_used_preferences_version</string>
<!-- Service --> <!-- Service -->
<string-array name="service_list" translatable="false"> <string-array name="service_list" translatable="false">
<item>@string/youtube</item> <item>@string/youtube</item>
@ -52,7 +56,7 @@
</string-array> </string-array>
<string name="minimize_on_exit_key" translatable="false">minimize_on_exit_key</string> <string name="minimize_on_exit_key" translatable="false">minimize_on_exit_key</string>
<string name="minimize_on_exit_value" translatable="false">@string/minimize_on_exit_none_key</string> <string name="minimize_on_exit_value" translatable="false">@string/minimize_on_exit_background_key</string>
<string name="minimize_on_exit_none_key" translatable="false">minimize_on_exit_none_key</string> <string name="minimize_on_exit_none_key" translatable="false">minimize_on_exit_none_key</string>
<string name="minimize_on_exit_background_key" translatable="false">minimize_on_exit_background_key</string> <string name="minimize_on_exit_background_key" translatable="false">minimize_on_exit_background_key</string>
<string name="minimize_on_exit_popup_key" translatable="false">minimize_on_exit_popup_key</string> <string name="minimize_on_exit_popup_key" translatable="false">minimize_on_exit_popup_key</string>
@ -114,6 +118,19 @@
<item>144p</item> <item>144p</item>
</string-array> </string-array>
<string name="settings_notifications_compact_view_key" translatable="false">notifications_compact_view</string>
<string name="settings_notifications_compact_view_default_value" translatable="false">0,1,2</string>
<string name="scale_to_square_image_in_notifications_key" translatable="false">scale_to_square_image_in_notifications</string>
<string name="notification_slot_0_key" translatable="false">notification_slot_0_key</string>
<string name="notification_slot_1_key" translatable="false">notification_slot_1_key</string>
<string name="notification_slot_2_key" translatable="false">notification_slot_2_key</string>
<string name="notification_slot_3_key" translatable="false">notification_slot_3_key</string>
<string name="notification_slot_4_key" translatable="false">notification_slot_4_key</string>
<string name="notification_slot_compact_0_key" translatable="false">notification_slot_compact_0_key</string>
<string name="notification_slot_compact_1_key" translatable="false">notification_slot_compact_1_key</string>
<string name="notification_slot_compact_2_key" translatable="false">notification_slot_compact_2_key</string>
<string name="video_mp4_key" translatable="false">video_mp4</string> <string name="video_mp4_key" translatable="false">video_mp4</string>
<string name="video_webm_key" translatable="false">video_webm</string> <string name="video_webm_key" translatable="false">video_webm</string>
@ -199,7 +216,6 @@
<string name="playback_skip_silence_key" translatable="false">playback_skip_silence_key</string> <string name="playback_skip_silence_key" translatable="false">playback_skip_silence_key</string>
<string name="app_language_key" translatable="false">app_language_key</string> <string name="app_language_key" translatable="false">app_language_key</string>
<string name="enable_lock_screen_video_thumbnail_key" translatable="false">enable_lock_screen_video_thumbnail</string>
<string name="feed_update_threshold_key" translatable="false">feed_update_threshold_key</string> <string name="feed_update_threshold_key" translatable="false">feed_update_threshold_key</string>
<string name="feed_update_threshold_default_value" translatable="false">300</string> <string name="feed_update_threshold_default_value" translatable="false">300</string>

View file

@ -57,9 +57,23 @@
<string name="kore_not_found">Install missing Kore app?</string> <string name="kore_not_found">Install missing Kore app?</string>
<string name="kore_package" translatable="false">org.xbmc.kore</string> <string name="kore_package" translatable="false">org.xbmc.kore</string>
<string name="show_play_with_kodi_title">Show \"Play with Kodi\" option</string> <string name="show_play_with_kodi_title">Show \"Play with Kodi\" option</string>
<string name="enable_lock_screen_video_thumbnail_title">Lock screen video thumbnail</string>
<string name="show_play_with_kodi_summary">Display an option to play a video via Kodi media center</string> <string name="show_play_with_kodi_summary">Display an option to play a video via Kodi media center</string>
<string name="enable_lock_screen_video_thumbnail_summary">A video thumbnail is shown on the lock screen when using the background player</string>
<string name="notification_scale_to_square_image_title">Scale thumbnail to 1:1 aspect ratio</string>
<string name="notification_scale_to_square_image_summary">Scale the video thumbnail shown in the notification from 16:9 to 1:1 aspect ratio (may introduce distortions)</string>
<string name="notification_action_0_title">First action button</string>
<string name="notification_action_1_title">Second 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_4_title">Fifth action button</string>
<string name="notification_actions_summary">Edit each notification action below by tapping on it.\nSelect up to three of them to be shown in the compact notification by using the checkboxes on the right.</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>
<string name="notification_action_buffering">Buffering</string>
<string name="notification_action_nothing">Nothing</string>
<string name="play_audio">Audio</string> <string name="play_audio">Audio</string>
<string name="default_audio_format_title">Default audio format</string> <string name="default_audio_format_title">Default audio format</string>
<string name="default_video_format_title">Default video format</string> <string name="default_video_format_title">Default video format</string>
@ -106,7 +120,6 @@
<string name="resume_on_audio_focus_gain_title">Resume playing</string> <string name="resume_on_audio_focus_gain_title">Resume playing</string>
<string name="resume_on_audio_focus_gain_summary">Continue playing after interruptions (e.g. phonecalls)</string> <string name="resume_on_audio_focus_gain_summary">Continue playing after interruptions (e.g. phonecalls)</string>
<string name="download_dialog_title">Download</string> <string name="download_dialog_title">Download</string>
<string name="next_video_title">Next</string>
<string name="autoplay_title">Autoplay</string> <string name="autoplay_title">Autoplay</string>
<string name="show_next_and_similar_title">Show \'Next\' and \'Similar\' videos</string> <string name="show_next_and_similar_title">Show \'Next\' and \'Similar\' videos</string>
<string name="show_hold_to_append_title">Show \"Hold to append\" tip</string> <string name="show_hold_to_append_title">Show \"Hold to append\" tip</string>
@ -134,12 +147,12 @@
<string name="settings_category_other_title">Other</string> <string name="settings_category_other_title">Other</string>
<string name="settings_category_debug_title">Debug</string> <string name="settings_category_debug_title">Debug</string>
<string name="settings_category_updates_title">Updates</string> <string name="settings_category_updates_title">Updates</string>
<string name="settings_category_notification_title">Notification</string>
<string name="background_player_playing_toast">Playing in background</string> <string name="background_player_playing_toast">Playing in background</string>
<string name="popup_playing_toast">Playing in popup mode</string> <string name="popup_playing_toast">Playing in popup mode</string>
<string name="background_player_append">Queued on background player</string> <string name="background_player_append">Queued on background player</string>
<string name="popup_playing_append">Queued on popup player</string> <string name="popup_playing_append">Queued on popup player</string>
<string name="c3s_url" translatable="false">https://www.c3s.cc/</string> <string name="c3s_url" translatable="false">https://www.c3s.cc/</string>
<string name="play_btn_text">Play</string>
<string name="content">Content</string> <string name="content">Content</string>
<string name="show_age_restricted_content_title">Age restricted content</string> <string name="show_age_restricted_content_title">Age restricted content</string>
<string name="video_is_age_restricted">Show age restricted video. Future changes are possible from the settings.</string> <string name="video_is_age_restricted">Show age restricted video. Future changes are possible from the settings.</string>

View file

@ -1,38 +1,37 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen 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"
android:title="@string/settings_category_appearance_title"> android:title="@string/settings_category_appearance_title">
<ListPreference <ListPreference
app:iconSpaceReserved="false"
android:defaultValue="@string/default_theme_value" android:defaultValue="@string/default_theme_value"
android:entries="@array/theme_description_list" android:entries="@array/theme_description_list"
android:entryValues="@array/theme_values_list" android:entryValues="@array/theme_values_list"
android:key="@string/theme_key" android:key="@string/theme_key"
android:summary="%s" android:summary="%s"
android:title="@string/theme_title"/> android:title="@string/theme_title"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:iconSpaceReserved="false"
android:defaultValue="true" android:defaultValue="true"
android:key="@string/show_hold_to_append_key" android:key="@string/show_hold_to_append_key"
android:summary="@string/show_hold_to_append_summary"
android:title="@string/show_hold_to_append_title" android:title="@string/show_hold_to_append_title"
android:summary="@string/show_hold_to_append_summary"/> app:iconSpaceReserved="false" />
<ListPreference <ListPreference
app:iconSpaceReserved="false"
android:defaultValue="@string/list_view_mode_value" android:defaultValue="@string/list_view_mode_value"
android:entries="@array/list_view_mode_description" android:entries="@array/list_view_mode_description"
android:entryValues="@array/list_view_mode_values" android:entryValues="@array/list_view_mode_values"
android:key="@string/list_view_mode_key" android:key="@string/list_view_mode_key"
android:summary="%s" android:summary="%s"
android:title="@string/list_view_mode"/> android:title="@string/list_view_mode"
app:iconSpaceReserved="false" />
<Preference <Preference
app:iconSpaceReserved="false"
android:key="@string/caption_settings_key" android:key="@string/caption_settings_key"
android:summary="@string/caption_setting_description"
android:title="@string/caption_setting_title" android:title="@string/caption_setting_title"
android:summary="@string/caption_setting_description"/> app:iconSpaceReserved="false" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -35,6 +35,12 @@
android:icon="?attr/ic_language" android:icon="?attr/ic_language"
android:title="@string/content"/> android:title="@string/content"/>
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.NotificationSettingsFragment"
android:icon="?attr/ic_play_arrow"
android:title="@string/settings_category_notification_title"/>
<PreferenceScreen <PreferenceScreen
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment" android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"

View file

@ -81,13 +81,6 @@
android:summary="@string/show_play_with_kodi_summary" android:summary="@string/show_play_with_kodi_summary"
android:title="@string/show_play_with_kodi_title"/> android:title="@string/show_play_with_kodi_title"/>
<SwitchPreferenceCompat
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/enable_lock_screen_video_thumbnail_key"
android:summary="@string/enable_lock_screen_video_thumbnail_summary"
android:title="@string/enable_lock_screen_video_thumbnail_title"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory