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/15] -[#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 5ea067d00..8baabed6b 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 cee885e22..5355e19ee 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 com.nostra13.universalimageloader.core.listener.ImageLoadingListener; 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 @@ public abstract class BasePlayer implements playQueue = queue; playQueue.init(); + if (playbackManager != null) playbackManager.dispose(); playbackManager = new MediaSourceManager(this, playQueue); if (playQueueAdapter != null) playQueueAdapter.dispose(); @@ -272,7 +274,6 @@ public abstract class BasePlayer implements public void destroy() { if (DEBUG) Log.d(TAG, "destroy() called"); destroyPlayer(); - clearThumbnailCache(); unregisterBroadcastReceiver(); trackSelector = null; @@ -314,11 +315,6 @@ public abstract class BasePlayer implements 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 abstract class BasePlayer implements public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); if (!isProgressLoopRunning()) startProgressLoop(); - if (!isCurrentWindowValid()) seekToDefault(); } public void onBuffering() {} @@ -522,11 +517,9 @@ public abstract class BasePlayer implements ); } - 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 abstract class BasePlayer implements (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 abstract class BasePlayer implements // 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 abstract class BasePlayer implements if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); simpleExoPlayer.prepare(mediaSource); - seekToDefault(); } @Override @@ -825,16 +830,24 @@ public abstract class BasePlayer implements 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 @@ public abstract class BasePlayer implements @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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "onPlayNext() called"); savePlaybackState(); - playQueue.offsetIndex(+1); } @@ -975,8 +996,9 @@ public abstract class BasePlayer implements 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 @@ public abstract class BasePlayer implements 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 4f27d1fee..dd7e0c71e 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.PlayQueue; 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 java.util.UUID; 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 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR 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 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR 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 final class MainVideoPlayer extends Activity implements StateSaver.WriteR animatePlayButtons(true, 200); }); - changeSystemUi(); getRootView().setKeepScreenOn(true); } @@ -798,31 +799,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + 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(); - playQueue.move(sourceIndex, targetIndex); - return true; + public void onMove(int sourceIndex, int targetIndex) { + if (playQueue != null) playQueue.move(sourceIndex, targetIndex); } - - @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/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index c68133094..1c3ffe911 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.player.event.PlayerEventListener; 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 abstract class ServicePlayerActivity extends AppCompatActivity } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + return new PlayQueueItemTouchCallback() { @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(); + 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 ea13a28e7..50c069b40 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.MoveEvent; 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 io.reactivex.subjects.PublishSubject; 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 windowRefreshTimeMillis; + 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 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 class MediaSourceManager { 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { final ReorderEvent reorderEvent = (ReorderEvent) event; move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); break; - case SELECT: case RECOVERY: default: break; @@ -347,8 +335,13 @@ public class MediaSourceManager { // 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 @@ public class MediaSourceManager { } 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { /* 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { } } + /** + * 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 @@ public class MediaSourceManager { 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 b37a269e2..34c7702bc 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 org.schabi.newpipe.playlist.PlayQueueItem; 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 000000000..405dba11e --- /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 e0836e06c..53e8d6fc4 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 47c45e82a..ecc66bb40 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 final class InfoCache { 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 7d71750eb..9d71ae83a 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.ServiceList; 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 @@ public class ServiceHelper { 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 c3480c547..11765f901 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 e7d337c17..8f608de3a 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 639a8037c..7f649e382 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 9bbd72fec..0c3ea77df 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 a897aa185..68d75737a 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 c97f12809..e1a353807 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 c8c1efb12..2ce8bf9e6 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/15] -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 8f608de3a..c581c3203 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/15] -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 9fa911e54..3529a37b1 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/15] -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 | 70 +++++++++++-------- .../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, 206 insertions(+), 271 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 8967d8cb0..ce4318427 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.support.v7.app.AppCompatActivity; 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 abstract class BaseFragment extends Fragment { 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 8baabed6b..eb5e92e88 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 @@ public class ImageDownloader extends BaseImageDownloader { 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 b3ca5f47f..2a95125df 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.ErrorActivity; 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 @@ public class VideoDetailFragment 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 class VideoDetailFragment } 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 7783d8a98..dbc61961e 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.report.UserAction; 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 class ChannelFragment extends BaseListInfoFragment { 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 9033560bd..3bcf9d322 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.PlaylistPlayQueue; 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 class PlaylistFragment extends BaseListInfoFragment { 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 4794def97..5dc6c17a4 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 e4087d8a8..2dffdbfdb 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 abstract class LocalItemHolder extends RecyclerView.ViewHolder { } 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 1fbea6cc4..d9eb7caa5 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 @@ package org.schabi.newpipe.fragments.local.holder; 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 class LocalPlaylistItemHolder extends PlaylistItemHolder { 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 0696f5f61..5f9555d9f 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.view.ViewGroup; 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 class LocalPlaylistStreamItemHolder extends LocalItemHolder { } // 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 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { 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 cd0630b37..199158672 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.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.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 class LocalStatisticStreamItemHolder extends LocalItemHolder { 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 class LocalStatisticStreamItemHolder extends LocalItemHolder { 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 bab76ddcb..57bc2a3cb 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.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.fragments.local.LocalItemBuilder; @@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { 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 0f7b00e6d..871138464 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.LocalItem; 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 class RemotePlaylistItemHolder extends PlaylistItemHolder { 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 4830ed33b..4fe2b701d 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 com.nostra13.universalimageloader.core.ImageLoader; 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 @@ public class WatchHistoryFragment extends HistoryFragment { 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 211fa60cd..643886da8 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 class ChannelMiniInfoItemHolder extends InfoItemHolder { 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 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { } 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 fb5aa2b7c..ebb5b4114 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.support.v7.widget.RecyclerView; 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 abstract class InfoItemHolder extends RecyclerView.ViewHolder { } 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 30d84e1bd..b6bd2f389 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.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.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 class PlaylistMiniInfoItemHolder extends InfoItemHolder { 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 class PlaylistMiniInfoItemHolder extends InfoItemHolder { 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 72c2830e1..048b907af 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.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.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 class StreamMiniInfoItemHolder extends InfoItemHolder { 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 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { 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 61720c6b4..83ed54cf5 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 final class BackgroundPlayer extends Service { shouldUpdateOnProgress = true; mReceiverComponent = new ComponentName(this, MediaButtonReceiver.class); - basePlayerImpl.audioReactor.registerMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.getAudioReactor().registerMediaButtonEventReceiver(mReceiverComponent); } @Override @@ -152,7 +152,7 @@ public final class BackgroundPlayer extends Service { lockManager.releaseWifiAndCpu(); } if (basePlayerImpl != null) { - basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.getAudioReactor().unregisterMediaButtonEventReceiver(mReceiverComponent); basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } @@ -575,38 +575,46 @@ public final class BackgroundPlayer extends Service { } 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 5355e19ee..5ec61b058 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 abstract class BasePlayer implements "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 abstract class BasePlayer implements } 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 df30c3e79..c1896599f 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au //////////////////////////////////////////////////////////////////////////*/ 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au } public void abandonAudioFocus() { - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { audioManager.abandonAudioFocusRequest(request); } else { audioManager.abandonAudioFocus(this); @@ -83,24 +91,20 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au 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 73cdf1113..7042bea89 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 class PlayQueueItemBuilder { 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 class PlayQueueItemBuilder { 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 class PlayQueueItemBuilder { 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 @@ public class PlayQueueItemBuilder { 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 26278ac75..f0ab3bc03 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.DialogInterface; 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 000000000..9ee8a1095 --- /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 23f5224c5..57a3cbef9 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 e1a353807..cd280ff02 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/15] -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 | 139 +++++++++--------- .../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, 92 insertions(+), 79 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3529a37b1..952bc3067 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 5ec61b058..de85a3704 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 abstract class BasePlayer implements // 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements } 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(); - - /* Skip recovering if the recovery position is not set.*/ - if (position == PlayQueueItem.RECOVERY_UNSET) return; - - if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + - " at: " + getTimeString((int)position)); - simpleExoPlayer.seekTo(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; + + } else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) { + if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); + // Is still synchronizing? + seekToDefault(); + + } 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements "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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 dd7e0c71e..90a4a8c9f 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 final class MainVideoPlayer extends Activity implements StateSaver.WriteR 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 123fbfee3..64dc03da6 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 @@ public final class PopupVideoPlayer extends Service { 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 1c3ffe911..50248891b 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 abstract class ServicePlayerActivity extends AppCompatActivity @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 48b13654c..aa896bb69 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 abstract class VideoPlayer extends BasePlayer // 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 abstract class VideoPlayer extends BasePlayer @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 abstract class VideoPlayer extends BasePlayer } protected void onFullScreenButtonClicked() { - if (!isPlayerReady()) return; - changeState(STATE_BLOCKED); } @@ -735,7 +733,7 @@ public abstract class VideoPlayer extends BasePlayer } 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 abstract class VideoPlayer extends BasePlayer 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 50c069b40..170668169 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { /* No exception handling since getLoadedMediaSource guarantees nonnull return */ .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); - } else { - maybeSynchronizePlayer(); } } @@ -467,6 +465,12 @@ public class MediaSourceManager { * 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 @@ public class MediaSourceManager { 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/15] -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 952bc3067..9b2569a66 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 18b3222a0..1be8c1f2c 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au } } + public void dispose() { + abandonAudioFocus(); + player.removeAudioDebugListener(this); + } + /*////////////////////////////////////////////////////////////////////////// // Audio Manager //////////////////////////////////////////////////////////////////////////*/ @@ -91,22 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au 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 000000000..8405e45fd --- /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 000000000..431a90d8a --- /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 000000000..a1a57a87d --- /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 000000000..429c26fd9 --- /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 000000000..2aa41bd63 --- /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 000000000..07504542c --- /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/15] -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 d07baf2a7..5f029cc50 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 class FailedMediaSource implements ManagedMediaSource { 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 f523667f9..fe7508ecc 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 class LoadedMediaSource implements ManagedMediaSource { } @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 3bb7ca429..46fd149bb 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 com.google.android.exoplayer2.source.MediaSource; 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 0d3436a01..2c57f2f9c 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 170668169..477358113 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { 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 @@ public class MediaSourceManager { 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 752dc223d..df4d19720 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 org.schabi.newpipe.util.ExtractorHelper; 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 class PlayQueueItem implements Serializable { 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/15] -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 9b2569a66..5c434c30c 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 1be8c1f2c..1edd67d24 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 90a4a8c9f..cbc4b8230 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.preference.PreferenceManager; 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.SeekBar; 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.R; 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 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; * * @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 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR } } + //////////////////////////////////////////////////////////////////////////// + // 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 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR 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 50248891b..1f850944d 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.extractor.stream.StreamInfo; 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.formatPitch; 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 @@ public abstract class ServicePlayerActivity extends AppCompatActivity 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 abstract class ServicePlayerActivity extends AppCompatActivity 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 abstract class ServicePlayerActivity extends AppCompatActivity } } + //////////////////////////////////////////////////////////////////////////// + // 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 abstract class ServicePlayerActivity extends AppCompatActivity 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 aa896bb69..b019ea91e 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 android.widget.TextView; 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 abstract class VideoPlayer extends BasePlayer 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 abstract class VideoPlayer extends BasePlayer 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 abstract class VideoPlayer extends BasePlayer 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 000000000..8a0a8a86c --- /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 000000000..a8c6a5dcd --- /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 cd280ff02..effdeaaba 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/15] -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 | 345 ++++++++++-------- .../newpipe/player/helper/PlayerHelper.java | 4 +- .../schabi/newpipe/util/SliderStrategy.java | 73 ++++ .../util/QuadraticSliderStrategyTest.java | 86 +++++ 5 files changed, 353 insertions(+), 160 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 d2c4e1b14..eb366d97f 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 abstract class BaseLocalListFragment extends BaseStateFragment @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 8a0a8a86c..7c7d87791 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.SeekBar; 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 class PlaybackParameterDialog extends DialogFragment { 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 class PlaybackParameterDialog extends DialogFragment { @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 class PlaybackParameterDialog extends DialogFragment { .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,45 +149,34 @@ public class PlaybackParameterDialog extends DialogFragment { 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)); - tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - tempoStepUpText.setOnClickListener(view -> - setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE)); + if (tempoStepUpText != null) { + tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + tempoStepUpText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - tempoStepDownText.setOnClickListener(view -> - setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE)); + if (tempoStepDownText != null) { + tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + tempoStepDownText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - 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. - } - }; + if (tempoSlider != null) { + tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); + tempoSlider.setProgress(strategy.progressOf(initialTempo)); + tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); + } } private void setupPitchControl(@NonNull View rootView) { @@ -193,32 +187,85 @@ public class PlaybackParameterDialog extends DialogFragment { 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)); + 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)); - pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - pitchStepUpText.setOnClickListener(view -> - setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE)); + if (pitchStepUpText != null) { + pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + pitchStepUpText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - pitchStepDownText.setOnClickListener(view -> - setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE)); + if (pitchStepDownText != null) { + pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + pitchStepDownText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); - pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch)); - pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + if (pitchSlider != null) { + pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); + pitchSlider.setProgress(strategy.progressOf(initialPitch)); + pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + } } - private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { + 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(); + }); + } + } + + 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(); + }); + } + + 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 currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); - if (fromUser) { // this change is first in chain - setPitch(currentPitch); - } else { - setPlaybackParameters(getCurrentTempo(), currentPitch); + final double currentTempo = strategy.valueOf(progress); + if (fromUser) { + onTempoSliderUpdated(currentTempo); + setCurrentPlaybackParameters(); } } @@ -234,38 +281,30 @@ public class PlaybackParameterDialog extends DialogFragment { }; } - 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 SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final double currentPitch = strategy.valueOf(progress); + if (fromUser) { // this change is first in chain + onPitchSliderUpdated(currentPitch); + setCurrentPlaybackParameters(); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + }; } - 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 @@ public class PlaybackParameterDialog extends DialogFragment { } } - 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 @@ public class PlaybackParameterDialog extends DialogFragment { } } - 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 @@ public class PlaybackParameterDialog extends DialogFragment { 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 b34cec724..63ac7e8a1 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 class PlayerHelper { : 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 000000000..efec1abb0 --- /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 000000000..8c8d52043 --- /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/15] -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 2a95125df..74e561f99 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 android.widget.Toast; 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 @@ public class VideoDetailFragment }; } - 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/15] -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 f799941bd..ac070fb44 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 final class BackgroundPlayer extends Service { 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 fe19030f9..cd1451d37 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements 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 abstract class BasePlayer implements } } - 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 cbc4b8230..dbc34b11a 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.SeekBar; 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 @@ public final class MainVideoPlayer extends AppCompatActivity 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 @@ public final class MainVideoPlayer extends AppCompatActivity 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 final class MainVideoPlayer extends AppCompatActivity 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 64dc03da6..20860d9c5 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 final class PopupVideoPlayer extends Service { onClose(); break; case ACTION_PLAY_PAUSE: - onVideoPlayPause(); + onPlayPause(); break; case ACTION_REPEAT: onRepeatClicked(); @@ -731,7 +731,7 @@ public final class PopupVideoPlayer extends Service { 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 1f850944d..994aa60e8 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 abstract class ServicePlayerActivity extends AppCompatActivity 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 07504542c..616879917 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 class BasePlayerMediaSession implements MediaSessionCallback { @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/15] -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 994aa60e8..239c9c8d3 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 abstract class ServicePlayerActivity extends AppCompatActivity 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 abstract class ServicePlayerActivity extends AppCompatActivity } //////////////////////////////////////////////////////////////////////////// - // 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); From 40ea5eb53d375a6615d0a255703908c8f15d7a83 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 23 Mar 2018 16:15:50 +0100 Subject: [PATCH 13/15] Update README.md Add compressed screenshots Add liberapay to donation section --- README.md | 27 +++++++++++------- assets/bitcoin_qr_code.png | Bin 346 -> 784 bytes assets/bountysource_qr_code.png | Bin 1269 -> 801 bytes assets/liberapay_donate_button.svg | 1 + assets/liberapay_qr_code.png | Bin 0 -> 807 bytes .../en-US/images/phoneScreenshots/shot_1.png | Bin 280512 -> 66176 bytes .../en-US/images/phoneScreenshots/shot_2.png | Bin 97633 -> 30026 bytes .../en-US/images/phoneScreenshots/shot_3.png | Bin 157863 -> 29447 bytes .../en-US/images/phoneScreenshots/shot_4.png | Bin 285126 -> 91922 bytes .../en-US/images/phoneScreenshots/shot_5.png | Bin 410030 -> 50829 bytes .../en-US/images/phoneScreenshots/shot_7.png | Bin 142697 -> 102615 bytes .../en-US/images/phoneScreenshots/shot_8.png | Bin 51055 -> 45961 bytes .../en-US/images/phoneScreenshots/shot_9.png | Bin 211855 -> 19404 bytes 13 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 assets/liberapay_donate_button.svg create mode 100644 assets/liberapay_qr_code.png diff --git a/README.md b/README.md index 52c1159dd..065e35bf1 100644 --- a/README.md +++ b/README.md @@ -77,19 +77,24 @@ The more is done the better it gets! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). ## Donate -If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/). +If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/). - - - - - - - - - - + + + + + + + + + + + + + + +
BitcoinBitcoin QR Code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
Visit NewPipe at bountysource.comCheck out how many bounties you can earn.
BitcoinBitcoin QR Code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
LiberapayVisit NewPipe at liberapay.comDonate via Liberapay
BountysourceVisit NewPipe at bountysource.comCheck out how many bounties you can earn.
## License diff --git a/assets/bitcoin_qr_code.png b/assets/bitcoin_qr_code.png index 17fa26518dbbce2349e78b4c1aae4df3910ee0cb..13e3f76fecac1e5c519a3542e7cad9a7c299ea09 100644 GIT binary patch delta 772 zcmcb`G=Xh`L_G%^0|P_Bqp2Yb3{2NOT^vIy=DeNm*muW)hxz&c`R`(iJRZE2yR;}r zQIl!k4WpxJy2oyucUt_V(wgB{G=rpC1COvn8>hnIEv%n@);yZN>Gavjr|cKDZ)ISb zkiYwm&B{N*RT17SK^M<0-`x{3@12}pacJ{yM}|!^S4-XDWtwodeopp-X@MU-Mep-& zxO6!6el$?-=JzmxhMaG{96=vTxWj7hy-L(=U^t@5rohiucskfI^Q zBGa^PpM7?2N!eOffrdXN@(T55G}%^qb6WhYbLXFaZ#zH3Cl4gL-(UJ^TWNPlec4J? zTg?WBC*PAA{%HJ`-ZXpVj~(+?#-553PZPCp*!PB&!(mUu-Or*8qBHJFUddtLtYBJV z@IFnNf63w{eL;+<9o#uI`6c?HUUPEHN4neuyG|D44_0t&m})Zy5m zpK2u{&))gQ=J|bn!@$Vl(D%IFNA$s}#XWto@9wSQcVJk=&fbx*XLevsOnv$E-P?cf zVKDq&HeX!f*}Zq4-5NDk>G+F=&0L?dBE_z(s^*s~!_MhHZ@m2YQk?Ng+_U86$Cvk} z?lFBoFZ|}~Uoz#p8JCOlOh{y9KC&m~{1hv$)fKCD1EXbgy4Qc138&s?uqLcJF{M6T z%ie)uk|m3S(<{BJd*44Rd|JiCqQKyJ=J-BE*{3`I`KNh&We{j!P+?jU?d9bE?*|9q3mE#tx|qaRDDzI=s-^^ohOnOR4xSCZI2?|63}& zbK1VA?@_SiJCQeEHi$W_;Qcu19lR@f zNA4PvM>gAbU~0NF>)_kOwc`JS{bfTcw=*eDbr%H&)_ryljht0HsrSyVKcoFU_f&$y91cq89ZJ6T-G@yGywoCT3qq~ delta 331 zcmV-R0kr;*2HFCU7=Hl(00013nW*^y00ALML_t(|+O^a>ZiFxp2H+pLa0eH#Je1Xv>>79p z=-1hjj)0!~wv1#6+=)iCzH+z_ryk9wO+#Zyr101;dNmt*&9!$k&=@-AWz@=fwsg#3 zf{tn!#88%altfLZ#v3}eN?TEot0~Uji_d#k()^loEY@u3 dKmLk8^Z~+~^P3w}ZZiM?002ovPDHLkV1mp-llcGu diff --git a/assets/bountysource_qr_code.png b/assets/bountysource_qr_code.png index 4fe03236a7fdbfcae026a6421adde61744fab141..18ff10fd041d94baf04714897d26fa64aacfe00d 100644 GIT binary patch literal 801 zcmV++1K#|JP)GW9u9k7J^;ugdpPX<^y#1+fT{XB3!4@| z0P-^!Apey09irAdeO5J&2Q_>2-#Z;1;~95YWCJbp9a#YQA4NZI-1C$)>Hvx zG>ckO1(4A!YE4!1&t7K%ouJNAlgmh)bu$7a(0^4A5Thn__EjLOA?p3k+=)ID86BTh z0EuUGd{zM@p3(7Hb>qjy9hN%bK?UOUKlR#5)EMrVRr>&>G2Ah$_5p}tBz1oppaaK& zcBpHm6Ndogy#J~1RyuJAK+gN0`gRPeXFHl+hO@jUUMPTE)TizP#PG8vkdE0Wjdwt) zmzr0tQD;>FM9Qny0OVVMq)erj;XqU8re46V2Y_761&G+QGwas@=?qUvU4Xiz&Rn%m z(hoo`>Hz`}&ESMtM=77HddL5rvjDlM2Z*Sz0kUeGQs=W){XABCrgEu%PJ1~69RXy~ z%NghhAd6npLsAP+`>0@@D=B~g1R$q5qurFj!^?199H-s|2#|0+KpOp3KTiD^kkrCD z##Ij~IX>010BHyyji7EIMgJ`jM=7&AydbK7fB-}@?wAFLI7AJqLU_`r1J)HlgnEDg zWHxALRw{G~xO&#j0uX?l&jQHsK(&vv47I=N!C5vy00NL#fT(i7I-n+AoX&l<=5Qw2 zOz8uV#hGL?r4K+BXHwx(3rL2$I;+BWGIsp~1R&>2>iALhD}bPP1dz=Hkj(^;%>me8wfzOJ0!O5W~h69W`8#%-pn^hudQ zdgeR;RQ|%uqA%(ykI;wUh4wcj;7S<_tUdT)@RczDC?0ATpE#v@81c>bE!8|PnYV6G z)OYEg4#^=~Gy8P7`FGW|g`c0>dk(?)j)8jeVtGqoz*#i7cMn)xSz0xFUat#SrzZPb za4*nG^v6@v4nLVPQ?LovT?qsZ9No{YQRJT79gHrB=Ah=}8vi{FN3$72>{Lj-8V~jo zh~+>P|3*G0^MX6qP3}Q)C+d(3^xY8Tu^6)kSJWu4<*LM6nR9U!Ii7%$}@Nj#Fu~NCt5UyCV_03)G9^-|AN`dm6_0Tv^Z|{V7F2AUvBSAESqc`8g9F= zqegJd0E!@6KdyNib%@f*KgmpgZN8=xxm=zG!|4gF+9?!K69()Sdpp6hyDHXwZ!I`RaO= zMERPKHKv16e9h;@^g2h{PHjr)AUTgP_X5)><}zQ`Wk1juco4MgP}8Q^p@s196euS% zArIo6q64XyLU;hhS3LyHC1-xyZu`0GCw0Pvl)%hg4tv}lL}SNJ$>CI%SN&iA1l~<) z*Q3~g8F@52d)MT}Ohls$X1xGWBL$`mv8BkPZ;0^X>IO{J7D=|M(i6ufqM7Z2X6J&HxLq|tv|ixYk_JDJTEbigNJmr`z7g#3|(u%FDt z792ELv0YABX>q4-#O#J7l-j6TrxpXSeN=QLegPjM#guY5O{g-E$iob{Wpuk>n?$!Lv%^r;~;@ zKV;hsVTnS(SinboyxdLTe@tV4n=POW);o7eSM@}5QGTnUHUAytyS;M*