From 150c3b413a8e75bad617fc952e1c530b967dcd14 Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Thu, 7 Sep 2017 13:01:02 -0700 Subject: [PATCH] -Fixed memory leak due to permanent remote view bitmap references. -Removed redundant code in popup player. --- .../newpipe/player/BackgroundPlayer.java | 38 ++-- .../org/schabi/newpipe/player/BasePlayer.java | 182 ++++++++++-------- .../newpipe/player/MainVideoPlayer.java | 23 +-- .../newpipe/player/PopupVideoPlayer.java | 158 ++------------- .../schabi/newpipe/player/VideoPlayer.java | 25 +-- .../player/playback/PlaybackManager.java | 49 +++-- .../schabi/newpipe/playlist/PlayQueue.java | 6 +- .../events/{SwapEvent.java => MoveEvent.java} | 6 +- .../playlist/events/PlayQueueEvent.java | 2 +- .../playlist/events/PlayQueueMessage.java | 4 +- 10 files changed, 204 insertions(+), 289 deletions(-) rename app/src/main/java/org/schabi/newpipe/playlist/events/{SwapEvent.java => MoveEvent.java} (69%) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 0cc6a7628..8c8339573 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -146,7 +146,7 @@ public class BackgroundPlayer extends Service { private void onScreenOnOff(boolean on) { if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); if (on) { - if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning.get()) basePlayerImpl.startProgressLoop(); + if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning()) basePlayerImpl.startProgressLoop(); } else basePlayerImpl.stopProgressLoop(); } @@ -212,7 +212,7 @@ public class BackgroundPlayer extends Service { * * @param drawableId if != -1, sets the drawable with that id on the play/pause button */ - private void updateNotification(int drawableId) { + private synchronized void updateNotification(int drawableId) { if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); if (notBuilder == null) return; if (drawableId != -1) { @@ -275,19 +275,27 @@ public class BackgroundPlayer extends Service { } @Override - public void initThumbnail() { + public void initThumbnail(final String url) { if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); updateNotification(-1); - super.initThumbnail(); + super.initThumbnail(url); } @Override public void onThumbnailReceived(Bitmap thumbnail) { super.onThumbnailReceived(thumbnail); + if (thumbnail != null) { - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); + videoThumbnail = thumbnail; + + // rebuild notification here since remote view does not release bitmaps, causing memory leaks + // remove this line to see for yourself + notBuilder = createNotification(); + + if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + updateNotification(-1); } } @@ -303,7 +311,7 @@ public class BackgroundPlayer extends Service { FAST_FORWARD_REWIND_AMOUNT = 10000; } PROGRESS_LOOP_INTERVAL = 1000; - basePlayerImpl.getPlayer().setVolume(1f); + simpleExoPlayer.setVolume(1f); } @Override @@ -382,13 +390,13 @@ public class BackgroundPlayer extends Service { public void sync(final StreamInfo info, final int sortedStreamsIndex) { super.sync(info, sortedStreamsIndex); - basePlayerImpl.setVideoTitle(info.name); - basePlayerImpl.setUploaderName(info.uploader_name); + setVideoTitle(info.name); + setUploaderName(info.uploader_name); - notRemoteView.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); - notRemoteView.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); - bigNotRemoteView.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); - bigNotRemoteView.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); + notRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle()); + notRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName()); + bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle()); + bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName()); updateNotification(-1); } @@ -436,7 +444,7 @@ public class BackgroundPlayer extends Service { onVideoPlayPause(); break; case ACTION_OPEN_DETAIL: - onOpenDetail(BackgroundPlayer.this, basePlayerImpl.getVideoUrl(), basePlayerImpl.getVideoTitle()); + onOpenDetail(BackgroundPlayer.this, getVideoUrl(), getVideoTitle()); break; case ACTION_REPEAT: onRepeatClicked(); @@ -483,7 +491,7 @@ public class BackgroundPlayer extends Service { super.onPaused(); updateNotification(R.drawable.ic_play_arrow_white); - if (isProgressLoopRunning.get()) stopProgressLoop(); + if (isProgressLoopRunning()) stopProgressLoop(); releaseWifiAndCpu(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 526084552..1e593adf2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -30,12 +30,9 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.media.AudioManager; import android.net.Uri; -import android.os.Handler; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; -import android.view.View; -import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; @@ -67,15 +64,14 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.player.playback.PlaybackManager; import org.schabi.newpipe.player.playback.PlaybackListener; +import org.schabi.newpipe.player.playback.PlaybackManager; import org.schabi.newpipe.playlist.ExternalPlayQueue; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -89,7 +85,19 @@ import java.util.ArrayList; import java.util.Formatter; import java.util.List; import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.SerialDisposable; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Predicate; +import io.reactivex.schedulers.Schedulers; /** * Base for the players, joining the common properties @@ -101,7 +109,7 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.OnAudioFocusChangeListener, PlaybackListener { // TODO: Check api version for deprecated audio manager methods - public static final boolean DEBUG = false; + public static final boolean DEBUG = true; public static final String TAG = "BasePlayer"; protected Context context; @@ -134,7 +142,6 @@ public abstract class BasePlayer implements Player.EventListener, protected String videoUrl = ""; protected String videoTitle = ""; protected String videoThumbnailUrl = ""; - protected long videoStartPos = -1; protected String uploaderName = ""; /*////////////////////////////////////////////////////////////////////////// @@ -144,8 +151,8 @@ public abstract class BasePlayer implements Player.EventListener, protected PlaybackManager playbackManager; protected PlayQueue playQueue; - protected int restoreQueueIndex; - protected long restoreWindowPos; + protected int queueStartPos = 0; + protected long videoStartPos = -1; /*////////////////////////////////////////////////////////////////////////// // Player @@ -157,21 +164,19 @@ public abstract class BasePlayer implements Player.EventListener, protected SimpleExoPlayer simpleExoPlayer; protected boolean isPrepared = false; - protected MediaSource mediaSource; protected CacheDataSourceFactory cacheDataSourceFactory; protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); protected final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); protected int PROGRESS_LOOP_INTERVAL = 100; - protected AtomicBoolean isProgressLoopRunning = new AtomicBoolean(); - protected Handler progressLoop; - protected Runnable progressUpdate; + protected Disposable progressUpdateReactor; + + protected SerialDisposable thumbnailReactor; //////////////////////////////////////////////////////////////////////////*/ public BasePlayer(Context context) { this.context = context; - this.progressLoop = new Handler(); this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); this.audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); @@ -184,6 +189,8 @@ public abstract class BasePlayer implements Player.EventListener, this.intentFilter = new IntentFilter(); setupBroadcastReceiver(intentFilter); context.registerReceiver(broadcastReceiver, intentFilter); + + this.thumbnailReactor = new SerialDisposable(); } public void setup() { @@ -223,23 +230,31 @@ public abstract class BasePlayer implements Player.EventListener, simpleExoPlayer.addListener(this); } - public void initListeners() { - progressUpdate = new Runnable() { - @Override - public void run() { - //if(DEBUG) Log.d(TAG, "progressUpdate run() called"); - onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); - if (isProgressLoopRunning.get()) progressLoop.postDelayed(this, PROGRESS_LOOP_INTERVAL); - } - }; + public void initListeners() {} + + protected Disposable getProgressReactor() { + return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .filter(new Predicate() { + @Override + public boolean test(@NonNull Long aLong) throws Exception { + return isProgressLoopRunning(); + } + }) + .subscribe(new Consumer() { + @Override + public void accept(Long aLong) throws Exception { + triggerProgressUpdate(); + } + }); } public void handleIntent(Intent intent) { if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); if (intent == null) return; - restoreQueueIndex = intent.getIntExtra(RESTORE_QUEUE_INDEX, 0); - restoreWindowPos = intent.getLongExtra(START_POSITION, 0); + queueStartPos = intent.getIntExtra(RESTORE_QUEUE_INDEX, 0); + videoStartPos = intent.getLongExtra(START_POSITION, 0); setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed())); switch (intent.getStringExtra(INTENT_TYPE)) { @@ -254,7 +269,6 @@ public abstract class BasePlayer implements Player.EventListener, } } - @SuppressWarnings("unchecked") public void handleExternalPlaylistIntent(Intent intent) { final int serviceId = intent.getIntExtra(ExternalPlayQueue.SERVICE_ID, -1); @@ -286,21 +300,37 @@ public abstract class BasePlayer implements Player.EventListener, playbackManager = new PlaybackManager(this, playQueue); } - public void initThumbnail() { - if (DEBUG) Log.d(TAG, "initThumbnail() called"); - videoThumbnail = null; - if (videoThumbnailUrl == null || videoThumbnailUrl.isEmpty()) return; - ImageLoader.getInstance().resume(); - ImageLoader.getInstance().loadImage(videoThumbnailUrl, new SimpleImageLoadingListener() { + public void initThumbnail(final String url) { + final Callable bitmapCallable = new Callable() { @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - if (simpleExoPlayer == null) return; - if (DEBUG) - Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]"); - videoThumbnail = loadedImage; - onThumbnailReceived(loadedImage); + public Bitmap call() throws Exception { + return ImageLoader.getInstance().loadImageSync(url); } - }); + }; + + Single.fromCallable(bitmapCallable) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new SingleObserver() { + @Override + public void onSubscribe(@NonNull Disposable d) { + thumbnailReactor.set(d); + } + + @Override + public void onSuccess(@NonNull Bitmap bitmap) { + onThumbnailReceived(bitmap); + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e(TAG, "Thumbnail Fetch Failed.", e); + } + }); + } + + public void onThumbnailReceived(Bitmap thumbnail) { + if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]"); } public void destroyPlayer() { @@ -309,7 +339,7 @@ public abstract class BasePlayer implements Player.EventListener, simpleExoPlayer.stop(); simpleExoPlayer.release(); } - if (progressLoop != null && isProgressLoopRunning.get()) stopProgressLoop(); + if (isProgressLoopRunning()) stopProgressLoop(); if (audioManager != null) { audioManager.abandonAudioFocus(this); audioManager = null; @@ -320,7 +350,11 @@ public abstract class BasePlayer implements Player.EventListener, if (DEBUG) Log.d(TAG, "destroy() called"); destroyPlayer(); unregisterBroadcastReceiver(); + + thumbnailReactor.dispose(); + thumbnailReactor = null; videoThumbnail = null; + simpleExoPlayer = null; } @@ -469,19 +503,19 @@ public abstract class BasePlayer implements Player.EventListener, public void onLoading() { if (DEBUG) Log.d(TAG, "onLoading() called"); - if (!isProgressLoopRunning.get()) startProgressLoop(); + if (!isProgressLoopRunning()) startProgressLoop(); } public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); - if (!isProgressLoopRunning.get()) startProgressLoop(); + if (!isProgressLoopRunning()) startProgressLoop(); } public void onBuffering() { } public void onPaused() { - if (isProgressLoopRunning.get()) stopProgressLoop(); + if (isProgressLoopRunning()) stopProgressLoop(); } public void onPausedSeek() { @@ -489,7 +523,7 @@ public abstract class BasePlayer implements Player.EventListener, public void onCompleted() { if (DEBUG) Log.d(TAG, "onCompleted() called"); - if (isProgressLoopRunning.get()) stopProgressLoop(); + if (isProgressLoopRunning()) stopProgressLoop(); } /*////////////////////////////////////////////////////////////////////////// @@ -524,22 +558,25 @@ public abstract class BasePlayer implements Player.EventListener, @Override public void onTimelineChanged(Timeline timeline, Object manifest) { + if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + Log.w(TAG, "onTracksChanged() called, unsupported operation. Is this expected?"); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + ", pitch: " + playbackParameters.pitch); } @Override public void onLoadingChanged(boolean isLoading) { if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); - if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning.get()) stopProgressLoop(); - else if (isLoading && !isProgressLoopRunning.get()) startProgressLoop(); + if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) stopProgressLoop(); + else if (isLoading && !isProgressLoopRunning()) startProgressLoop(); } @Override @@ -595,6 +632,11 @@ public abstract class BasePlayer implements Player.EventListener, playbackManager.refresh(newIndex); } + @Override + public void onRepeatModeChanged(int i) { + if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]"); + } + /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -614,13 +656,13 @@ public abstract class BasePlayer implements Player.EventListener, if (simpleExoPlayer == null) return; if (DEBUG) Log.d(TAG, "Unblocking..."); - if (restoreQueueIndex != playQueue.getIndex()) { - restoreQueueIndex = playQueue.getIndex(); - restoreWindowPos = 0; + if (queueStartPos != playQueue.getIndex()) { + queueStartPos = playQueue.getIndex(); + videoStartPos = 0; } simpleExoPlayer.prepare(playbackManager.getMediaSource()); - simpleExoPlayer.seekTo(playbackManager.getCurrentSourceIndex(), restoreWindowPos); + simpleExoPlayer.seekTo(playbackManager.getCurrentSourceIndex(), videoStartPos); simpleExoPlayer.setPlayWhenReady(false); } @@ -633,15 +675,16 @@ public abstract class BasePlayer implements Player.EventListener, videoThumbnailUrl = info.thumbnail_url; videoTitle = info.name; - initThumbnail(); + initThumbnail(videoThumbnailUrl); if (simpleExoPlayer.getCurrentWindowIndex() != playbackManager.getCurrentSourceIndex()) { if (DEBUG) Log.w(TAG, "Rewinding to correct window"); if (simpleExoPlayer.getCurrentTimeline().getWindowCount() > playbackManager.getCurrentSourceIndex()) { simpleExoPlayer.seekToDefaultPosition(playbackManager.getCurrentSourceIndex()); } else { - Toast.makeText(context, "Play Queue out of sync", Toast.LENGTH_SHORT).show(); - simpleExoPlayer.seekToDefaultPosition(); + if (DEBUG) Log.w(TAG, "Play Queue out of sync"); + playbackManager.reset(); + return; } } @@ -674,26 +717,12 @@ public abstract class BasePlayer implements Player.EventListener, public void onVideoPlayPause() { if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); - if (currentState == STATE_COMPLETED) { - onVideoPlayPauseRepeat(); - return; - } - if (!isPlaying()) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); else audioManager.abandonAudioFocus(this); simpleExoPlayer.setPlayWhenReady(!isPlaying()); } - public void onVideoPlayPauseRepeat() { - if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called"); - changeState(STATE_LOADING); - setVideoStartPos(0); - simpleExoPlayer.seekTo(0); - simpleExoPlayer.setPlayWhenReady(true); - audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); - } - public void onFastRewind() { if (DEBUG) Log.d(TAG, "onFastRewind() called"); seekBy(-FAST_FORWARD_REWIND_AMOUNT); @@ -704,10 +733,6 @@ public abstract class BasePlayer implements Player.EventListener, seekBy(FAST_FORWARD_REWIND_AMOUNT); } - public void onThumbnailReceived(Bitmap thumbnail) { - if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]"); - } - public void seekBy(int milliSeconds) { if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) @@ -746,14 +771,13 @@ public abstract class BasePlayer implements Player.EventListener, } protected void startProgressLoop() { - progressLoop.removeCallbacksAndMessages(null); - isProgressLoopRunning.set(true); - progressLoop.post(progressUpdate); + if (progressUpdateReactor != null) progressUpdateReactor.dispose(); + progressUpdateReactor = getProgressReactor(); } protected void stopProgressLoop() { - isProgressLoopRunning.set(false); - progressLoop.removeCallbacksAndMessages(null); + if (progressUpdateReactor != null) progressUpdateReactor.dispose(); + progressUpdateReactor = null; } protected void tryDeleteCacheFiles(Context context) { @@ -902,4 +926,8 @@ public abstract class BasePlayer implements Player.EventListener, public boolean isPlayerReady() { return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED; } + + public boolean isProgressLoopRunning() { + return progressUpdateReactor != null && !progressUpdateReactor.isDisposed(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index a2f1c3a9c..b4b790086 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -217,7 +217,7 @@ public class MainVideoPlayer extends Activity { MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); gestureDetector = new GestureDetector(context, listener); gestureDetector.setIsLongpressEnabled(false); - playerImpl.getRootView().setOnTouchListener(listener); + getRootView().setOnTouchListener(listener); repeatButton.setOnClickListener(this); playPauseButton.setOnClickListener(this); @@ -252,8 +252,10 @@ public class MainVideoPlayer extends Activity { @Override public void onFullScreenButtonClicked() { + super.onFullScreenButtonClicked(); + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); - if (playerImpl.getPlayer() == null) return; + if (simpleExoPlayer == null) return; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(MainVideoPlayer.this)) { @@ -261,11 +263,11 @@ public class MainVideoPlayer extends Activity { return; } - context.startService(NavigationHelper.getOpenVideoPlayerIntent(context, PopupVideoPlayer.class, playerImpl)); - if (playerImpl != null) playerImpl.destroyPlayer(); + context.startService(NavigationHelper.getOpenVideoPlayerIntent(context, PopupVideoPlayer.class, this)); + destroyPlayer(); ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - MainVideoPlayer.this.finish(); + finish(); } @Override @@ -302,10 +304,10 @@ public class MainVideoPlayer extends Activity { if (getCurrentState() != STATE_COMPLETED) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); - animateView(playerImpl.getControlsRoot(), true, 300, 0, new Runnable() { + animateView(getControlsRoot(), true, 300, 0, new Runnable() { @Override public void run() { - if (getCurrentState() == STATE_PLAYING && !playerImpl.isSomePopupMenuVisible()) { + if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { hideControls(300, DEFAULT_CONTROLS_HIDE_TIME); } } @@ -321,7 +323,7 @@ public class MainVideoPlayer extends Activity { @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); - if (playerImpl.wasPlaying()) { + if (wasPlaying()) { hideControls(100, 0); } } @@ -457,11 +459,6 @@ public class MainVideoPlayer extends Activity { public ImageButton getPlayPauseButton() { return playPauseButton; } - - @Override - public void onRepeatModeChanged(int i) { - - } } private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index c8eb98263..8614b7398 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -67,6 +67,10 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.old.PlayVideoActivity; +import org.schabi.newpipe.player.playback.PlaybackManager; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; @@ -115,14 +119,7 @@ public class PopupVideoPlayer extends Service { private float minimumWidth, minimumHeight; private float maximumWidth, maximumHeight; - private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; private NotificationManager notificationManager; - private NotificationCompat.Builder notBuilder; - private RemoteViews notRemoteView; - - - private ImageLoader imageLoader = ImageLoader.getInstance(); - private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); private VideoPlayerImpl playerImpl; private Disposable currentWorker; @@ -148,7 +145,6 @@ public class PopupVideoPlayer extends Service { if (playerImpl.getPlayer() == null) initPopup(); if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); - if (imageLoader != null) imageLoader.clearMemoryCache(); if (intent.getStringExtra(Constants.KEY_URL) != null) { final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); final String url = intent.getStringExtra(Constants.KEY_URL); @@ -245,61 +241,6 @@ public class PopupVideoPlayer extends Service { windowManager.addView(rootView, windowLayoutParams); } - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - - private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification); - - if (playerImpl.getVideoThumbnail() == null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); - else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail()); - - notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); - notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); - - notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); - - switch (playerImpl.simpleExoPlayer.getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); - break; - case Player.REPEAT_MODE_ONE: - //todo change image - notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 168); - break; - case Player.REPEAT_MODE_ALL: - notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); - break; - } - - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_play_arrow_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContent(notRemoteView); - } - - /** - * 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 - */ - private void updateNotification(int drawableId) { - if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); - if (notBuilder == null || notRemoteView == null) return; - if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); - } - /*////////////////////////////////////////////////////////////////////////// // Misc //////////////////////////////////////////////////////////////////////////*/ @@ -400,25 +341,22 @@ public class PopupVideoPlayer extends Service { @Override public void destroy() { super.destroy(); - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null); } @Override public void onThumbnailReceived(Bitmap thumbnail) { super.onThumbnailReceived(thumbnail); - if (thumbnail != null) { - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); - updateNotification(-1); - } } @Override public void onFullScreenButtonClicked() { + super.onFullScreenButtonClicked(); + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); Intent intent; if (!getSharedPreferences().getBoolean(getResources().getString(R.string.use_old_player_key), false)) { - intent = NavigationHelper.getOpenVideoPlayerIntent(context, MainVideoPlayer.class, playerImpl); - if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false); + intent = NavigationHelper.getOpenVideoPlayerIntent(context, MainVideoPlayer.class, this); + if (!isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } else { intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class) @@ -429,31 +367,10 @@ public class PopupVideoPlayer extends Service { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); - if (playerImpl != null) playerImpl.destroyPlayer(); + destroyPlayer(); stopSelf(); } - @Override - public void onRepeatClicked() { - super.onRepeatClicked(); - switch (simpleExoPlayer.getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - // Drawable didn't work on low API :/ - //notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white); - // Set the icon to 30% opacity - 255 (max) * .3 - notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); - break; - case Player.REPEAT_MODE_ONE: - // todo change image - notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 168); - break; - case Player.REPEAT_MODE_ALL: - notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); - break; - } - updateNotification(-1); - } - @Override public void onDismiss(PopupMenu menu) { super.onDismiss(menu); @@ -469,7 +386,7 @@ public class PopupVideoPlayer extends Service { @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); - if (playerImpl.wasPlaying()) { + if (wasPlaying()) { hideControls(100, 0); } } @@ -507,13 +424,13 @@ public class PopupVideoPlayer extends Service { onVideoClose(); break; case ACTION_PLAY_PAUSE: - playerImpl.onVideoPlayPause(); + onVideoPlayPause(); break; case ACTION_OPEN_DETAIL: - onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl(), playerImpl.getVideoTitle()); + onOpenDetail(PopupVideoPlayer.this, getVideoUrl(), getVideoTitle()); break; case ACTION_REPEAT: - playerImpl.onRepeatClicked(); + onRepeatClicked(); break; } } @@ -524,38 +441,32 @@ public class PopupVideoPlayer extends Service { @Override public void onLoading() { super.onLoading(); - updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onPlaying() { super.onPlaying(); - updateNotification(R.drawable.ic_pause_white); } @Override public void onBuffering() { super.onBuffering(); - updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onPaused() { super.onPaused(); - updateNotification(R.drawable.ic_play_arrow_white); showAndAnimateControl(R.drawable.ic_play_arrow_white, false); } @Override public void onPausedSeek() { super.onPausedSeek(); - updateNotification(R.drawable.ic_play_arrow_white); } @Override public void onCompleted() { super.onCompleted(); - updateNotification(R.drawable.ic_replay_white); showAndAnimateControl(R.drawable.ic_replay_white, false); } @@ -564,10 +475,6 @@ public class PopupVideoPlayer extends Service { public TextView getResizingIndicator() { return resizingIndicator; } - - @Override - public void onRepeatModeChanged(int i) { - } } private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { @@ -746,49 +653,16 @@ public class PopupVideoPlayer extends Service { this.serviceId = serviceId; } - public void onReceive(StreamInfo info) { - playerImpl.setVideoTitle(info.name); - playerImpl.setVideoUrl(info.url); - playerImpl.setVideoThumbnailUrl(info.thumbnail_url); - playerImpl.setUploaderName(info.uploader_name); - - playerImpl.setVideoStreamsList(new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false))); - playerImpl.setAudioStream(ListHelper.getHighestQualityAudio(info.audio_streams)); - - int defaultResolution = ListHelper.getPopupDefaultResolutionIndex(context, playerImpl.getVideoStreamsList()); - playerImpl.setSelectedIndexStream(defaultResolution); - - if (DEBUG) { - Log.d(TAG, "FetcherHandler.StreamExtractor: chosen = " - + MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " " - + info.video_streams.get(defaultResolution).resolution + " > " - + info.video_streams.get(defaultResolution).url); - } - + public void onReceive(final StreamInfo info) { if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000); else playerImpl.setVideoStartPos(-1); mainHandler.post(new Runnable() { @Override public void run() { + playerImpl.playQueue = new SinglePlayQueue(info, PlayQueueItem.DEFAULT_QUALITY); playerImpl.playQueue.init(); - } - }); - - imageLoader.resume(); - imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { - @Override - public void onLoadingComplete(final String imageUri, View view, final Bitmap loadedImage) { - if (playerImpl == null || playerImpl.getPlayer() == null) return; - if (DEBUG) Log.d(TAG, "FetcherHandler.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]"); - mainHandler.post(new Runnable() { - @Override - public void run() { - playerImpl.setVideoThumbnail(loadedImage); - if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - updateNotification(-1); - } - }); + playerImpl.playbackManager = new PlaybackManager(playerImpl, playerImpl.playQueue); } }); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 4837a00c0..242e87683 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -309,7 +309,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. public void onLoading() { if (DEBUG) Log.d(TAG, "onLoading() called"); - if (!isProgressLoopRunning.get()) startProgressLoop(); + if (!isProgressLoopRunning()) startProgressLoop(); controlsVisibilityHandler.removeCallbacksAndMessages(null); animateView(controlsRoot, false, 300); @@ -331,7 +331,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. @Override public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); - if (!isProgressLoopRunning.get()) startProgressLoop(); + if (!isProgressLoopRunning()) startProgressLoop(); showAndAnimateControl(-1, true); loadingPanel.setVisibility(View.GONE); showControlsThenHide(); @@ -362,7 +362,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. public void onCompleted() { if (DEBUG) Log.d(TAG, "onCompleted() called"); - if (isProgressLoopRunning.get()) stopProgressLoop(); + if (isProgressLoopRunning()) stopProgressLoop(); showControls(500); animateView(endScreen, true, 800); @@ -445,22 +445,15 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. } } - @Override - public void onVideoPlayPauseRepeat() { - if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called"); - if (qualityChanged) { - setVideoStartPos(0); - //play(true); - } else super.onVideoPlayPauseRepeat(); - } - @Override public void onThumbnailReceived(Bitmap thumbnail) { super.onThumbnailReceived(thumbnail); if (thumbnail != null) endScreen.setImageBitmap(thumbnail); } - protected abstract void onFullScreenButtonClicked(); + protected void onFullScreenButtonClicked() { + if (!isPlayerReady()) return; + } @Override public void onFastRewind() { @@ -501,8 +494,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. if (qualityPopupMenuGroupId == menuItem.getGroupId()) { if (selectedIndexStream == menuItem.getItemId()) return true; - restoreQueueIndex = playQueue.getIndex(); - restoreWindowPos = simpleExoPlayer.getCurrentPosition(); + queueStartPos = playQueue.getIndex(); + videoStartPos = simpleExoPlayer.getCurrentPosition(); playbackManager.updateCurrent(menuItem.getItemId()); qualityTextView.setText(menuItem.getTitle()); @@ -580,7 +573,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING); - if (!isProgressLoopRunning.get()) startProgressLoop(); + if (!isProgressLoopRunning()) startProgressLoop(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java index 28527f1fb..25798af75 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackManager.java @@ -12,7 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; -import org.schabi.newpipe.playlist.events.SwapEvent; +import org.schabi.newpipe.playlist.events.MoveEvent; import java.util.ArrayList; import java.util.Collections; @@ -91,7 +91,9 @@ public class PlaybackManager { public void report(final Exception error) { // ignore error checking for now, just remove the current index - if (error == null || !tryBlock()) return; + if (error == null) return; + + tryBlock(); final int index = playQueue.getIndex(); playQueue.remove(index); @@ -101,7 +103,7 @@ public class PlaybackManager { } public void updateCurrent(final int newSortedStreamsIndex) { - if (!tryBlock()) return; + tryBlock(); PlayQueueItem item = playQueue.getCurrent(); item.setSortedQualityIndex(newSortedStreamsIndex); @@ -110,6 +112,13 @@ public class PlaybackManager { load(); } + public void reset() { + tryBlock(); + + resetSources(); + load(); + } + public void dispose() { if (playQueueReactor != null) playQueueReactor.cancel(); if (disposables != null) disposables.dispose(); @@ -143,8 +152,8 @@ public class PlaybackManager { switch (event.type()) { case INIT: isBlocked = true; + break; case APPEND: - load(); break; case SELECT: onSelect(); @@ -153,10 +162,9 @@ public class PlaybackManager { final RemoveEvent removeEvent = (RemoveEvent) event; remove(removeEvent.index()); break; - case SWAP: - final SwapEvent swapEvent = (SwapEvent) event; - swap(swapEvent.getFrom(), swapEvent.getTo()); - load(); + case MOVE: + final MoveEvent moveEvent = (MoveEvent) event; + move(moveEvent.getFrom(), moveEvent.getTo()); break; default: break; @@ -167,6 +175,8 @@ public class PlaybackManager { playQueue.fetch(); } else if (playQueue.isEmpty()) { playbackListener.shutdown(); + } else { + load(); // All event warrants a load } if (playQueueReactor != null) playQueueReactor.request(1); @@ -176,9 +186,7 @@ public class PlaybackManager { public void onError(@NonNull Throwable e) {} @Override - public void onComplete() { - dispose(); - } + public void onComplete() {} }; } @@ -214,21 +222,26 @@ public class PlaybackManager { /* * Responds to a SELECT event. - * If the selected item is already loaded, then we simply synchronize and + * + * If the player is being blocked, then nothing should happen. + * + * Otherwise: + * + * When the selected item is already loaded, then we simply synchronize and * start loading some more items. * - * If the current item has not been fully loaded, then the player will be + * When the current item has not been fully loaded, then the player will be * blocked. The sources will be reset and reloaded, to conserve memory. * */ private void onSelect() { - if (isCurrentIndexLoaded() && !isBlocked) { + if (isBlocked) return; + + if (isCurrentIndexLoaded()) { sync(); } else { tryBlock(); resetSources(); } - - load(); } private void sync() { @@ -249,6 +262,7 @@ public class PlaybackManager { final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.get(currentIndex); if (currentItem != null) load(currentItem); + else return; // The rest are just for seamless playback final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); @@ -270,7 +284,6 @@ public class PlaybackManager { return; } - if (disposables.size() > 8) disposables.clear(); disposables.add(d); } @@ -328,7 +341,7 @@ public class PlaybackManager { } } - private void swap(final int source, final int target) { + private void move(final int source, final int target) { final int sourceIndex = sourceToQueueIndex.indexOf(source); final int targetIndex = sourceToQueueIndex.indexOf(target); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index 01b98e19f..f45093e27 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -10,7 +10,7 @@ import org.schabi.newpipe.playlist.events.InitEvent; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.SelectEvent; -import org.schabi.newpipe.playlist.events.SwapEvent; +import org.schabi.newpipe.playlist.events.MoveEvent; import java.io.Serializable; import java.util.ArrayList; @@ -28,7 +28,7 @@ public abstract class PlayQueue implements Serializable { private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); private final int INDEX_CHANGE_DEBOUNCE = 350; - public static final boolean DEBUG = false; + public static final boolean DEBUG = true; private final ArrayList streams; private final AtomicInteger queueIndex; @@ -178,7 +178,7 @@ public abstract class PlayQueue implements Serializable { queueIndex.set(newIndex); } - broadcast(new SwapEvent(source, target)); + broadcast(new MoveEvent(source, target)); } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/events/SwapEvent.java b/app/src/main/java/org/schabi/newpipe/playlist/events/MoveEvent.java similarity index 69% rename from app/src/main/java/org/schabi/newpipe/playlist/events/SwapEvent.java rename to app/src/main/java/org/schabi/newpipe/playlist/events/MoveEvent.java index d8337dea1..9f1107300 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/events/SwapEvent.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/events/MoveEvent.java @@ -1,16 +1,16 @@ package org.schabi.newpipe.playlist.events; -public class SwapEvent implements PlayQueueMessage { +public class MoveEvent implements PlayQueueMessage { final private int from; final private int to; @Override public PlayQueueEvent type() { - return PlayQueueEvent.SWAP; + return PlayQueueEvent.MOVE; } - public SwapEvent(final int from, final int to) { + public MoveEvent(final int from, final int to) { this.from = from; this.to = to; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueEvent.java b/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueEvent.java index 78ac16a79..50828e456 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueEvent.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueEvent.java @@ -16,6 +16,6 @@ public enum PlayQueueEvent { REMOVE, // sent when two streams swap place in the play queue - SWAP + MOVE } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueMessage.java b/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueMessage.java index 83614e709..33720f58a 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueMessage.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/events/PlayQueueMessage.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.playlist.events; -public interface PlayQueueMessage { +import java.io.Serializable; + +public interface PlayQueueMessage extends Serializable { PlayQueueEvent type(); }