From 61b422502b4baab2c1a33693bfdcc0d0d7d2bad4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 13 Mar 2018 20:25:22 -0700 Subject: [PATCH 01/12] -[#1060] Added toggle to disable thumbnail loading. -Added button to wipe metadata cache. -Added more paddings on player buttons. -Added new animations to main player secondary controls and play queue expand/collapse. -Refactored play queue item touch callback for use in all players. -Improved MediaSourceManager to better handle expired stream reloading. -[#1186] Changed live sync button text to "LIVE". -Removed MediaSourceManager loader coupling on main players. -Moved service dependent expiry resolution to ServiceHelper. -[#1186] Fixed livestream timeline updates causing negative time position. -[#1186] Fixed livestream not starting from live-edge. -Fixed main player system UI not retracting on playback start. --- .../org/schabi/newpipe/ImageDownloader.java | 23 +++- .../org/schabi/newpipe/player/BasePlayer.java | 81 ++++++++----- .../newpipe/player/MainVideoPlayer.java | 53 +++------ .../newpipe/player/ServicePlayerActivity.java | 40 +------ .../player/playback/MediaSourceManager.java | 111 ++++++++++-------- .../player/playback/PlaybackListener.java | 10 ++ .../playlist/PlayQueueItemTouchCallback.java | 52 ++++++++ .../settings/HistorySettingsFragment.java | 23 ++++ .../org/schabi/newpipe/util/InfoCache.java | 9 +- .../schabi/newpipe/util/ServiceHelper.java | 12 ++ .../activity_player_queue_control.xml | 6 +- .../main/res/layout/activity_main_player.xml | 16 ++- .../layout/activity_player_queue_control.xml | 6 +- app/src/main/res/layout/player_popup.xml | 6 +- app/src/main/res/values/settings_keys.xml | 4 + app/src/main/res/values/strings.xml | 21 ++-- app/src/main/res/xml/content_settings.xml | 6 + app/src/main/res/xml/history_settings.xml | 5 + 18 files changed, 305 insertions(+), 179 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index 5ea067d0029..8baabed6bea 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -1,25 +1,40 @@ package org.schabi.newpipe; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import org.schabi.newpipe.extractor.NewPipe; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class ImageDownloader extends BaseImageDownloader { + private static final ByteArrayInputStream DUMMY_INPUT_STREAM = + new ByteArrayInputStream(new byte[]{}); + + private final SharedPreferences preferences; + private final String downloadThumbnailKey; + public ImageDownloader(Context context) { super(context); + this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key); } - public ImageDownloader(Context context, int connectTimeout, int readTimeout) { - super(context, connectTimeout, readTimeout); + private boolean isDownloadingThumbnail() { + return preferences.getBoolean(downloadThumbnailKey, true); } protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { - Downloader downloader = (Downloader) NewPipe.getDownloader(); - return downloader.stream(imageUri); + if (isDownloadingThumbnail()) { + final Downloader downloader = (Downloader) NewPipe.getDownloader(); + return downloader.stream(imageUri); + } else { + return DUMMY_INPUT_STREAM; + } } } 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 cee885e22f6..5355e19eee1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -57,6 +57,7 @@ import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; @@ -244,6 +245,7 @@ protected void initPlayback(@NonNull final PlayQueue queue, playQueue = queue; playQueue.init(); + if (playbackManager != null) playbackManager.dispose(); playbackManager = new MediaSourceManager(this, playQueue); if (playQueueAdapter != null) playQueueAdapter.dispose(); @@ -272,7 +274,6 @@ public void destroyPlayer() { public void destroy() { if (DEBUG) Log.d(TAG, "destroy() called"); destroyPlayer(); - clearThumbnailCache(); unregisterBroadcastReceiver(); trackSelector = null; @@ -314,11 +315,6 @@ public void onLoadingCancelled(String imageUri, View view) { if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "]"); } - - protected void clearThumbnailCache() { - ImageLoader.getInstance().clearMemoryCache(); - } - /*////////////////////////////////////////////////////////////////////////// // MediaSource Building //////////////////////////////////////////////////////////////////////////*/ @@ -448,7 +444,6 @@ public void onBlocked() { public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); if (!isProgressLoopRunning()) startProgressLoop(); - if (!isCurrentWindowValid()) seekToDefault(); } public void onBuffering() {} @@ -522,11 +517,9 @@ public void triggerProgressUpdate() { ); } - private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .filter(ignored -> isProgressLoopRunning()) .subscribe(ignored -> triggerProgressUpdate()); } @@ -541,16 +534,19 @@ public void onTimelineChanged(Timeline timeline, Object manifest, (manifest == null ? "no manifest" : "available manifest") + ", " + "timeline size = [" + timeline.getWindowCount() + "], " + "reason = [" + reason + "]"); + if (playQueue == null) return; switch (reason) { case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes - if (playQueue != null && playbackManager != null && - // ensures MediaSourceManager#update is complete - timeline.getWindowCount() == playQueue.size()) { - playbackManager.load(); + // ensures MediaSourceManager#update is complete + final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); + // Ensure dynamic/livestream timeline changes does not cause negative position + if (isPlaylistStable && !isCurrentWindowValid()) { + simpleExoPlayer.seekTo(/*clampToMillis=*/0); } + break; } } @@ -775,6 +771,16 @@ public void onSeekProcessed() { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ + @Override + public boolean isNearPlaybackEdge(final long timeToEndMillis) { + // If live, then not near playback edge + if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false; + + final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); + final long currentDurationMillis = simpleExoPlayer.getDuration(); + return currentDurationMillis - currentPositionMillis < timeToEndMillis; + } + @Override public void onPlaybackBlock() { if (simpleExoPlayer == null) return; @@ -796,7 +802,6 @@ public void onPlaybackUnblock(final MediaSource mediaSource) { if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); simpleExoPlayer.prepare(mediaSource); - seekToDefault(); } @Override @@ -825,16 +830,24 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, if (simpleExoPlayer == null) return; final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Play Queue may be desynchronized: item " + + Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + "index=[" + currentPlayQueueIndex + "], " + "queue index=[" + playQueue.getIndex() + "]"); - // on metadata changed + // Check if bad seek position + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex > currentPlaylistSize) || + currentPlaylistIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to " + + "index=[" + currentPlayQueueIndex + "] with " + + "playlist length=[" + currentPlaylistSize + "]"); + + // If not playing correct stream, change window position } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; - if (DEBUG) Log.d(TAG, "Rewinding to correct" + + if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + " window=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); @@ -858,6 +871,11 @@ abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, @Nullable @Override public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { + final StreamType streamType = info.getStreamType(); + if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { + return null; + } + if (!info.getHlsUrl().isEmpty()) { return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); } else if (!info.getDashMpdUrl().isEmpty()) { @@ -909,6 +927,9 @@ public void onPrepared(boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (playWhenReady) audioReactor.requestAudioFocus(); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + + // On live prepared + if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault(); } public void onVideoPlayPause() { @@ -945,14 +966,15 @@ public void onPlayPrevious() { if (simpleExoPlayer == null || playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); - savePlaybackState(); - - /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. - * Also restart the track if the current track is the first in a queue.*/ - if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { - final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); - simpleExoPlayer.seekTo(startPos); + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, + * restart current track. Also restart the track if the current track + * is the first in a queue.*/ + if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || + playQueue.getIndex() == 0) { + seekToDefault(); + playQueue.offsetIndex(0); } else { + savePlaybackState(); playQueue.offsetIndex(-1); } } @@ -962,7 +984,6 @@ public void onPlayNext() { if (DEBUG) Log.d(TAG, "onPlayNext() called"); savePlaybackState(); - playQueue.offsetIndex(+1); } @@ -975,8 +996,9 @@ public void onSelected(final PlayQueueItem item) { if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { seekToDefault(); } else { - playQueue.setIndex(index); + savePlaybackState(); } + playQueue.setIndex(index); } public void seekBy(int milliSeconds) { @@ -1015,8 +1037,11 @@ private void registerView() { protected void reload() { if (playbackManager != null) { - playbackManager.reset(); - playbackManager.load(); + playbackManager.dispose(); + } + + if (playQueue != null) { + playbackManager = new MediaSourceManager(this, playQueue); } } 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 4f27d1feec6..dd7e0c71e8d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -62,6 +62,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; +import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -76,6 +77,8 @@ import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +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.animateView; import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; @@ -110,7 +113,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); setVolumeControlStream(AudioManager.STREAM_MUSIC); - changeSystemUi(); + hideSystemUi(); setContentView(R.layout.activity_main_player); playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); @@ -597,28 +600,27 @@ private void onQueueClicked() { updatePlaybackButtons(); getControlsRoot().setVisibility(View.INVISIBLE); - queueLayout.setVisibility(View.VISIBLE); + animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true, + DEFAULT_CONTROLS_DURATION); itemsList.scrollToPosition(playQueue.getIndex()); } private void onQueueClosed() { - queueLayout.setVisibility(View.GONE); + animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false, + DEFAULT_CONTROLS_DURATION); queueVisible = false; } private void onMoreOptionsClicked() { if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); - if (secondaryControls.getVisibility() == View.VISIBLE) { - moreOptionsButton.setImageDrawable(getResources().getDrawable( - R.drawable.ic_expand_more_white_24dp)); - animateView(secondaryControls, false, 200); - } else { - moreOptionsButton.setImageDrawable(getResources().getDrawable( - R.drawable.ic_expand_less_white_24dp)); - animateView(secondaryControls, true, 200); - } + final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION); showControls(DEFAULT_CONTROLS_DURATION); } @@ -696,7 +698,6 @@ public void onPlaying() { animatePlayButtons(true, 200); }); - changeSystemUi(); getRootView().setKeepScreenOn(true); } @@ -798,31 +799,11 @@ public void onScrolledDown(RecyclerView recyclerView) { } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - final int sourceIndex = source.getLayoutPosition(); - final int targetIndex = target.getLayoutPosition(); - playQueue.move(sourceIndex, targetIndex); - return true; - } - + return new PlayQueueItemTouchCallback() { @Override - public boolean isLongPressDragEnabled() { - return false; + public void onMove(int sourceIndex, int targetIndex) { + if (playQueue != null) playQueue.move(sourceIndex, targetIndex); } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index c68133094a2..1c3ffe9118f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -34,6 +34,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; +import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -61,9 +62,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; - private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; - private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; - private View rootView; private RecyclerView itemsList; @@ -398,43 +396,11 @@ public void onScrolledDown(RecyclerView recyclerView) { } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { - @Override - public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, - int viewSizeOutOfBounds, int totalSize, - long msSinceStartScroll) { - final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, - viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, - Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); - return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); - } - + return new PlayQueueItemTouchCallback() { @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, - RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - final int sourceIndex = source.getLayoutPosition(); - final int targetIndex = target.getLayoutPosition(); + public void onMove(int sourceIndex, int targetIndex) { if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); - return true; } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index ea13a28e75e..50c069b40f6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.ReorderEvent; +import org.schabi.newpipe.util.ServiceHelper; -import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -42,7 +42,7 @@ import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { - @NonNull private final static String TAG = "MediaSourceManager"; + @NonNull private final String TAG = "MediaSourceManager@" + hashCode(); /** * Determines how many streams before and after the current stream should be loaded. @@ -60,17 +60,18 @@ public class MediaSourceManager { @NonNull private final PlayQueue playQueue; /** - * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing - * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure - * the {@link StreamInfo} used in subsequent playback is up-to-date. - *

- * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to - * replace the expired one on whereupon {@link #loadImmediate()} is called. + * Determines the gap time between the playback position and the playback duration which + * the {@link #getEdgeIntervalSignal()} begins to request loading. * - * @see #loadImmediate() - * @see #isCorrectionNeeded(PlayQueueItem) + * @see #progressUpdateIntervalMillis + * */ + private final long playbackNearEndGapMillis; + /** + * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between + * each request for loading, once {@link #playbackNearEndGapMillis} has reached. * */ - private final long windowRefreshTimeMillis; + private final long progressUpdateIntervalMillis; + @NonNull private final Observable nearEndIntervalSignal; /** * Process only the last load order when receiving a stream of load orders (lessens I/O). @@ -106,23 +107,31 @@ public class MediaSourceManager { public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, - /*loadDebounceMillis=*/400L, - /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); + this(listener, playQueue, /*loadDebounceMillis=*/400L, + /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS), + /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS)); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final long loadDebounceMillis, - final long windowRefreshTimeMillis) { + final long playbackNearEndGapMillis, + final long progressUpdateIntervalMillis) { if (playQueue.getBroadcastReceiver() == null) { throw new IllegalArgumentException("Play Queue has not been initialized."); } + if (playbackNearEndGapMillis < progressUpdateIntervalMillis) { + throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis + + " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis + + " ms] for them to be useful."); + } this.playbackListener = listener; this.playQueue = playQueue; - this.windowRefreshTimeMillis = windowRefreshTimeMillis; + this.playbackNearEndGapMillis = playbackNearEndGapMillis; + this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; + this.nearEndIntervalSignal = getEdgeIntervalSignal(); this.loadDebounceMillis = loadDebounceMillis; this.debouncedSignal = PublishSubject.create(); @@ -161,28 +170,6 @@ public void dispose() { sources.releaseSource(); } - /** - * Loads the current playing stream and the streams within its windowSize bound. - * - * Unblocks the player once the item at the current index is loaded. - * */ - public void load() { - if (DEBUG) Log.d(TAG, "load() called."); - loadDebounced(); - } - - /** - * Blocks the player and repopulate the sources. - * - * Does not ensure the player is unblocked and should be done explicitly - * through {@link #load() load}. - * */ - public void reset() { - if (DEBUG) Log.d(TAG, "reset() called."); - - maybeBlock(); - populateSources(); - } /*////////////////////////////////////////////////////////////////////////// // Event Reactor //////////////////////////////////////////////////////////////////////////*/ @@ -219,11 +206,13 @@ private void onPlayQueueChanged(final PlayQueueEvent event) { switch (event.type()) { case INIT: case ERROR: - reset(); - break; + maybeBlock(); case APPEND: populateSources(); break; + case SELECT: + maybeRenewCurrentIndex(); + break; case REMOVE: final RemoveEvent removeEvent = (RemoveEvent) event; remove(removeEvent.getRemoveIndex()); @@ -238,7 +227,6 @@ private void onPlayQueueChanged(final PlayQueueEvent event) { final ReorderEvent reorderEvent = (ReorderEvent) event; move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); break; - case SELECT: case RECOVERY: default: break; @@ -347,8 +335,13 @@ private void maybeSynchronizePlayer() { // MediaSource Loading //////////////////////////////////////////////////////////////////////////*/ + private Observable getEdgeIntervalSignal() { + return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) + .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); + } + private Disposable getDebouncedLoader() { - return debouncedSignal + return debouncedSignal.mergeWith(nearEndIntervalSignal) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(timestamp -> loadImmediate()); @@ -359,13 +352,14 @@ private void loadDebounced() { } private void loadImmediate() { + if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); // The current item has higher priority final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.getItem(currentIndex); if (currentItem == null) return; // Evict the items being loaded to free up memory - if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { + if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) { loaderReactor.clear(); loadingItems.clear(); } @@ -377,7 +371,7 @@ private void loadImmediate() { final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); final int rightLimit = currentIndex + WINDOW_SIZE + 1; final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>( + final Set items = new HashSet<>( playQueue.getStreams().subList(leftBound,rightBound)); // Do a round robin @@ -385,6 +379,7 @@ private void loadImmediate() { if (excess >= 0) { items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); } + items.remove(currentItem); for (final PlayQueueItem item : items) { maybeLoadItem(item); @@ -405,9 +400,9 @@ private void maybeLoadItem(@NonNull final PlayQueueItem item) { /* No exception handling since getLoadedMediaSource guarantees nonnull return */ .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); + } else { + maybeSynchronizePlayer(); } - - maybeSynchronizePlayer(); } private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { @@ -423,7 +418,8 @@ private Single getLoadedMediaSource(@NonNull final PlayQueue return new FailedMediaSource(stream, exception); } - final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis; + final long expiration = System.currentTimeMillis() + + ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } @@ -467,6 +463,24 @@ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { } } + /** + * Checks if the current playing index contains an expired {@link ManagedMediaSource}. + * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and + * {@link #loadImmediate()} is called to reload the current item. + * */ + private void maybeRenewCurrentIndex() { + final int currentIndex = playQueue.getIndex(); + if (sources.getSize() <= currentIndex) return; + + final ManagedMediaSource currentSource = + (ManagedMediaSource) sources.getMediaSource(currentIndex); + final PlayQueueItem currentItem = playQueue.getItem(); + if (!currentSource.canReplace(currentItem)) return; + + if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); + update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate); + } /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ @@ -476,6 +490,7 @@ private void resetSources() { this.sources.releaseSource(); this.sources = new DynamicConcatenatingMediaSource(false, + // Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order new ShuffleOrder.UnshuffledShuffleOrder(0)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index b37a269e2ec..34c7702bcc5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -11,6 +11,16 @@ import java.util.List; public interface PlaybackListener { + + /** + * Called to check if the currently playing stream is close to the end of its playback. + * Implementation should return true when the current playback position is within + * timeToEndMillis or less until its playback completes or transitions. + * + * May be called at any time. + * */ + boolean isNearPlaybackEdge(final long timeToEndMillis); + /** * Called when the stream at the current queue index is not ready yet. * Signals to the listener to block the player from playing anything and notify the source diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java new file mode 100644 index 00000000000..405dba11ef7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java @@ -0,0 +1,52 @@ +package org.schabi.newpipe.playlist; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; + +public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback { + private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; + private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; + + public PlayQueueItemTouchCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); + } + + public abstract void onMove(final int sourceIndex, final int targetIndex); + + @Override + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, + int viewSizeOutOfBounds, int totalSize, + long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, + Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); + return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + + final int sourceIndex = source.getLayoutPosition(); + final int targetIndex = target.getLayoutPosition(); + onMove(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index e0836e06c53..53e8d6fc41c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -1,12 +1,35 @@ package org.schabi.newpipe.settings; import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.Preference; +import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.InfoCache; public class HistorySettingsFragment extends BasePreferenceFragment { + private String cacheWipeKey; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + cacheWipeKey = getString(R.string.metadata_cache_wipe_key); + } + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.history_settings); } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference.getKey().equals(cacheWipeKey)) { + InfoCache.getInstance().clearCache(); + Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice, + Toast.LENGTH_SHORT).show(); + } + + return super.onPreferenceTreeClick(preference); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 47c45e82af6..ecc66bb406d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -43,7 +43,6 @@ public final class InfoCache { * Trim the cache to this size */ private static final int TRIM_CACHE_TO = 30; - private static final int DEFAULT_TIMEOUT_HOURS = 4; private static final LruCache lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); @@ -66,13 +65,7 @@ public Info getFromKey(int serviceId, @NonNull String url) { public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); - final long expirationMillis; - if (info.getServiceId() == SoundCloud.getServiceId()) { - expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); - } else { - expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); - } - + final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); synchronized (lruCache) { final CacheData data = new CacheData(info, expirationMillis); lruCache.put(keyOf(serviceId, url), data); diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 7d71750eb47..9d71ae83ab5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import java.util.concurrent.TimeUnit; + +import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; + public class ServiceHelper { private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; @@ -98,4 +102,12 @@ private static void setSelectedServicePreferences(Context context, String servic PreferenceManager.getDefaultSharedPreferences(context).edit(). putString(context.getString(R.string.current_service_key), serviceName).apply(); } + + public static long getCacheExpirationMillis(final int serviceId) { + if (serviceId == SoundCloud.getServiceId()) { + return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); + } else { + return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); + } + } } diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index c3480c547a0..11765f9011b 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -301,9 +301,13 @@ android:id="@+id/live_sync" android:layout_width="wrap_content" android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="?attr/colorAccent" + android:maxLength="4" android:background="?attr/selectableItemBackground" android:visibility="gone"/> diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index e7d337c1724..8f608de3a7d 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -308,7 +308,7 @@ android:id="@+id/toggleOrientation" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginLeft="2dp" + android:layout_marginLeft="4dp" android:layout_marginRight="2dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" @@ -325,8 +325,8 @@ android:id="@+id/switchPopup" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginLeft="2dp" - android:layout_marginRight="2dp" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" android:layout_toLeftOf="@id/toggleOrientation" android:layout_centerVertical="true" android:clickable="true" @@ -341,8 +341,8 @@ android:id="@+id/switchBackground" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginLeft="2dp" - android:layout_marginRight="2dp" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" android:layout_toLeftOf="@id/switchPopup" android:layout_centerVertical="true" android:clickable="true" @@ -403,9 +403,13 @@ android:id="@+id/playbackLiveSync" android:layout_width="wrap_content" android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="@android:color/white" + android:maxLength="4" android:visibility="gone" android:background="?attr/selectableItemBackground" tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index 639a8037cba..7f649e38288 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -151,9 +151,13 @@ android:id="@+id/live_sync" android:layout_width="wrap_content" android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="?attr/colorAccent" + android:maxLength="4" android:background="?attr/selectableItemBackground" android:visibility="gone"/> diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml index 9bbd72fece5..0c3ea77df37 100644 --- a/app/src/main/res/layout/player_popup.xml +++ b/app/src/main/res/layout/player_popup.xml @@ -195,9 +195,13 @@ android:id="@+id/playbackLiveSync" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center_vertical" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="@android:color/white" + android:maxLength="4" android:visibility="gone" android:background="?attr/selectableItemBackground" tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index a897aa185ae..68d75737a02 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -160,6 +160,10 @@ import_data export_data + download_thumbnail_key + + cache_wipe_key + file_rename file_replacement_character diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c97f12809e9..e1a3538075d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,11 @@ Remember last size and position of popup Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision + Load thumbnails + Disable to stop all non-cached thumbnail from loading and save on data and memory usage + Wipe cached metadata + Remove all cached webpage data + Metadata cache wiped Auto-queue next stream Automatically append a related stream when playback starts on the last stream in a non-repeating play queue. Player gesture controls @@ -89,7 +94,7 @@ Download Next video Show next and similar videos - Show Hold to Append Tip + Show hold to append tip Show tip when background or popup button is pressed on video details page URL not supported Default content country @@ -98,7 +103,7 @@ Player Behavior Video & Audio - History + History & Cache Popup Appearance Other @@ -418,18 +423,16 @@ ZOOM Auto-generated - Caption Font Size - Smaller Font - Normal Font - Larger Font - - SYNC + Caption font size + Smaller font + Normal font + Larger font Enable LeakCanary Memory leak monitoring may cause app to become unresponsive when heap dumping - Report Out-of-Lifecycle Errors + Report Out-of-lifecycle errors Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index c8c1efb124e..2ce8bf9e6c8 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -37,6 +37,12 @@ android:summary="@string/auto_queue_summary" android:title="@string/auto_queue_title"/> + + + + From a5f9927459f48cdd4f57a648dd1247a85f978efb Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 13 Mar 2018 20:48:26 -0700 Subject: [PATCH 02/12] -Fixed main player animations not working on first call. --- app/src/main/res/layout/activity_main_player.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 8f608de3a7d..c581c3203dc 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -52,7 +52,7 @@ android:id="@+id/playQueuePanel" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" + android:visibility="invisible" android:background="?attr/queue_background_color" tools:visibility="visible"> @@ -254,7 +254,7 @@ android:focusable="true" android:scaleType="fitXY" android:src="@drawable/ic_expand_more_white_24dp" - android:background="?attr/selectableItemBackground" + android:background="?attr/selectableItemBackgroundBorderless" tools:ignore="ContentDescription,RtlHardcoded"/> @@ -266,7 +266,7 @@ android:gravity="top" android:paddingLeft="5dp" android:paddingRight="5dp" - android:visibility="gone" + android:visibility="invisible" tools:ignore="RtlHardcoded" tools:visibility="visible"> From 2fa9aa04f4f6025b2b38306680a6b99d0ab999b4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 13 Mar 2018 21:45:44 -0700 Subject: [PATCH 03/12] -Bump support library and multidex version. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9fa911e54cd..3529a37b174 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ android { } ext { - supportLibVersion = '27.0.2' + supportLibVersion = '27.1.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -77,7 +77,7 @@ dependencies { debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' - debugImplementation 'com.android.support:multidex:1.0.2' + debugImplementation 'com.android.support:multidex:1.0.3' implementation 'io.reactivex.rxjava2:rxjava:2.1.7' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' From 0258726f0a4ffed04b7255389c104b3b93d6e31c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 20:07:20 -0700 Subject: [PATCH 04/12] -Changed thumbnail toggle in disabled mode to load dark dummy image. -Changed play queue items to display service names. -Fixed Soundcloud playlist not fitting thumbnail. -Refactored image display options to follow uniform behavior. -Refactoring and style changes on audio reactor and media button receiver. --- .../java/org/schabi/newpipe/BaseFragment.java | 33 --------- .../org/schabi/newpipe/ImageDownloader.java | 22 +++--- .../fragments/detail/VideoDetailFragment.java | 7 +- .../list/channel/ChannelFragment.java | 7 +- .../list/playlist/PlaylistFragment.java | 4 +- .../fragments/local/LocalItemBuilder.java | 2 - .../local/holder/LocalItemHolder.java | 21 ------ .../local/holder/LocalPlaylistItemHolder.java | 9 +-- .../holder/LocalPlaylistStreamItemHolder.java | 19 +---- .../LocalStatisticStreamItemHolder.java | 17 +---- .../local/holder/PlaylistItemHolder.java | 13 ---- .../holder/RemotePlaylistItemHolder.java | 3 +- .../newpipe/history/WatchHistoryFragment.java | 3 +- .../holder/ChannelMiniInfoItemHolder.java | 17 +---- .../info_list/holder/InfoItemHolder.java | 14 ---- .../holder/PlaylistMiniInfoItemHolder.java | 17 +---- .../holder/StreamMiniInfoItemHolder.java | 16 +---- .../newpipe/player/BackgroundPlayer.java | 72 ++++++++++--------- .../org/schabi/newpipe/player/BasePlayer.java | 10 +-- .../newpipe/player/helper/AudioReactor.java | 44 ++++++------ .../playlist/PlayQueueItemBuilder.java | 41 ++--------- .../settings/ContentSettingsFragment.java | 25 +++++++ .../newpipe/util/ImageDisplayConstants.java | 58 +++++++++++++++ .../main/res/layout/list_playlist_item.xml | 2 +- app/src/main/res/values/strings.xml | 3 +- 25 files changed, 207 insertions(+), 272 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 8967d8cb037..ce431842788 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -8,9 +8,7 @@ import android.util.Log; import android.view.View; -import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import com.squareup.leakcanary.RefWatcher; import icepick.Icepick; @@ -94,35 +92,4 @@ public void setTitle(String title) { activity.getSupportActionBar().setTitle(title); } } - - /*////////////////////////////////////////////////////////////////////////// - // DisplayImageOptions default configurations - //////////////////////////////////////////////////////////////////////////*/ - - public static final DisplayImageOptions BASE_OPTIONS = - new DisplayImageOptions.Builder().cacheInMemory(true).build(); - - public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_OPTIONS) - .showImageOnLoading(R.drawable.buddy) - .showImageForEmptyUri(R.drawable.buddy) - .showImageOnFail(R.drawable.buddy) - .build(); - - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_OPTIONS) - .displayer(new FadeInBitmapDisplayer(250)) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnFail(R.drawable.dummy_thumbnail) - .build(); - - public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_OPTIONS) - .showImageOnLoading(R.drawable.channel_banner) - .showImageForEmptyUri(R.drawable.channel_banner) - .showImageOnFail(R.drawable.channel_banner) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index 8baabed6bea..eb5e92e889d 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -1,26 +1,26 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; import android.preference.PreferenceManager; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import org.schabi.newpipe.extractor.NewPipe; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class ImageDownloader extends BaseImageDownloader { - private static final ByteArrayInputStream DUMMY_INPUT_STREAM = - new ByteArrayInputStream(new byte[]{}); - + private final Resources resources; private final SharedPreferences preferences; private final String downloadThumbnailKey; public ImageDownloader(Context context) { super(context); + this.resources = context.getResources(); this.preferences = PreferenceManager.getDefaultSharedPreferences(context); this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key); } @@ -29,12 +29,18 @@ private boolean isDownloadingThumbnail() { return preferences.getBoolean(downloadThumbnailKey, true); } - protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { + @SuppressLint("ResourceType") + @Override + public InputStream getStream(String imageUri, Object extra) throws IOException { if (isDownloadingThumbnail()) { - final Downloader downloader = (Downloader) NewPipe.getDownloader(); - return downloader.stream(imageUri); + return super.getStream(imageUri, extra); } else { - return DUMMY_INPUT_STREAM; + return resources.openRawResource(R.drawable.dummy_thumbnail_dark); } } + + protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { + final Downloader downloader = (Downloader) NewPipe.getDownloader(); + return downloader.stream(imageUri); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index b3ca5f47f3c..2a95125df97 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -73,6 +73,7 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; @@ -587,7 +588,8 @@ private void initThumbnailViews(StreamInfo info) { imageLoader.displayImage( info.getThumbnailUrl(), thumbnailImageView, - DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() { + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, + new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { ErrorActivity.reportError( @@ -604,7 +606,8 @@ public void onLoadingFailed(String imageUri, View view, FailReason failReason) { } if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { - imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 7783d8a98dd..dbc61961e8f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -44,6 +44,7 @@ import org.schabi.newpipe.subscription.SubscriptionService; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; @@ -419,8 +420,10 @@ public void handleResult(@NonNull ChannelInfo result) { super.handleResult(result); headerRootLayout.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, DISPLAY_BANNER_OPTIONS); - imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, + ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); + imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); if (result.getSubscriberCount() != -1) { headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 9033560bdb8..3bcf9d32236 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -37,6 +37,7 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -271,7 +272,8 @@ public void handleResult(@NonNull final PlaylistInfo result) { playlistCtrl.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.getStreamCount(), (int) result.getStreamCount())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java index 4794def97e0..5dc6c17a40b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -1,12 +1,10 @@ package org.schabi.newpipe.fragments.local; import android.content.Context; -import android.graphics.Bitmap; import android.widget.ImageView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.util.OnClickGesture; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java index e4087d8a815..2dffdbfdb92 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java @@ -1,14 +1,8 @@ package org.schabi.newpipe.fragments.local.holder; -import android.graphics.Bitmap; -import android.support.annotation.DimenRes; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; -import android.widget.ImageView; - -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.fragments.local.LocalItemBuilder; @@ -45,19 +39,4 @@ public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup par } public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); - - /*////////////////////////////////////////////////////////////////////////// - // ImageLoaderOptions - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Base display options - */ - public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .cacheOnDisk(true) - .bitmapConfig(Bitmap.Config.RGB_565) - .resetViewBeforeLoading(false) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java index 1fbea6cc4bc..d9eb7caa538 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -2,15 +2,11 @@ import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import java.text.DateFormat; @@ -29,7 +25,8 @@ public void updateFromItem(final LocalItem localItem, final DateFormat dateForma itemStreamCountView.setText(String.valueOf(item.streamCount)); itemUploaderView.setVisibility(View.INVISIBLE); - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); super.updateFromItem(localItem, dateFormat); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 0696f5f6168..5f9555d9f31 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.fragments.local.holder; -import android.graphics.Bitmap; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; @@ -8,14 +7,12 @@ import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; - import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; @@ -61,7 +58,8 @@ public void updateFromItem(final LocalItem localItem, final DateFormat dateForma } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { @@ -92,15 +90,4 @@ private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) return false; }; } - - /** - * Display options for stream thumbnails - */ - private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java index cd0630b376a..19915867293 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -6,13 +6,12 @@ import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; @@ -84,7 +83,8 @@ public void updateFromItem(final LocalItem localItem, final DateFormat dateForma itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { @@ -100,15 +100,4 @@ public void updateFromItem(final LocalItem localItem, final DateFormat dateForma return true; }); } - - /** - * Display options for stream thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java index bab76ddcbd3..57bc2a3cbbf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java @@ -4,8 +4,6 @@ import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.fragments.local.LocalItemBuilder; @@ -48,15 +46,4 @@ public void updateFromItem(final LocalItem localItem, final DateFormat dateForma return true; }); } - - /** - * Display options for playlist thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) - .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) - .showImageOnFail(R.drawable.dummy_thumbnail_playlist) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java index 0f7b00e6dd8..8711384642b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java @@ -6,6 +6,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; @@ -26,7 +27,7 @@ public void updateFromItem(final LocalItem localItem, final DateFormat dateForma NewPipe.getNameOfService(item.getServiceId()))); itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, - DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); super.updateFromItem(localItem, dateFormat); } diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java index 4830ed33bb5..4fe2b701da6 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java @@ -20,6 +20,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; @@ -147,7 +148,7 @@ void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) holder.uploader.setText(entry.uploader); holder.duration.setText(Localization.getDurationString(entry.duration)); ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, - StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index 211fa60cd48..643886da893 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -1,15 +1,13 @@ package org.schabi.newpipe.info_list.holder; -import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import de.hdodenhof.circleimageview.CircleImageView; @@ -42,7 +40,7 @@ public void updateFromItem(final InfoItem infoItem) { itemBuilder.getImageLoader() .displayImage(item.getThumbnailUrl(), itemThumbnailView, - ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnChannelSelectedListener() != null) { @@ -59,15 +57,4 @@ protected String getDetailLine(final ChannelInfoItem item) { } return details; } - - /** - * Display options for channel thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.buddy_channel_item) - .showImageForEmptyUri(R.drawable.buddy_channel_item) - .showImageOnFail(R.drawable.buddy_channel_item) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java index fb5aa2b7cde..ebb5b4114b9 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -4,8 +4,6 @@ import android.view.LayoutInflater; import android.view.ViewGroup; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -38,16 +36,4 @@ public InfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup p } public abstract void updateFromItem(final InfoItem infoItem); - - /*////////////////////////////////////////////////////////////////////////// - // ImageLoaderOptions - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Base display options - */ - public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index 30d84e1bd17..b6bd2f38940 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -4,12 +4,11 @@ import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public final ImageView itemThumbnailView; @@ -40,7 +39,8 @@ public void updateFromItem(final InfoItem infoItem) { itemUploaderView.setText(item.getUploaderName()); itemBuilder.getImageLoader() - .displayImage(item.getThumbnailUrl(), itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + .displayImage(item.getThumbnailUrl(), itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnPlaylistSelectedListener() != null) { @@ -56,15 +56,4 @@ public void updateFromItem(final InfoItem infoItem) { return true; }); } - - /** - * Display options for playlist thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) - .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) - .showImageOnFail(R.drawable.dummy_thumbnail_playlist) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 72c2830e14b..048b907af5b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -6,13 +6,12 @@ import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; public class StreamMiniInfoItemHolder extends InfoItemHolder { @@ -61,7 +60,7 @@ public void updateFromItem(final InfoItem infoItem) { itemBuilder.getImageLoader() .displayImage(item.getThumbnailUrl(), itemThumbnailView, - StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnStreamSelectedListener() != null) { @@ -98,15 +97,4 @@ private void disableLongClick() { itemView.setLongClickable(false); itemView.setOnLongClickListener(null); } - - /** - * Display options for stream thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); } 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 61720c6b409..83ed54cf589 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -121,7 +121,7 @@ public void onCreate() { shouldUpdateOnProgress = true; mReceiverComponent = new ComponentName(this, MediaButtonReceiver.class); - basePlayerImpl.audioReactor.registerMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.getAudioReactor().registerMediaButtonEventReceiver(mReceiverComponent); } @Override @@ -152,7 +152,7 @@ private void onClose() { lockManager.releaseWifiAndCpu(); } if (basePlayerImpl != null) { - basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.getAudioReactor().unregisterMediaButtonEventReceiver(mReceiverComponent); basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } @@ -575,38 +575,46 @@ public void onCompleted() { } public static class MediaButtonReceiver extends BroadcastReceiver { - - public MediaButtonReceiver() { - super(); - } - @Override public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { - KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (event.getAction() == KeyEvent.ACTION_UP) { - int keycode = event.getKeyCode(); - PendingIntent pendingIntent = null; - if (keycode == KeyEvent.KEYCODE_MEDIA_NEXT) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PAUSE || keycode == KeyEvent.KEYCODE_MEDIA_PLAY) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_MEDIA_REWIND) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); - } - if (pendingIntent != null) { - try { - pendingIntent.send(); - } catch (Exception e) { - Log.e(TAG, "Error Sending intent MediaButtonReceiver", e); - } - } - - } + if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) return; + final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (event.getAction() != KeyEvent.ACTION_UP) return; + final int keycode = event.getKeyCode(); + + final PendingIntent pendingIntent; + switch (keycode) { + case KeyEvent.KEYCODE_MEDIA_NEXT: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); + break; + default: + pendingIntent = null; + } + + if (pendingIntent == null) return; + try { + pendingIntent.send(); + } catch (Exception e) { + Log.e(TAG, "Error Sending intent MediaButtonReceiver", e); } } } 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 5355e19eee1..5ec61b05886 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -838,9 +838,9 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, "queue index=[" + playQueue.getIndex() + "]"); // Check if bad seek position - } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex > currentPlaylistSize) || - currentPlaylistIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to " + + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) || + currentPlayQueueIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to invalid " + "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); @@ -848,9 +848,9 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + - " window=[" + currentPlayQueueIndex + "]," + + " index=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + - " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); + " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index df30c3e79d9..c1896599f9c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -22,6 +22,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au private static final String TAG = "AudioFocusReactor"; + private static final boolean SHOULD_BUILD_FOCUS_REQUEST = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + private static final boolean CAN_USE_MEDIA_BUTTONS = + Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1; + private static final String MEDIA_BUTTON_DEPRECATED_ERROR = + "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."; + private static final int DUCK_DURATION = 1500; private static final float DUCK_AUDIO_TO = .2f; @@ -38,9 +46,9 @@ public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlay this.player = player; this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - player.setAudioDebugListener(this); + player.addAudioDebugListener(this); - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) .setAcceptsDelayedFocusGain(true) .setWillPauseWhenDucked(true) @@ -56,7 +64,7 @@ public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlay //////////////////////////////////////////////////////////////////////////*/ public void requestAudioFocus() { - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { audioManager.requestAudioFocus(request); } else { audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); @@ -64,7 +72,7 @@ public void requestAudioFocus() { } public void abandonAudioFocus() { - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { audioManager.abandonAudioFocusRequest(request); } else { audioManager.abandonAudioFocus(this); @@ -83,24 +91,20 @@ public void setVolume(final int volume) { audioManager.setStreamVolume(STREAM_TYPE, volume, 0); } - private boolean shouldBuildFocusRequest() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - } - public void registerMediaButtonEventReceiver(ComponentName componentName) { - if (android.os.Build.VERSION.SDK_INT > 27) { - Log.e(TAG, "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); - return; + if (CAN_USE_MEDIA_BUTTONS) { + audioManager.registerMediaButtonEventReceiver(componentName); + } else { + Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); } - audioManager.registerMediaButtonEventReceiver(componentName); } public void unregisterMediaButtonEventReceiver(ComponentName componentName) { - if (android.os.Build.VERSION.SDK_INT > 27) { - Log.e(TAG, "unregisterMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); - return; + if (CAN_USE_MEDIA_BUTTONS) { + audioManager.unregisterMediaButtonEventReceiver(componentName); + } else { + Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); } - audioManager.unregisterMediaButtonEventReceiver(componentName); } /*////////////////////////////////////////////////////////////////////////// @@ -165,12 +169,8 @@ public void onAnimationEnd(Animator animation) { player.setVolume(to); } }); - valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - player.setVolume(((float) animation.getAnimatedValue())); - } - }); + valueAnimator.addUpdateListener(animation -> + player.setVolume(((float) animation.getAnimatedValue()))); valueAnimator.start(); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java index 73cdf11135f..7042bea89ad 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java @@ -1,28 +1,22 @@ package org.schabi.newpipe.playlist; import android.content.Context; -import android.graphics.Bitmap; import android.text.TextUtils; import android.view.MotionEvent; import android.view.View; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; -import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; - public class PlayQueueItemBuilder { private static final String TAG = PlayQueueItemBuilder.class.toString(); - private final int thumbnailWidthPx; - private final int thumbnailHeightPx; - private final DisplayImageOptions imageOptions; - public interface OnSelectedListener { void selected(PlayQueueItem item, View view); void held(PlayQueueItem item, View view); @@ -31,11 +25,7 @@ public interface OnSelectedListener { private OnSelectedListener onItemClickListener; - public PlayQueueItemBuilder(final Context context) { - thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width); - thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height); - imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx); - } + public PlayQueueItemBuilder(final Context context) {} public void setOnSelectedListener(OnSelectedListener listener) { this.onItemClickListener = listener; @@ -43,7 +33,8 @@ public void setOnSelectedListener(OnSelectedListener listener) { public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) { if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); - if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader()); + holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(), + NewPipe.getNameOfService(item.getServiceId()))); if (item.getDuration() > 0) { holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); @@ -51,7 +42,8 @@ public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueu holder.itemDurationView.setVisibility(View.GONE); } - ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); + ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); holder.itemRoot.setOnClickListener(view -> { if (onItemClickListener != null) { @@ -81,23 +73,4 @@ private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder return false; }; } - - private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) { - final BitmapProcessor bitmapProcessor = bitmap -> { - final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); - bitmap.recycle(); - return resizedBitmap; - }; - - return new DisplayImageOptions.Builder() - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways - .preProcessor(bitmapProcessor) - .imageScaleType(ImageScaleType.EXACTLY) - .cacheInMemory(true) - .cacheOnDisk(true) - .build(); - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 26278ac75b4..f0ab3bc0312 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -6,12 +6,14 @@ import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.util.Log; import android.widget.Toast; import com.nononsenseapps.filepicker.Utils; +import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -47,6 +49,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private File newpipe_db; private File newpipe_db_journal; + private String thumbnailLoadToggleKey; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference.getKey().equals(thumbnailLoadToggleKey)) { + final ImageLoader imageLoader = ImageLoader.getInstance(); + imageLoader.stop(); + imageLoader.clearDiskCache(); + imageLoader.clearMemoryCache(); + imageLoader.resume(); + Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice, + Toast.LENGTH_SHORT).show(); + } + + return super.onPreferenceTreeClick(preference); + } + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java new file mode 100644 index 00000000000..9ee8a109583 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java @@ -0,0 +1,58 @@ +package org.schabi.newpipe.util; + +import android.graphics.Bitmap; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; + +import org.schabi.newpipe.R; + +public class ImageDisplayConstants { + private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250; + + /** + * Base display options + */ + private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .resetViewBeforeLoading(true) + .bitmapConfig(Bitmap.Config.RGB_565) + .imageScaleType(ImageScaleType.EXACTLY) + .displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS)) + .build(); + + /*////////////////////////////////////////////////////////////////////////// + // DisplayImageOptions default configurations + //////////////////////////////////////////////////////////////////////////*/ + + public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.buddy) + .showImageOnFail(R.drawable.buddy) + .build(); + + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnFail(R.drawable.dummy_thumbnail) + .build(); + + public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.channel_banner) + .showImageOnFail(R.drawable.channel_banner) + .build(); + + public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/res/layout/list_playlist_item.xml b/app/src/main/res/layout/list_playlist_item.xml index 23f5224c50f..57a3cbef9da 100644 --- a/app/src/main/res/layout/list_playlist_item.xml +++ b/app/src/main/res/layout/list_playlist_item.xml @@ -19,7 +19,7 @@ android:layout_alignParentTop="true" android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:contentDescription="@string/list_thumbnail_view_description" - android:scaleType="fitEnd" + android:scaleType="centerCrop" android:src="@drawable/dummy_thumbnail_playlist" tools:ignore="RtlHardcoded"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1a3538075d..cd280ff0266 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,7 +75,8 @@ Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision Load thumbnails - Disable to stop all non-cached thumbnail from loading and save on data and memory usage + Disable to stop all thumbnails from loading and save on data and memory usage. Changing this will clear both in-memory and on-disk image cache. + Image cache wiped Wipe cached metadata Remove all cached webpage data Metadata cache wiped From 5a05cb96beeb73c7586af1f2c9c249b6213c5a18 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 20:07:38 -0700 Subject: [PATCH 05/12] -Changed start position seek to occur after media source window has been prepared. -Fixed livestream not seeking to live when started from play queue. -Fixed media source manager synchronization to only occur after timeline change has completed. -Fixed auto queue not working when last item is replayed after the auto-queued item is removed. -Updated ExoPlayer to 2.7.1. --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 137 +++++++++--------- .../newpipe/player/MainVideoPlayer.java | 1 + .../newpipe/player/PopupVideoPlayer.java | 2 +- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 10 +- .../player/playback/MediaSourceManager.java | 15 +- 7 files changed, 91 insertions(+), 78 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3529a37b174..952bc3067fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:4.2.1' - implementation 'com.google.android.exoplayer:exoplayer:2.7.0' + implementation 'com.google.android.exoplayer:exoplayer:2.7.1' debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' 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 5ec61b05886..de85a3704eb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -149,7 +149,8 @@ public abstract class BasePlayer implements protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; - protected boolean isPrepared = false; + private boolean isPrepared = false; + private boolean isSynchronizing = false; protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; @@ -402,6 +403,7 @@ public void unregisterBroadcastReceiver() { // States Implementation //////////////////////////////////////////////////////////////////////////*/ + public static final int STATE_PREFLIGHT = -1; public static final int STATE_BLOCKED = 123; public static final int STATE_PLAYING = 124; public static final int STATE_BUFFERING = 125; @@ -409,7 +411,7 @@ public void unregisterBroadcastReceiver() { public static final int STATE_PAUSED_SEEK = 127; public static final int STATE_COMPLETED = 128; - protected int currentState = -1; + protected int currentState = STATE_PREFLIGHT; public void changeState(int state) { if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); @@ -540,11 +542,13 @@ public void onTimelineChanged(Timeline timeline, Object manifest, case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes - // ensures MediaSourceManager#update is complete + // Ensures MediaSourceManager#update is complete final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); // Ensure dynamic/livestream timeline changes does not cause negative position - if (isPlaylistStable && !isCurrentWindowValid()) { - simpleExoPlayer.seekTo(/*clampToMillis=*/0); + if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { + if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + + "clamping position to default time."); + seekTo(/*clampToTime=*/0); } break; } @@ -596,49 +600,55 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { } break; case Player.STATE_READY: //3 - maybeRecover(); + maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); break; } - if (currentState == STATE_PAUSED_SEEK) break; changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); break; case Player.STATE_ENDED: // 4 - // Ensure the current window has actually ended - // since single windows that are still loading may produce an ended state - if (isCurrentWindowValid() && - simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { - changeState(STATE_COMPLETED); - isPrepared = false; - } + changeState(STATE_COMPLETED); + isPrepared = false; break; } } - private void maybeRecover() { + private void maybeCorrectSeekPosition() { + if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; + final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); + if (currentSourceItem == null) return; - // Check if already playing correct window - final boolean isCurrentPeriodCorrect = + final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); + final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; + final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - // Check if recovering - if (isCurrentPeriodCorrect && currentSourceItem != null) { - /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, - * rounding this position to the nearest second will help alleviate this.*/ - final long position = currentSourceItem.getRecoveryPosition(); + if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { + // Is recovering previous playback? + if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" + + "[" + getTimeString((int)recoveryPositionMillis) + "]"); + seekTo(recoveryPositionMillis); + playQueue.unsetRecovery(currentSourceIndex); + isSynchronizing = false; - /* Skip recovering if the recovery position is not set.*/ - if (position == PlayQueueItem.RECOVERY_UNSET) return; + } else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) { + if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); + // Is still synchronizing? + seekToDefault(); - if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + - " at: " + getTimeString((int)position)); - simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); - playQueue.unsetRecovery(currentSourceIndex); + } else if (isSynchronizing && presetStartPositionMillis != 0L) { + if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + + "position=[" + presetStartPositionMillis + "]"); + // Has another start position? + seekTo(presetStartPositionMillis); + currentInfo.setStartPosition(0); } + + isSynchronizing = false; } /** @@ -810,11 +820,26 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + (info != null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + if (simpleExoPlayer == null || playQueue == null) return; + final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; final boolean hasStreamInfoChanged = currentInfo != info; + + final int currentPlayQueueIndex = playQueue.indexOf(item); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); + + // when starting playback on the last item when not repeating, maybe auto queue + if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && + getRepeatMode() == Player.REPEAT_MODE_OFF && + PlayerHelper.isAutoQueueEnabled(context)) { + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + } + // If nothing to synchronize if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { - return; // Nothing to synchronize + return; } currentItem = item; @@ -824,13 +849,8 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, registerView(); initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); } - - final int currentPlayQueueIndex = playQueue.indexOf(item); onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); - if (simpleExoPlayer == null) return; - final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); - final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + @@ -844,22 +864,16 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - // If not playing correct stream, change window position - } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { - final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; + // If not playing correct stream, change window position and sets flag + // for synchronizing once window position is corrected + // @see maybeCorrectSeekPosition() + } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || + !isPlaying()) { if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + " index=[" + currentPlayQueueIndex + "]," + - " at=[" + getTimeString((int)startPos) + "]," + " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); - simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); - } - - // when starting playback on the last item when not repeating, maybe auto queue - if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && - getRepeatMode() == Player.REPEAT_MODE_OFF && - PlayerHelper.isAutoQueueEnabled(context)) { - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); - if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + isSynchronizing = true; + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); } } @@ -927,9 +941,6 @@ public void onPrepared(boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (playWhenReady) audioReactor.requestAudioFocus(); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - - // On live prepared - if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault(); } public void onVideoPlayPause() { @@ -1001,16 +1012,16 @@ public void onSelected(final PlayQueueItem item) { playQueue.setIndex(index); } - 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))) { - return; - } + public void seekTo(long positionMillis) { + if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); + if (simpleExoPlayer == null || positionMillis < 0 || + positionMillis > simpleExoPlayer.getDuration()) return; + simpleExoPlayer.seekTo(positionMillis); + } - int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); - if (progress < 0) progress = 0; - simpleExoPlayer.seekTo(progress); + public void seekBy(long offsetMillis) { + if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); } public boolean isCurrentWindowValid() { @@ -1094,10 +1105,6 @@ public String getUploaderName() { return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); } - public boolean isCompleted() { - return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED; - } - public boolean isPlaying() { final int state = simpleExoPlayer.getPlaybackState(); return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) @@ -1148,8 +1155,8 @@ public PlayQueueAdapter getPlayQueueAdapter() { return playQueueAdapter; } - public boolean isPlayerReady() { - return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED; + public boolean isPrepared() { + return isPrepared; } public boolean isProgressLoopRunning() { 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 dd7e0c71e8d..90a4a8c9ffb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -640,6 +640,7 @@ public void onStopTrackingTouch(SeekBar seekBar) { public void onDismiss(PopupMenu menu) { super.onDismiss(menu); if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUi(); } @Override 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 123fbfee3b2..64dc03da684 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -716,7 +716,7 @@ private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureL public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false; + if (playerImpl == null || !playerImpl.isPlaying()) return false; if (e.getX() > popupWidth / 2) { playerImpl.onFastForward(); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1c3ffe9118f..50248891b5c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -509,7 +509,7 @@ public void onStartTrackingTouch(SeekBar seekBar) { @Override public void onStopTrackingTouch(SeekBar seekBar) { - if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress()); + if (player != null) player.seekTo(seekBar.getProgress()); seekDisplay.setVisibility(View.GONE); seeking = false; } 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 48b13654cab..aa896bb69f1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -425,7 +425,7 @@ public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { // Create subtitle sources for (final Subtitles subtitle : info.getSubtitles()) { final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null || context == null) continue; + if (mimeType == null) continue; final Format textFormat = Format.createTextSampleFormat(null, mimeType, SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); @@ -599,7 +599,7 @@ public void destroy() { @Override public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { - if (!isPrepared) return; + if (!isPrepared()) return; if (duration != playbackSeekBar.getMax()) { playbackEndTime.setText(getTimeString(duration)); @@ -624,8 +624,6 @@ public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { } protected void onFullScreenButtonClicked() { - if (!isPlayerReady()) return; - changeState(STATE_BLOCKED); } @@ -735,7 +733,7 @@ private void onCaptionClicked() { } private void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { + if (getAspectRatioFrameLayout() != null) { final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); final int newResizeMode = nextResizeMode(currentResizeMode); getAspectRatioFrameLayout().setResizeMode(newResizeMode); @@ -772,7 +770,7 @@ public void onStartTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) { if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); - simpleExoPlayer.seekTo(seekBar.getProgress()); + seekTo(seekBar.getProgress()); if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 50c069b40f6..170668169d8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -307,7 +307,7 @@ private void maybeSync() { if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked.get() || currentItem == null) return; + if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return; final Consumer onSuccess = info -> syncInternal(currentItem, info); final Consumer onError = throwable -> syncInternal(currentItem, null); @@ -400,8 +400,6 @@ private void maybeLoadItem(@NonNull final PlayQueueItem item) { /* No exception handling since getLoadedMediaSource guarantees nonnull return */ .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); - } else { - maybeSynchronizePlayer(); } } @@ -467,6 +465,12 @@ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { * Checks if the current playing index contains an expired {@link ManagedMediaSource}. * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and * {@link #loadImmediate()} is called to reload the current item. + *

+ * If not, then the media source at the current index is ready for playback, and + * {@link #maybeSynchronizePlayer()} is called. + *

+ * Under both cases, {@link #maybeSync()} will be called to ensure the listener + * is up-to-date. * */ private void maybeRenewCurrentIndex() { final int currentIndex = playQueue.getIndex(); @@ -475,7 +479,10 @@ private void maybeRenewCurrentIndex() { final ManagedMediaSource currentSource = (ManagedMediaSource) sources.getMediaSource(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(); - if (!currentSource.canReplace(currentItem)) return; + if (!currentSource.canReplace(currentItem)) { + maybeSynchronizePlayer(); + return; + } if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); From bc7188c8a8c8036ed67a93aa2572f5de676721ce Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 23:42:46 -0700 Subject: [PATCH 06/12] -Added media session implementation for all players. -Extracted version numbers in gradle dependencies. -Updated ExoPlayer to 2.7.1. -Updated RxJava to 2.1.10, RxAndroid to 2.0.2 and RxBinding to 2.1.1. -Removed deprecated implementation of media buttons. --- app/build.gradle | 36 +++--- app/src/main/AndroidManifest.xml | 6 - .../newpipe/player/BackgroundPlayer.java | 54 --------- .../org/schabi/newpipe/player/BasePlayer.java | 12 +- .../newpipe/player/helper/AudioReactor.java | 33 ++---- .../player/helper/MediaSessionManager.java | 38 ++++++ .../mediasession/DummyPlaybackPreparer.java | 45 +++++++ .../mediasession/MediaSessionCallback.java | 17 +++ .../mediasession/PlayQueueNavigator.java | 111 ++++++++++++++++++ .../PlayQueuePlaybackController.java | 31 +++++ .../playback/BasePlayerMediaSession.java | 77 ++++++++++++ 11 files changed, 358 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java diff --git a/app/build.gradle b/app/build.gradle index 952bc3067fa..9b2569a66aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,11 @@ android { ext { supportLibVersion = '27.1.0' + exoPlayerLibVersion = '2.7.1' + roomDbLibVersion = '1.0.0' + leakCanaryVersion = '1.5.4' + okHttpVersion = '1.5.0' + icepickVersion = '3.2.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -73,27 +78,28 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:4.2.1' - implementation 'com.google.android.exoplayer:exoplayer:2.7.1' + implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" + implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" - debugImplementation 'com.facebook.stetho:stetho:1.5.0' - debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' + debugImplementation "com.facebook.stetho:stetho:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion" debugImplementation 'com.android.support:multidex:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.1.7' - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + implementation 'io.reactivex.rxjava2:rxjava:2.1.10' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' - implementation 'android.arch.persistence.room:runtime:1.0.0' - implementation 'android.arch.persistence.room:rxjava2:1.0.0' - annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' + implementation "android.arch.persistence.room:runtime:$roomDbLibVersion" + implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion" + annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion" - implementation 'frankiesardo:icepick:3.2.0' - annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + implementation "frankiesardo:icepick:$icepickVersion" + annotationProcessor "frankiesardo:icepick-processor:$icepickVersion" - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' - betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" + betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0' + debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 18b3222a068..1be8c1f2ce1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,12 +43,6 @@ android:launchMode="singleTask" android:label="@string/title_activity_background_player"/> - - - - - - = Build.VERSION_CODES.O; - private static final boolean CAN_USE_MEDIA_BUTTONS = - Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1; - private static final String MEDIA_BUTTON_DEPRECATED_ERROR = - "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."; - private static final int DUCK_DURATION = 1500; private static final float DUCK_AUDIO_TO = .2f; @@ -42,7 +37,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au private final AudioFocusRequest request; - public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { + public AudioReactor(@NonNull final Context context, + @NonNull final SimpleExoPlayer player) { this.player = player; this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -59,6 +55,11 @@ public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlay } } + public void dispose() { + abandonAudioFocus(); + player.removeAudioDebugListener(this); + } + /*////////////////////////////////////////////////////////////////////////// // Audio Manager //////////////////////////////////////////////////////////////////////////*/ @@ -91,22 +92,6 @@ public void setVolume(final int volume) { audioManager.setStreamVolume(STREAM_TYPE, volume, 0); } - public void registerMediaButtonEventReceiver(ComponentName componentName) { - if (CAN_USE_MEDIA_BUTTONS) { - audioManager.registerMediaButtonEventReceiver(componentName); - } else { - Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); - } - } - - public void unregisterMediaButtonEventReceiver(ComponentName componentName) { - if (CAN_USE_MEDIA_BUTTONS) { - audioManager.unregisterMediaButtonEventReceiver(componentName); - } else { - Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); - } - } - /*////////////////////////////////////////////////////////////////////////// // AudioFocus //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java new file mode 100644 index 00000000000..8405e45fd5c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.player.helper; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.media.session.MediaSessionCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer; +import org.schabi.newpipe.player.mediasession.MediaSessionCallback; +import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; +import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; + +public class MediaSessionManager { + private static final String TAG = "MediaSessionManager"; + + private final MediaSessionCompat mediaSession; + private final MediaSessionConnector sessionConnector; + + public MediaSessionManager(@NonNull final Context context, + @NonNull final Player player, + @NonNull final MediaSessionCallback callback) { + this.mediaSession = new MediaSessionCompat(context, TAG); + this.sessionConnector = new MediaSessionConnector(mediaSession, + new PlayQueuePlaybackController(callback)); + this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); + this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer()); + } + + public MediaSessionCompat getMediaSession() { + return mediaSession; + } + + public MediaSessionConnector getSessionConnector() { + return sessionConnector; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java new file mode 100644 index 00000000000..431a90d8a40 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.player.mediasession; + +import android.net.Uri; +import android.os.Bundle; +import android.os.ResultReceiver; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer { + @Override + public long getSupportedPrepareActions() { + return 0; + } + + @Override + public void onPrepare() { + + } + + @Override + public void onPrepareFromMediaId(String mediaId, Bundle extras) { + + } + + @Override + public void onPrepareFromSearch(String query, Bundle extras) { + + } + + @Override + public void onPrepareFromUri(Uri uri, Bundle extras) { + + } + + @Override + public String[] getCommands() { + return new String[0]; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java new file mode 100644 index 00000000000..a1a57a87d11 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.player.mediasession; + +import android.support.v4.media.MediaDescriptionCompat; + +public interface MediaSessionCallback { + void onSkipToPrevious(); + void onSkipToNext(); + void onSkipToIndex(final int index); + + int getCurrentPlayingIndex(); + int getQueueSize(); + MediaDescriptionCompat getQueueMetadata(final int index); + + void onPlay(); + void onPause(); + void onSetShuffle(final boolean isShuffled); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java new file mode 100644 index 00000000000..429c26fd9d8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -0,0 +1,111 @@ +package org.schabi.newpipe.player.mediasession; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.session.MediaSessionCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import com.google.android.exoplayer2.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + + +public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { + public static final int DEFAULT_MAX_QUEUE_SIZE = 10; + + private final MediaSessionCompat mediaSession; + private final MediaSessionCallback callback; + private final int maxQueueSize; + + private long activeQueueItemId; + + public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, + @NonNull final MediaSessionCallback callback) { + this.mediaSession = mediaSession; + this.callback = callback; + this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + + this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + } + + @Override + public long getSupportedQueueNavigatorActions(@Nullable Player player) { + return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; + } + + @Override + public void onTimelineChanged(Player player) { + publishFloatingQueueWindow(); + } + + @Override + public void onCurrentWindowIndexChanged(Player player) { + if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID + || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { + publishFloatingQueueWindow(); + } else if (!player.getCurrentTimeline().isEmpty()) { + activeQueueItemId = player.getCurrentWindowIndex(); + } + } + + @Override + public long getActiveQueueItemId(@Nullable Player player) { + return callback.getCurrentPlayingIndex(); + } + + @Override + public void onSkipToPrevious(Player player) { + callback.onSkipToPrevious(); + } + + @Override + public void onSkipToQueueItem(Player player, long id) { + callback.onSkipToIndex((int) id); + } + + @Override + public void onSkipToNext(Player player) { + callback.onSkipToNext(); + } + + private void publishFloatingQueueWindow() { + if (callback.getQueueSize() == 0) { + mediaSession.setQueue(Collections.emptyList()); + activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + return; + } + + // Yes this is almost a copypasta, got a problem with that? =\ + int windowCount = callback.getQueueSize(); + int currentWindowIndex = callback.getCurrentPlayingIndex(); + int queueSize = Math.min(maxQueueSize, windowCount); + int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, + windowCount - queueSize); + + List queue = new ArrayList<>(); + for (int i = startIndex; i < startIndex + queueSize; i++) { + queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); + } + mediaSession.setQueue(queue); + activeQueueItemId = currentWindowIndex; + } + + @Override + public String[] getCommands() { + return new String[0]; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java new file mode 100644 index 00000000000..2aa41bd631d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.player.mediasession; + +import android.support.v4.media.session.PlaybackStateCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController; + +public class PlayQueuePlaybackController extends DefaultPlaybackController { + private final MediaSessionCallback callback; + + public PlayQueuePlaybackController(final MediaSessionCallback callback) { + super(); + this.callback = callback; + } + + @Override + public void onPlay(Player player) { + callback.onPlay(); + } + + @Override + public void onPause(Player player) { + callback.onPause(); + } + + @Override + public void onSetShuffleMode(Player player, int shuffleMode) { + callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL + || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java new file mode 100644 index 00000000000..07504542c15 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -0,0 +1,77 @@ +package org.schabi.newpipe.player.playback; + +import android.net.Uri; +import android.support.v4.media.MediaDescriptionCompat; + +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.mediasession.MediaSessionCallback; +import org.schabi.newpipe.playlist.PlayQueueItem; + +public class BasePlayerMediaSession implements MediaSessionCallback { + private BasePlayer player; + + public BasePlayerMediaSession(final BasePlayer player) { + this.player = player; + } + + @Override + public void onSkipToPrevious() { + player.onPlayPrevious(); + } + + @Override + public void onSkipToNext() { + player.onPlayNext(); + } + + @Override + public void onSkipToIndex(int index) { + if (player.getPlayQueue() == null) return; + player.onSelected(player.getPlayQueue().getItem(index)); + } + + @Override + public int getCurrentPlayingIndex() { + if (player.getPlayQueue() == null) return -1; + return player.getPlayQueue().getIndex(); + } + + @Override + public int getQueueSize() { + if (player.getPlayQueue() == null) return -1; + return player.getPlayQueue().size(); + } + + @Override + public MediaDescriptionCompat getQueueMetadata(int index) { + if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { + return null; + } + + final PlayQueueItem item = player.getPlayQueue().getItem(index); + MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() + .setMediaId(String.valueOf(index)) + .setTitle(item.getTitle()) + .setSubtitle(item.getUploader()); + + final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); + if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri); + + return descriptionBuilder.build(); + } + + @Override + public void onPlay() { + if (!player.isPlaying()) player.onVideoPlayPause(); + } + + @Override + public void onPause() { + if (player.isPlaying()) player.onVideoPlayPause(); + } + + @Override + public void onSetShuffle(boolean isShuffled) { + player.onShuffleModeEnabledChanged(isShuffled); + } +} From 5167fe078bb066677644393bc52e6117d1e83c89 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 17 Mar 2018 16:04:02 -0700 Subject: [PATCH 07/12] -Refactored synchronization checks out from MediaSourceManager to ManagedMediaSource. -Fixed null input causing potential NPE on PlayQueueItem. --- .../player/mediasource/FailedMediaSource.java | 8 ++++- .../player/mediasource/LoadedMediaSource.java | 10 ++++-- .../mediasource/ManagedMediaSource.java | 18 ++++++++++- .../mediasource/PlaceholderMediaSource.java | 8 ++++- .../player/playback/MediaSourceManager.java | 21 ++++--------- .../newpipe/playlist/PlayQueueItem.java | 31 ++++++++++--------- 6 files changed, 61 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index d07baf2a7cd..5f029cc5060 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -72,7 +72,13 @@ public void releasePeriod(MediaPeriod mediaPeriod) {} public void releaseSource() {} @Override - public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, + final boolean isInterruptable) { return newIdentity != playQueueItem || canRetry(); } + + @Override + public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + return playQueueItem == stream; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index f523667f9cc..fe7508ecc01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -59,7 +59,13 @@ public void releaseSource() { } @Override - public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { - return newIdentity != stream || isExpired(); + public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, + final boolean isInterruptable) { + return newIdentity != stream || (isInterruptable && isExpired()); + } + + @Override + public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + return this.stream == stream; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java index 3bb7ca429c9..46fd149bb90 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -7,5 +7,21 @@ import org.schabi.newpipe.playlist.PlayQueueItem; public interface ManagedMediaSource extends MediaSource { - boolean canReplace(@NonNull final PlayQueueItem newIdentity); + /** + * Determines whether or not this {@link ManagedMediaSource} can be replaced. + * + * @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if + * it is different from the existing stream in the + * {@link ManagedMediaSource}, then it should be replaced. + * @param isInterruptable specifies if this {@link ManagedMediaSource} potentially + * being played. + * */ + boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, + final boolean isInterruptable); + + /** + * Determines if the {@link PlayQueueItem} is the one the + * {@link ManagedMediaSource} encapsulates over. + * */ + boolean isStreamEqual(@NonNull final PlayQueueItem stream); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 0d3436a01c6..2c57f2f9cbe 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -19,7 +19,13 @@ public class PlaceholderMediaSource implements ManagedMediaSource { @Override public void releaseSource() {} @Override - public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, + final boolean isInterruptable) { return true; } + + @Override + public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 170668169d8..47735811301 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -268,15 +268,10 @@ private boolean isPlayQueueReady() { private boolean isPlaybackReady() { if (sources.getSize() != playQueue.size()) return false; - final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); + final ManagedMediaSource mediaSource = + (ManagedMediaSource) sources.getMediaSource(playQueue.getIndex()); final PlayQueueItem playQueueItem = playQueue.getItem(); - - if (mediaSource instanceof LoadedMediaSource) { - return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); - } else if (mediaSource instanceof FailedMediaSource) { - return playQueueItem == ((FailedMediaSource) mediaSource).getStream(); - } - return false; + return mediaSource.isStreamEqual(playQueueItem); } private void maybeBlock() { @@ -453,12 +448,8 @@ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { if (index == -1 || index >= sources.getSize()) return false; final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); - - if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { - return item != ((LoadedMediaSource) mediaSource).getStream(); - } else { - return mediaSource.canReplace(item); - } + return mediaSource.shouldBeReplacedWith(item, + /*mightBeInProgress=*/index != playQueue.getIndex()); } /** @@ -479,7 +470,7 @@ private void maybeRenewCurrentIndex() { final ManagedMediaSource currentSource = (ManagedMediaSource) sources.getMediaSource(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(); - if (!currentSource.canReplace(currentItem)) { + if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) { maybeSynchronizePlayer(); return; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java index 752dc223de7..df4d19720ef 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -11,20 +11,19 @@ import java.io.Serializable; import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; public class PlayQueueItem implements Serializable { - final public static long RECOVERY_UNSET = Long.MIN_VALUE; + public final static long RECOVERY_UNSET = Long.MIN_VALUE; + private final static String EMPTY_STRING = ""; - final private String title; - final private String url; + @NonNull final private String title; + @NonNull final private String url; final private int serviceId; final private long duration; - final private String thumbnailUrl; - final private String uploader; - final private StreamType streamType; + @NonNull final private String thumbnailUrl; + @NonNull final private String uploader; + @NonNull final private StreamType streamType; private long recoveryPosition; private Throwable error; @@ -42,15 +41,16 @@ public class PlayQueueItem implements Serializable { item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); } - private PlayQueueItem(final String name, final String url, final int serviceId, - final long duration, final String thumbnailUrl, final String uploader, - final StreamType streamType) { - this.title = name; - this.url = url; + private PlayQueueItem(@Nullable final String name, @Nullable final String url, + final int serviceId, final long duration, + @Nullable final String thumbnailUrl, @Nullable final String uploader, + @NonNull final StreamType streamType) { + this.title = name != null ? name : EMPTY_STRING; + this.url = url != null ? url : EMPTY_STRING; this.serviceId = serviceId; this.duration = duration; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING; + this.uploader = uploader != null ? uploader : EMPTY_STRING; this.streamType = streamType; this.recoveryPosition = RECOVERY_UNSET; @@ -84,6 +84,7 @@ public String getUploader() { return uploader; } + @NonNull public StreamType getStreamType() { return streamType; } From e885822a3484d5404fe6cce51cb0eb87cadccf4f Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 21 Mar 2018 00:11:54 -0700 Subject: [PATCH 08/12] -Added playback speed control dialog to allow full user control over player tempo and pitch parameters. -Changed tempo and pitch button in service player activity and tempo button in main video player to open speed control dialog. -Changed LIVE button to be no longer clickable when player position is at or beyond default position. -Changed main video player to use AppCompatActivity rather than Activity. -Fixed video player tempo button not updating when player speed parameters change. -Fixed player crashing on lower sdk versions due to no MediaButtonReceiver, added intent back to manifest. -Fixed inconsistent gradle library naming. -Fixed stetho dependencies incorrect version. --- app/build.gradle | 23 +- app/src/main/AndroidManifest.xml | 6 + .../org/schabi/newpipe/player/BasePlayer.java | 21 +- .../newpipe/player/MainVideoPlayer.java | 22 +- .../newpipe/player/ServicePlayerActivity.java | 66 +--- .../schabi/newpipe/player/VideoPlayer.java | 10 +- .../helper/PlaybackParameterDialog.java | 348 ++++++++++++++++++ .../res/layout/dialog_playback_parameter.xml | 313 ++++++++++++++++ app/src/main/res/values/strings.xml | 8 + 9 files changed, 755 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java create mode 100644 app/src/main/res/layout/dialog_playback_parameter.xml diff --git a/app/build.gradle b/app/build.gradle index 9b2569a66aa..5c434c30c5e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,9 +51,10 @@ ext { supportLibVersion = '27.1.0' exoPlayerLibVersion = '2.7.1' roomDbLibVersion = '1.0.0' - leakCanaryVersion = '1.5.4' - okHttpVersion = '1.5.0' - icepickVersion = '3.2.0' + leakCanaryLibVersion = '1.5.4' + okHttpLibVersion = '1.5.0' + icepickLibVersion = '3.2.0' + stethoLibVersion = '1.5.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -81,8 +82,8 @@ dependencies { implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" - debugImplementation "com.facebook.stetho:stetho:$okHttpVersion" - debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion" + debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" debugImplementation 'com.android.support:multidex:1.0.3' implementation 'io.reactivex.rxjava2:rxjava:2.1.10' @@ -93,13 +94,13 @@ dependencies { implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion" annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion" - implementation "frankiesardo:icepick:$icepickVersion" - annotationProcessor "frankiesardo:icepick-processor:$icepickVersion" + implementation "frankiesardo:icepick:$icepickLibVersion" + annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion" - debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" - betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" - releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" + betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1be8c1f2ce1..1edd67d24bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,12 @@ + + + + + + = currentTimeline.getWindowCount()) { + return false; + } + + Timeline.Window timelineWindow = new Timeline.Window(); + currentTimeline.getWindow(currentWindowIndex, timelineWindow); + return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); + } + public boolean isPlaying() { final int state = simpleExoPlayer.getPlaybackState(); return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) 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 90a4a8c9ffb..cbc4b823035 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -19,7 +19,6 @@ package org.schabi.newpipe.player; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -33,6 +32,7 @@ import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.DisplayMetrics; @@ -49,6 +49,7 @@ import android.widget.TextView; import android.widget.Toast; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; @@ -57,6 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -87,7 +89,8 @@ * * @author mauriciocolli */ -public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead { +public final class MainVideoPlayer extends AppCompatActivity + implements StateSaver.WriteRead, PlaybackParameterDialog.Callback { private static final String TAG = ".MainVideoPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; @@ -340,6 +343,15 @@ protected void setShuffleButton(final ImageButton shuffleButton, final boolean s } } + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { + if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); + } + /////////////////////////////////////////////////////////////////////////// @SuppressWarnings({"unused", "WeakerAccess"}) @@ -630,6 +642,12 @@ private void onScreenRotationClicked() { showControlsThenHide(); } + @Override + public void onPlaybackSpeedClicked() { + PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) + .show(getSupportFragmentManager(), TAG); + } + @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 50248891b5c..1f850944d66 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; @@ -43,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; public abstract class ServicePlayerActivity extends AppCompatActivity - implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener { + implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, + View.OnClickListener, PlaybackParameterDialog.Callback { private boolean serviceBound; private ServiceConnection serviceConnection; @@ -57,8 +59,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; - private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61; - private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; @@ -85,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ProgressBar progressBar; private TextView playbackSpeedButton; - private PopupMenu playbackSpeedPopupMenu; private TextView playbackPitchButton; - private PopupMenu playbackPitchPopupMenu; //////////////////////////////////////////////////////////////////////////// // Abstracts @@ -317,45 +315,6 @@ private void buildControls() { shuffleButton.setOnClickListener(this); playbackSpeedButton.setOnClickListener(this); playbackPitchButton.setOnClickListener(this); - - playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton); - playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton); - buildPlaybackSpeedMenu(); - buildPlaybackPitchMenu(); - } - - private void buildPlaybackSpeedMenu() { - if (playbackSpeedPopupMenu == null) return; - - playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID); - for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) { - final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i]; - final String formattedSpeed = formatSpeed(playbackSpeed); - final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed); - item.setOnMenuItemClickListener(menuItem -> { - if (player == null) return false; - - player.setPlaybackSpeed(playbackSpeed); - return true; - }); - } - } - - private void buildPlaybackPitchMenu() { - if (playbackPitchPopupMenu == null) return; - - playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID); - for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) { - final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i]; - final String formattedPitch = formatPitch(playbackPitch); - final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch); - item.setOnMenuItemClickListener(menuItem -> { - if (player == null) return false; - - player.setPlaybackPitch(playbackPitch); - return true; - }); - } } private void buildItemPopupMenu(final PlayQueueItem item, final View view) { @@ -474,10 +433,12 @@ public void onClick(View view) { player.onShuffleClicked(); } else if (view.getId() == playbackSpeedButton.getId()) { - playbackSpeedPopupMenu.show(); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); } else if (view.getId() == playbackPitchButton.getId()) { - playbackPitchPopupMenu.show(); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); } else if (view.getId() == metadata.getId()) { scrollToSelected(); @@ -488,6 +449,15 @@ public void onClick(View view) { } } + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { + if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); + } + //////////////////////////////////////////////////////////////////////////// // Seekbar Listener //////////////////////////////////////////////////////////////////////////// @@ -539,6 +509,10 @@ public void onProgressUpdate(int currentProgress, int duration, int bufferPercen progressSeekBar.setProgress(currentProgress); progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); } + + if (player != null) { + progressLiveSync.setClickable(!player.isLiveEdge()); + } } @Override 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 aa896bb69f1..b019ea91eed 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; @@ -523,6 +524,12 @@ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra onTextTrackUpdate(); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + super.onPlaybackParametersChanged(playbackParameters); + playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed)); + } + @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (DEBUG) { @@ -615,6 +622,7 @@ public void onUpdateProgress(int currentProgress, int duration, int bufferPercen if (DEBUG && bufferPercent % 20 == 0) { //Limit log Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); } + playbackLiveSync.setClickable(!isLiveEdge()); } @Override @@ -718,7 +726,7 @@ public void onQualitySelectorClicked() { wasPlaying = simpleExoPlayer.getPlayWhenReady(); } - private void onPlaybackSpeedClicked() { + public void onPlaybackSpeedClicked() { if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); playbackSpeedPopupMenu.show(); isSomePopupMenuVisible = true; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java new file mode 100644 index 00000000000..8a0a8a86c58 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -0,0 +1,348 @@ +package org.schabi.newpipe.player.helper; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.schabi.newpipe.R; + +import static org.schabi.newpipe.player.BasePlayer.DEBUG; + +public class PlaybackParameterDialog extends DialogFragment { + private static final String TAG = "PlaybackParameterDialog"; + + public static final float MINIMUM_PLAYBACK_VALUE = 0.25f; + public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f; + + public static final String STEP_UP_SIGN = "+"; + public static final String STEP_DOWN_SIGN = "-"; + public static final float PLAYBACK_STEP_VALUE = 0.05f; + + public static final float NIGHTCORE_TEMPO = 1.20f; + public static final float NIGHTCORE_PITCH_LOWER = 1.15f; + public static final float NIGHTCORE_PITCH_UPPER = 1.25f; + + public static final float DEFAULT_TEMPO = 1.00f; + public static final float DEFAULT_PITCH = 1.00f; + + private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; + private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + + public interface Callback { + void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); + } + + private Callback callback; + + private float initialTempo = DEFAULT_TEMPO; + private float initialPitch = DEFAULT_PITCH; + + private SeekBar tempoSlider; + private TextView tempoMinimumText; + private TextView tempoMaximumText; + private TextView tempoCurrentText; + private TextView tempoStepDownText; + private TextView tempoStepUpText; + + private SeekBar pitchSlider; + private TextView pitchMinimumText; + private TextView pitchMaximumText; + private TextView pitchCurrentText; + private TextView pitchStepDownText; + private TextView pitchStepUpText; + + private CheckBox unhookingCheckbox; + + private TextView nightCorePresetText; + private TextView resetPresetText; + + public static PlaybackParameterDialog newInstance(final float playbackTempo, + final float playbackPitch) { + PlaybackParameterDialog dialog = new PlaybackParameterDialog(); + dialog.initialTempo = playbackTempo; + dialog.initialPitch = playbackPitch; + return dialog; + } + + /*////////////////////////////////////////////////////////////////////////// + // Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context != null && context instanceof Callback) { + callback = (Callback) context; + } else { + dismiss(); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); + initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putFloat(INITIAL_TEMPO_KEY, initialTempo); + outState.putFloat(INITIAL_PITCH_KEY, initialPitch); + } + + /*////////////////////////////////////////////////////////////////////////// + // Dialog + //////////////////////////////////////////////////////////////////////////*/ + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); + setupView(view); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.playback_speed_control) + .setView(view) + .setCancelable(true) + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> + setPlaybackParameters(initialTempo, initialPitch)) + .setPositiveButton(R.string.finish, (dialogInterface, i) -> + setPlaybackParameters(getCurrentTempo(), getCurrentPitch())); + + return dialogBuilder.create(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Dialog Builder + //////////////////////////////////////////////////////////////////////////*/ + + private void setupView(@NonNull View rootView) { + setupHookingControl(rootView); + setupTempoControl(rootView); + setupPitchControl(rootView); + setupPresetControl(rootView); + } + + private void setupTempoControl(@NonNull View rootView) { + tempoSlider = rootView.findViewById(R.id.tempoSeekbar); + tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); + tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); + tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText); + tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); + tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); + + tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); + tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); + tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); + + tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + tempoStepUpText.setOnClickListener(view -> + setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE)); + + tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + tempoStepDownText.setOnClickListener(view -> + setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE)); + + tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); + tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo)); + tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); + } + + private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); + if (fromUser) { // this change is first in chain + setTempo(currentTempo); + } else { + setPlaybackParameters(currentTempo, getCurrentPitch()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + }; + } + + private void setupPitchControl(@NonNull View rootView) { + pitchSlider = rootView.findViewById(R.id.pitchSeekbar); + pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); + pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); + pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); + pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); + pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); + + pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); + pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); + pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); + + pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + pitchStepUpText.setOnClickListener(view -> + setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE)); + + pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + pitchStepDownText.setOnClickListener(view -> + setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE)); + + pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); + pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch)); + pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + } + + private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); + if (fromUser) { // this change is first in chain + setPitch(currentPitch); + } else { + setPlaybackParameters(getCurrentTempo(), currentPitch); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + }; + } + + private void setupHookingControl(@NonNull View rootView) { + unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); + unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + if (isChecked) return; + // When unchecked, slide back to the minimum of current tempo or pitch + final float minimum = Math.min(getCurrentPitch(), getCurrentTempo()); + setSliders(minimum); + }); + } + + private void setupPresetControl(@NonNull View rootView) { + nightCorePresetText = rootView.findViewById(R.id.presetNightcore); + nightCorePresetText.setOnClickListener(view -> { + final float randomPitch = NIGHTCORE_PITCH_LOWER + + (float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); + + setTempoSlider(NIGHTCORE_TEMPO); + setPitchSlider(randomPitch); + }); + + resetPresetText = rootView.findViewById(R.id.presetReset); + resetPresetText.setOnClickListener(view -> { + setTempoSlider(DEFAULT_TEMPO); + setPitchSlider(DEFAULT_PITCH); + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + private void setTempo(final float newTempo) { + if (unhookingCheckbox == null) return; + if (!unhookingCheckbox.isChecked()) { + setSliders(newTempo); + } else { + setTempoSlider(newTempo); + } + } + + private void setPitch(final float newPitch) { + if (unhookingCheckbox == null) return; + if (!unhookingCheckbox.isChecked()) { + setSliders(newPitch); + } else { + setPitchSlider(newPitch); + } + } + + private void setSliders(final float newValue) { + setTempoSlider(newValue); + setPitchSlider(newValue); + } + + private void setTempoSlider(final float newTempo) { + if (tempoSlider == null) return; + // seekbar doesn't register progress if it is the same as the existing progress + tempoSlider.setProgress(Integer.MAX_VALUE); + tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo)); + } + + private void setPitchSlider(final float newPitch) { + if (pitchSlider == null) return; + pitchSlider.setProgress(Integer.MAX_VALUE); + pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch)); + } + + private void setPlaybackParameters(final float tempo, final float pitch) { + if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { + if (DEBUG) Log.d(TAG, "Setting playback parameters to " + + "tempo=[" + tempo + "], " + + "pitch=[" + pitch + "]"); + + tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); + pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); + callback.onPlaybackParameterChanged(tempo, pitch); + } + } + + private float getCurrentTempo() { + return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + tempoSlider.getProgress()); + } + + private float getCurrentPitch() { + return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + pitchSlider.getProgress()); + } + + /** + * Converts from zeroed float with a minimum offset to the nearest rounded slider + * equivalent integer + * */ + private static int getSliderEquivalent(final float minimumValue, final float floatValue) { + return Math.round((floatValue - minimumValue) * 100f); + } + + /** + * Converts from slider integer value to an equivalent float value with a given minimum offset + * */ + private static float getSliderEquivalent(final float minimumValue, final int intValue) { + return ((float) intValue) / 100f + minimumValue; + } + + private static String getStepUpPercentString(final float percent) { + return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); + } + + private static String getStepDownPercentString(final float percent) { + return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); + } +} diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml new file mode 100644 index 00000000000..a8c6a5dcd66 --- /dev/null +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd280ff0266..effdeaaba7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,4 +456,12 @@ yourid, soundcloud.com/yourid Keep in mind that this operation can be network expensive.\n\nDo you want to continue? + + + Playback Speed Control + Tempo + Pitch + Unhook (may cause distortion) + Nightcore + Default From 18d019c62ae18f9c42a19d98e9cfe7d0cb43fd4a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 21 Mar 2018 20:08:33 -0700 Subject: [PATCH 09/12] -Added quadratic slider strategy implementation and tests. -Modified playback speed control to use quadratic sliders instead of linear. -Modified number formatters in player helper to use double instead of float. -Simplified slider behavior in playback parameter dialog. -Fixed potential NPE in base local fragment. --- .../local/bookmark/BaseLocalListFragment.java | 5 +- .../helper/PlaybackParameterDialog.java | 335 ++++++++++-------- .../newpipe/player/helper/PlayerHelper.java | 4 +- .../schabi/newpipe/util/SliderStrategy.java | 73 ++++ .../util/QuadraticSliderStrategyTest.java | 86 +++++ 5 files changed, 348 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java create mode 100644 app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java index d2c4e1b1471..eb366d97f81 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java @@ -151,7 +151,10 @@ public void showEmptyState() { @Override public void showListFooter(final boolean show) { - itemsList.post(() -> itemListAdapter.showFooter(show)); + if (itemsList == null) return; + itemsList.post(() -> { + if (itemListAdapter != null) itemListAdapter.showFooter(show); + }); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 8a0a8a86c58..7c7d8779199 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -14,59 +14,64 @@ import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.SliderStrategy; import static org.schabi.newpipe.player.BasePlayer.DEBUG; public class PlaybackParameterDialog extends DialogFragment { - private static final String TAG = "PlaybackParameterDialog"; + @NonNull private static final String TAG = "PlaybackParameterDialog"; - public static final float MINIMUM_PLAYBACK_VALUE = 0.25f; - public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f; + public static final double MINIMUM_PLAYBACK_VALUE = 0.25f; + public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; - public static final String STEP_UP_SIGN = "+"; - public static final String STEP_DOWN_SIGN = "-"; - public static final float PLAYBACK_STEP_VALUE = 0.05f; + public static final char STEP_UP_SIGN = '+'; + public static final char STEP_DOWN_SIGN = '-'; + public static final double PLAYBACK_STEP_VALUE = 0.05f; - public static final float NIGHTCORE_TEMPO = 1.20f; - public static final float NIGHTCORE_PITCH_LOWER = 1.15f; - public static final float NIGHTCORE_PITCH_UPPER = 1.25f; + public static final double NIGHTCORE_TEMPO = 1.20f; + public static final double NIGHTCORE_PITCH_LOWER = 1.15f; + public static final double NIGHTCORE_PITCH_UPPER = 1.25f; - public static final float DEFAULT_TEMPO = 1.00f; - public static final float DEFAULT_PITCH = 1.00f; + public static final double DEFAULT_TEMPO = 1.00f; + public static final double DEFAULT_PITCH = 1.00f; - private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; - private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; + @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; public interface Callback { void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); } - private Callback callback; + @Nullable private Callback callback; - private float initialTempo = DEFAULT_TEMPO; - private float initialPitch = DEFAULT_PITCH; + @NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic( + MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE, + /*centerAt=*/1.00f, /*sliderGranularity=*/10000); - private SeekBar tempoSlider; - private TextView tempoMinimumText; - private TextView tempoMaximumText; - private TextView tempoCurrentText; - private TextView tempoStepDownText; - private TextView tempoStepUpText; + private double initialTempo = DEFAULT_TEMPO; + private double initialPitch = DEFAULT_PITCH; - private SeekBar pitchSlider; - private TextView pitchMinimumText; - private TextView pitchMaximumText; - private TextView pitchCurrentText; - private TextView pitchStepDownText; - private TextView pitchStepUpText; + @Nullable private SeekBar tempoSlider; + @Nullable private TextView tempoMinimumText; + @Nullable private TextView tempoMaximumText; + @Nullable private TextView tempoCurrentText; + @Nullable private TextView tempoStepDownText; + @Nullable private TextView tempoStepUpText; - private CheckBox unhookingCheckbox; + @Nullable private SeekBar pitchSlider; + @Nullable private TextView pitchMinimumText; + @Nullable private TextView pitchMaximumText; + @Nullable private TextView pitchCurrentText; + @Nullable private TextView pitchStepDownText; + @Nullable private TextView pitchStepUpText; - private TextView nightCorePresetText; - private TextView resetPresetText; + @Nullable private CheckBox unhookingCheckbox; - public static PlaybackParameterDialog newInstance(final float playbackTempo, - final float playbackPitch) { + @Nullable private TextView nightCorePresetText; + @Nullable private TextView resetPresetText; + + public static PlaybackParameterDialog newInstance(final double playbackTempo, + final double playbackPitch) { PlaybackParameterDialog dialog = new PlaybackParameterDialog(); dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; @@ -91,16 +96,16 @@ public void onAttach(Context context) { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { - initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); - initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH); + initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); + initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putFloat(INITIAL_TEMPO_KEY, initialTempo); - outState.putFloat(INITIAL_PITCH_KEY, initialPitch); + outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); + outState.putDouble(INITIAL_PITCH_KEY, initialPitch); } /*////////////////////////////////////////////////////////////////////////// @@ -111,7 +116,7 @@ public void onSaveInstanceState(Bundle outState) { @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); - setupView(view); + setupControlViews(view); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) .setTitle(R.string.playback_speed_control) @@ -120,16 +125,16 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { .setNegativeButton(R.string.cancel, (dialogInterface, i) -> setPlaybackParameters(initialTempo, initialPitch)) .setPositiveButton(R.string.finish, (dialogInterface, i) -> - setPlaybackParameters(getCurrentTempo(), getCurrentPitch())); + setCurrentPlaybackParameters()); return dialogBuilder.create(); } /*////////////////////////////////////////////////////////////////////////// - // Dialog Builder + // Control Views //////////////////////////////////////////////////////////////////////////*/ - private void setupView(@NonNull View rootView) { + private void setupControlViews(@NonNull View rootView) { setupHookingControl(rootView); setupTempoControl(rootView); setupPitchControl(rootView); @@ -144,32 +149,123 @@ private void setupTempoControl(@NonNull View rootView) { tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); - tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); - tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); - tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); + if (tempoCurrentText != null) + tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); + if (tempoMaximumText != null) + tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); + if (tempoMinimumText != null) + tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); + + if (tempoStepUpText != null) { + tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + tempoStepUpText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } + + if (tempoStepDownText != null) { + tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + tempoStepDownText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } + + if (tempoSlider != null) { + tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); + tempoSlider.setProgress(strategy.progressOf(initialTempo)); + tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); + } + } + + private void setupPitchControl(@NonNull View rootView) { + pitchSlider = rootView.findViewById(R.id.pitchSeekbar); + pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); + pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); + pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); + pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); + pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); + + if (pitchCurrentText != null) + pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); + if (pitchMaximumText != null) + pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); + if (pitchMinimumText != null) + pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); + + if (pitchStepUpText != null) { + pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + pitchStepUpText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } + + if (pitchStepDownText != null) { + pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + pitchStepDownText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } + + if (pitchSlider != null) { + pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); + pitchSlider.setProgress(strategy.progressOf(initialPitch)); + pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + } + } - tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - tempoStepUpText.setOnClickListener(view -> - setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE)); + private void setupHookingControl(@NonNull View rootView) { + unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); + if (unhookingCheckbox != null) { + unhookingCheckbox.setChecked(initialPitch != initialTempo); + unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + if (isChecked) return; + // When unchecked, slide back to the minimum of current tempo or pitch + final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); + setSliders(minimum); + setCurrentPlaybackParameters(); + }); + } + } - tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - tempoStepDownText.setOnClickListener(view -> - setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE)); + private void setupPresetControl(@NonNull View rootView) { + nightCorePresetText = rootView.findViewById(R.id.presetNightcore); + if (nightCorePresetText != null) { + nightCorePresetText.setOnClickListener(view -> { + final double randomPitch = NIGHTCORE_PITCH_LOWER + + Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); + + setTempoSlider(NIGHTCORE_TEMPO); + setPitchSlider(randomPitch); + setCurrentPlaybackParameters(); + }); + } - tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); - tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo)); - tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); + resetPresetText = rootView.findViewById(R.id.presetReset); + if (resetPresetText != null) { + resetPresetText.setOnClickListener(view -> { + setTempoSlider(DEFAULT_TEMPO); + setPitchSlider(DEFAULT_PITCH); + setCurrentPlaybackParameters(); + }); + } } - + + /*////////////////////////////////////////////////////////////////////////// + // Sliders + //////////////////////////////////////////////////////////////////////////*/ + private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { return new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); - if (fromUser) { // this change is first in chain - setTempo(currentTempo); - } else { - setPlaybackParameters(currentTempo, getCurrentPitch()); + final double currentTempo = strategy.valueOf(progress); + if (fromUser) { + onTempoSliderUpdated(currentTempo); + setCurrentPlaybackParameters(); } } @@ -185,40 +281,14 @@ public void onStopTrackingTouch(SeekBar seekBar) { }; } - private void setupPitchControl(@NonNull View rootView) { - pitchSlider = rootView.findViewById(R.id.pitchSeekbar); - pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); - pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); - pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); - pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); - pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); - - pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); - pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); - pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); - - pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - pitchStepUpText.setOnClickListener(view -> - setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE)); - - pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - pitchStepDownText.setOnClickListener(view -> - setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE)); - - pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); - pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch)); - pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); - } - private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { return new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); + final double currentPitch = strategy.valueOf(progress); if (fromUser) { // this change is first in chain - setPitch(currentPitch); - } else { - setPlaybackParameters(getCurrentTempo(), currentPitch); + onPitchSliderUpdated(currentPitch); + setCurrentPlaybackParameters(); } } @@ -234,38 +304,7 @@ public void onStopTrackingTouch(SeekBar seekBar) { }; } - private void setupHookingControl(@NonNull View rootView) { - unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); - unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { - if (isChecked) return; - // When unchecked, slide back to the minimum of current tempo or pitch - final float minimum = Math.min(getCurrentPitch(), getCurrentTempo()); - setSliders(minimum); - }); - } - - private void setupPresetControl(@NonNull View rootView) { - nightCorePresetText = rootView.findViewById(R.id.presetNightcore); - nightCorePresetText.setOnClickListener(view -> { - final float randomPitch = NIGHTCORE_PITCH_LOWER + - (float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); - - setTempoSlider(NIGHTCORE_TEMPO); - setPitchSlider(randomPitch); - }); - - resetPresetText = rootView.findViewById(R.id.presetReset); - resetPresetText.setOnClickListener(view -> { - setTempoSlider(DEFAULT_TEMPO); - setPitchSlider(DEFAULT_PITCH); - }); - } - - /*////////////////////////////////////////////////////////////////////////// - // Helper - //////////////////////////////////////////////////////////////////////////*/ - - private void setTempo(final float newTempo) { + private void onTempoSliderUpdated(final double newTempo) { if (unhookingCheckbox == null) return; if (!unhookingCheckbox.isChecked()) { setSliders(newTempo); @@ -274,7 +313,7 @@ private void setTempo(final float newTempo) { } } - private void setPitch(final float newPitch) { + private void onPitchSliderUpdated(final double newPitch) { if (unhookingCheckbox == null) return; if (!unhookingCheckbox.isChecked()) { setSliders(newPitch); @@ -283,25 +322,30 @@ private void setPitch(final float newPitch) { } } - private void setSliders(final float newValue) { + private void setSliders(final double newValue) { setTempoSlider(newValue); setPitchSlider(newValue); } - private void setTempoSlider(final float newTempo) { + private void setTempoSlider(final double newTempo) { if (tempoSlider == null) return; - // seekbar doesn't register progress if it is the same as the existing progress - tempoSlider.setProgress(Integer.MAX_VALUE); - tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo)); + tempoSlider.setProgress(strategy.progressOf(newTempo)); } - private void setPitchSlider(final float newPitch) { + private void setPitchSlider(final double newPitch) { if (pitchSlider == null) return; - pitchSlider.setProgress(Integer.MAX_VALUE); - pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch)); + pitchSlider.setProgress(strategy.progressOf(newPitch)); } - private void setPlaybackParameters(final float tempo, final float pitch) { + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + private void setCurrentPlaybackParameters() { + setPlaybackParameters(getCurrentTempo(), getCurrentPitch()); + } + + private void setPlaybackParameters(final double tempo, final double pitch) { if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { if (DEBUG) Log.d(TAG, "Setting playback parameters to " + "tempo=[" + tempo + "], " + @@ -309,40 +353,27 @@ private void setPlaybackParameters(final float tempo, final float pitch) { tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - callback.onPlaybackParameterChanged(tempo, pitch); + callback.onPlaybackParameterChanged((float) tempo, (float) pitch); } } - private float getCurrentTempo() { - return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + private double getCurrentTempo() { + return tempoSlider == null ? initialTempo : strategy.valueOf( tempoSlider.getProgress()); } - private float getCurrentPitch() { - return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + private double getCurrentPitch() { + return pitchSlider == null ? initialPitch : strategy.valueOf( pitchSlider.getProgress()); } - /** - * Converts from zeroed float with a minimum offset to the nearest rounded slider - * equivalent integer - * */ - private static int getSliderEquivalent(final float minimumValue, final float floatValue) { - return Math.round((floatValue - minimumValue) * 100f); - } - - /** - * Converts from slider integer value to an equivalent float value with a given minimum offset - * */ - private static float getSliderEquivalent(final float minimumValue, final int intValue) { - return ((float) intValue) / 100f + minimumValue; - } - - private static String getStepUpPercentString(final float percent) { + @NonNull + private static String getStepUpPercentString(final double percent) { return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); } - private static String getStepDownPercentString(final float percent) { + @NonNull + private static String getStepDownPercentString(final double percent) { return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index b34cec72493..63ac7e8a187 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -60,11 +60,11 @@ public static String getTimeString(int milliSeconds) { : stringFormatter.format("%02d:%02d", minutes, seconds).toString(); } - public static String formatSpeed(float speed) { + public static String formatSpeed(double speed) { return speedFormatter.format(speed); } - public static String formatPitch(float pitch) { + public static String formatPitch(double pitch) { return pitchFormatter.format(pitch); } diff --git a/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java new file mode 100644 index 00000000000..efec1abb023 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java @@ -0,0 +1,73 @@ +package org.schabi.newpipe.util; + +public interface SliderStrategy { + /** + * Converts from zeroed double with a minimum offset to the nearest rounded slider + * equivalent integer + * */ + int progressOf(final double value); + + /** + * Converts from slider integer value to an equivalent double value with a given + * minimum offset + * */ + double valueOf(final int progress); + + // TODO: also implement linear strategy when needed + + final class Quadratic implements SliderStrategy { + private final double leftGap; + private final double rightGap; + private final double center; + + private final int centerProgress; + + /** + * Quadratic slider strategy that scales the value of a slider given how far the slider + * progress is from the center of the slider. The further away from the center, + * the faster the interpreted value changes, and vice versa. + * + * @param minimum the minimum value of the interpreted value of the slider. + * @param maximum the maximum value of the interpreted value of the slider. + * @param center center of the interpreted value between the minimum and maximum, which + * will be used as the center value on the slider progress. Doesn't need + * to be the average of the minimum and maximum values, but must be in + * between the two. + * @param maxProgress the maximum possible progress of the slider, this is the + * value that is shown for the UI and controls the granularity of + * the slider. Should be as large as possible to avoid floating + * point round-off error. Using odd number is recommended. + * */ + public Quadratic(double minimum, double maximum, double center, int maxProgress) { + if (center < minimum || center > maximum) { + throw new IllegalArgumentException("Center must be in between minimum and maximum"); + } + + this.leftGap = minimum - center; + this.rightGap = maximum - center; + this.center = center; + + this.centerProgress = maxProgress / 2; + } + + @Override + public int progressOf(double value) { + final double difference = value - center; + final double root = difference >= 0 ? + Math.sqrt(difference / rightGap) : + -Math.sqrt(Math.abs(difference / leftGap)); + final double offset = Math.round(root * centerProgress); + + return (int) (centerProgress + offset); + } + + @Override + public double valueOf(int progress) { + final int offset = progress - centerProgress; + final double square = Math.pow(((double) offset) / ((double) centerProgress), 2); + final double difference = square * (offset >= 0 ? rightGap : leftGap); + + return difference + center; + } + } +} diff --git a/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java b/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java new file mode 100644 index 00000000000..8c8d5204388 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java @@ -0,0 +1,86 @@ +package org.schabi.newpipe.util; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class QuadraticSliderStrategyTest { + private final static int STEP = 100; + private final static float DELTA = 1f / (float) STEP; + + private final SliderStrategy.Quadratic standard = + new SliderStrategy.Quadratic(0f, 100f, 50f, STEP); + @Test + public void testLeftBound() throws Exception { + assertEquals(standard.progressOf(0), 0); + assertEquals(standard.valueOf(0), 0f, DELTA); + } + + @Test + public void testCenter() throws Exception { + assertEquals(standard.progressOf(50), 50); + assertEquals(standard.valueOf(50), 50f, DELTA); + } + + @Test + public void testRightBound() throws Exception { + assertEquals(standard.progressOf(100), 100); + assertEquals(standard.valueOf(100), 100f, DELTA); + } + + @Test + public void testLeftRegion() throws Exception { + final int leftProgress = standard.progressOf(25); + final double leftValue = standard.valueOf(25); + assertTrue(leftProgress > 0 && leftProgress < 50); + assertTrue(leftValue > 0f && leftValue < 50); + } + + @Test + public void testRightRegion() throws Exception { + final int leftProgress = standard.progressOf(75); + final double leftValue = standard.valueOf(75); + assertTrue(leftProgress > 50 && leftProgress < 100); + assertTrue(leftValue > 50f && leftValue < 100); + } + + @Test + public void testConversion() throws Exception { + assertEquals(standard.progressOf(standard.valueOf(0)), 0); + assertEquals(standard.progressOf(standard.valueOf(25)), 25); + assertEquals(standard.progressOf(standard.valueOf(50)), 50); + assertEquals(standard.progressOf(standard.valueOf(75)), 75); + assertEquals(standard.progressOf(standard.valueOf(100)), 100); + } + + @Test + public void testReverseConversion() throws Exception { + // Need a larger delta since step size / granularity is too small and causes + // floating point round-off errors during conversion + final float largeDelta = 1f; + + assertEquals(standard.valueOf(standard.progressOf(0)), 0f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(25)), 25f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(50)), 50f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(75)), 75f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(100)), 100f, largeDelta); + } + + @Test + public void testQuadraticPropertyLeftRegion() throws Exception { + final double differenceCloserToCenter = + Math.abs(standard.valueOf(40) - standard.valueOf(45)); + final double differenceFurtherFromCenter = + Math.abs(standard.valueOf(10) - standard.valueOf(15)); + assertTrue(differenceCloserToCenter < differenceFurtherFromCenter); + } + + @Test + public void testQuadraticPropertyRightRegion() throws Exception { + final double differenceCloserToCenter = + Math.abs(standard.valueOf(75) - standard.valueOf(70)); + final double differenceFurtherFromCenter = + Math.abs(standard.valueOf(95) - standard.valueOf(90)); + assertTrue(differenceCloserToCenter < differenceFurtherFromCenter); + } +} From 8b60397f068c2d99f0d92d796400c6f77c2e5cd6 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Mar 2018 18:11:59 -0700 Subject: [PATCH 10/12] -Changed detail fragment thumbnail failure to produce a snackbar error rather than a full error activity. --- .../fragments/detail/VideoDetailFragment.java | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 2a95125df97..74e561f991f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -43,6 +43,7 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import org.schabi.newpipe.R; @@ -582,27 +583,20 @@ private View.OnTouchListener getOnControlsTouchListener() { }; } - private void initThumbnailViews(StreamInfo info) { + private void initThumbnailViews(@NonNull StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (!TextUtils.isEmpty(info.getThumbnailUrl())) { - imageLoader.displayImage( - info.getThumbnailUrl(), - thumbnailImageView, - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, - new SimpleImageLoadingListener() { + final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); + final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError( - activity, - failReason.getCause(), - null, - activity.findViewById(android.R.id.content), - ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, - NewPipe.getNameOfService(currentInfo.getServiceId()), - imageUri, - R.string.could_not_load_thumbnails)); + showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, + infoServiceName, imageUri, R.string.could_not_load_thumbnails); } - }); + }; + + imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); } if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { From 72eaff148cbb473c840e59f4dd339c3c6c73abf2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Mar 2018 18:12:11 -0700 Subject: [PATCH 11/12] -Fixed main player paused video not abandoning audio focus after navigating away from activity during interruption, when resume on focus regain is enabled. -Separated onPause and onPlay functions from onPlayPause. -Renamed onVideoPlayPause to onPlayPause. --- .../newpipe/player/BackgroundPlayer.java | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 33 ++++++++++++++----- .../newpipe/player/MainVideoPlayer.java | 7 ++-- .../newpipe/player/PopupVideoPlayer.java | 4 +-- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../playback/BasePlayerMediaSession.java | 4 +-- 6 files changed, 33 insertions(+), 19 deletions(-) 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 f799941bdc5..ac070fb444e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -486,7 +486,7 @@ public void onBroadcastReceived(Intent intent) { onClose(); break; case ACTION_PLAY_PAUSE: - onVideoPlayPause(); + onPlayPause(); break; case ACTION_REPEAT: onRepeatClicked(); 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 fe19030f96b..cd1451d3765 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -392,7 +392,7 @@ public void onBroadcastReceived(Intent intent) { if (intent == null || intent.getAction() == null) return; switch (intent.getAction()) { case AudioManager.ACTION_AUDIO_BECOMING_NOISY: - if (isPlaying()) onVideoPlayPause(); + onPause(); break; } } @@ -948,14 +948,11 @@ public void onPrepared(boolean playWhenReady) { changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); } - public void onVideoPlayPause() { - if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); + public void onPlay() { + if (DEBUG) Log.d(TAG, "onPlay() called"); + if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return; - if (!isPlaying()) { - audioReactor.requestAudioFocus(); - } else { - audioReactor.abandonAudioFocus(); - } + audioReactor.requestAudioFocus(); if (getCurrentState() == STATE_COMPLETED) { if (playQueue.getIndex() == 0) { @@ -965,7 +962,25 @@ public void onVideoPlayPause() { } } - simpleExoPlayer.setPlayWhenReady(!isPlaying()); + simpleExoPlayer.setPlayWhenReady(true); + } + + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause() called"); + if (audioReactor == null || simpleExoPlayer == null) return; + + audioReactor.abandonAudioFocus(); + simpleExoPlayer.setPlayWhenReady(false); + } + + public void onPlayPause() { + if (DEBUG) Log.d(TAG, "onPlayPause() called"); + + if (!isPlaying()) { + onPlay(); + } else { + onPause(); + } } public void onFastRewind() { 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 cbc4b823035..dbc34b11a6d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -49,7 +49,6 @@ import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; @@ -153,7 +152,7 @@ protected void onResume() { if (DEBUG) Log.d(TAG, "onResume() called"); if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() && !playerImpl.isPlaying()) { - playerImpl.onVideoPlayPause(); + playerImpl.onPlay(); } activityPaused = false; @@ -188,7 +187,7 @@ protected void onPause() { if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { playerImpl.wasPlaying = playerImpl.isPlaying(); - if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause(); + playerImpl.onPause(); } activityPaused = true; } @@ -563,7 +562,7 @@ public void onPlayBackgroundButtonClicked() { public void onClick(View v) { super.onClick(v); if (v.getId() == playPauseButton.getId()) { - onVideoPlayPause(); + onPlayPause(); } else if (v.getId() == playPreviousButton.getId()) { onPlayPrevious(); 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 64dc03da684..20860d9c597 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -618,7 +618,7 @@ public void onBroadcastReceived(Intent intent) { onClose(); break; case ACTION_PLAY_PAUSE: - onVideoPlayPause(); + onPlayPause(); break; case ACTION_REPEAT: onRepeatClicked(); @@ -731,7 +731,7 @@ public boolean onDoubleTap(MotionEvent e) { public boolean onSingleTapConfirmed(MotionEvent e) { if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); if (playerImpl == null || playerImpl.getPlayer() == null) return false; - playerImpl.onVideoPlayPause(); + playerImpl.onPlayPause(); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1f850944d66..994aa60e855 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -424,7 +424,7 @@ public void onClick(View view) { player.onPlayPrevious(); } else if (view.getId() == playPauseButton.getId()) { - player.onVideoPlayPause(); + player.onPlayPause(); } else if (view.getId() == forwardButton.getId()) { player.onPlayNext(); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java index 07504542c15..6168799179c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -62,12 +62,12 @@ public MediaDescriptionCompat getQueueMetadata(int index) { @Override public void onPlay() { - if (!player.isPlaying()) player.onVideoPlayPause(); + player.onPlay(); } @Override public void onPause() { - if (player.isPlaying()) player.onVideoPlayPause(); + player.onPause(); } @Override From 02f48ccc7f4e0338557a47bc24d0993e2b4a71ff Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Mar 2018 18:44:03 -0700 Subject: [PATCH 12/12] -Removed duplicate dialog open instances in service player activity. --- .../newpipe/player/ServicePlayerActivity.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 994aa60e855..239c9c8d306 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -433,12 +433,10 @@ public void onClick(View view) { player.onShuffleClicked(); } else if (view.getId() == playbackSpeedButton.getId()) { - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + openPlaybackParameterDialog(); } else if (view.getId() == playbackPitchButton.getId()) { - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + openPlaybackParameterDialog(); } else if (view.getId() == metadata.getId()) { scrollToSelected(); @@ -450,9 +448,15 @@ public void onClick(View view) { } //////////////////////////////////////////////////////////////////////////// - // Playback Parameters Listener + // Playback Parameters //////////////////////////////////////////////////////////////////////////// + private void openPlaybackParameterDialog() { + if (player == null) return; + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + } + @Override public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);