mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	-[#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.
This commit is contained in:
		| @@ -1,25 +1,40 @@ | |||||||
| package org.schabi.newpipe; | package org.schabi.newpipe; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.download.BaseImageDownloader; | import com.nostra13.universalimageloader.core.download.BaseImageDownloader; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
|  |  | ||||||
|  | import java.io.ByteArrayInputStream; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
|  |  | ||||||
| public class ImageDownloader extends BaseImageDownloader { | 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) { |     public ImageDownloader(Context context) { | ||||||
|         super(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) { |     private boolean isDownloadingThumbnail() { | ||||||
|         super(context, connectTimeout, readTimeout); |         return preferences.getBoolean(downloadThumbnailKey, true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { |     protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { | ||||||
|         Downloader downloader = (Downloader) NewPipe.getDownloader(); |         if (isDownloadingThumbnail()) { | ||||||
|  |             final Downloader downloader = (Downloader) NewPipe.getDownloader(); | ||||||
|             return downloader.stream(imageUri); |             return downloader.stream(imageUri); | ||||||
|  |         } else { | ||||||
|  |             return DUMMY_INPUT_STREAM; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -57,6 +57,7 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | |||||||
| import org.schabi.newpipe.Downloader; | import org.schabi.newpipe.Downloader; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.history.HistoryRecordManager; | import org.schabi.newpipe.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.player.helper.AudioReactor; | import org.schabi.newpipe.player.helper.AudioReactor; | ||||||
| import org.schabi.newpipe.player.helper.LoadController; | import org.schabi.newpipe.player.helper.LoadController; | ||||||
| @@ -244,6 +245,7 @@ public abstract class BasePlayer implements | |||||||
|  |  | ||||||
|         playQueue = queue; |         playQueue = queue; | ||||||
|         playQueue.init(); |         playQueue.init(); | ||||||
|  |         if (playbackManager != null) playbackManager.dispose(); | ||||||
|         playbackManager = new MediaSourceManager(this, playQueue); |         playbackManager = new MediaSourceManager(this, playQueue); | ||||||
|  |  | ||||||
|         if (playQueueAdapter != null) playQueueAdapter.dispose(); |         if (playQueueAdapter != null) playQueueAdapter.dispose(); | ||||||
| @@ -272,7 +274,6 @@ public abstract class BasePlayer implements | |||||||
|     public void destroy() { |     public void destroy() { | ||||||
|         if (DEBUG) Log.d(TAG, "destroy() called"); |         if (DEBUG) Log.d(TAG, "destroy() called"); | ||||||
|         destroyPlayer(); |         destroyPlayer(); | ||||||
|         clearThumbnailCache(); |  | ||||||
|         unregisterBroadcastReceiver(); |         unregisterBroadcastReceiver(); | ||||||
|  |  | ||||||
|         trackSelector = null; |         trackSelector = null; | ||||||
| @@ -314,11 +315,6 @@ public abstract class BasePlayer implements | |||||||
|         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + |         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + | ||||||
|                 "imageUri = [" + imageUri + "], view = [" + view + "]"); |                 "imageUri = [" + imageUri + "], view = [" + view + "]"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void clearThumbnailCache() { |  | ||||||
|         ImageLoader.getInstance().clearMemoryCache(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // MediaSource Building |     // MediaSource Building | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -448,7 +444,6 @@ public abstract class BasePlayer implements | |||||||
|     public void onPlaying() { |     public void onPlaying() { | ||||||
|         if (DEBUG) Log.d(TAG, "onPlaying() called"); |         if (DEBUG) Log.d(TAG, "onPlaying() called"); | ||||||
|         if (!isProgressLoopRunning()) startProgressLoop(); |         if (!isProgressLoopRunning()) startProgressLoop(); | ||||||
|         if (!isCurrentWindowValid()) seekToDefault(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onBuffering() {} |     public void onBuffering() {} | ||||||
| @@ -522,11 +517,9 @@ public abstract class BasePlayer implements | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     private Disposable getProgressReactor() { |     private Disposable getProgressReactor() { | ||||||
|         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) |         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .filter(ignored -> isProgressLoopRunning()) |  | ||||||
|                 .subscribe(ignored -> triggerProgressUpdate()); |                 .subscribe(ignored -> triggerProgressUpdate()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -541,16 +534,19 @@ public abstract class BasePlayer implements | |||||||
|                 (manifest == null ? "no manifest" : "available manifest") + ", " + |                 (manifest == null ? "no manifest" : "available manifest") + ", " + | ||||||
|                 "timeline size = [" + timeline.getWindowCount() + "], " + |                 "timeline size = [" + timeline.getWindowCount() + "], " + | ||||||
|                 "reason = [" + reason + "]"); |                 "reason = [" + reason + "]"); | ||||||
|  |         if (playQueue == null) return; | ||||||
|  |  | ||||||
|         switch (reason) { |         switch (reason) { | ||||||
|             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block |             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block | ||||||
|             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock |             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock | ||||||
|             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes |             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes | ||||||
|                 if (playQueue != null && playbackManager != null && |  | ||||||
|                 // ensures MediaSourceManager#update is complete |                 // ensures MediaSourceManager#update is complete | ||||||
|                         timeline.getWindowCount() == playQueue.size()) { |                 final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); | ||||||
|                     playbackManager.load(); |                 // 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 |     // 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 |     @Override | ||||||
|     public void onPlaybackBlock() { |     public void onPlaybackBlock() { | ||||||
|         if (simpleExoPlayer == null) return; |         if (simpleExoPlayer == null) return; | ||||||
| @@ -796,7 +802,6 @@ public abstract class BasePlayer implements | |||||||
|         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); |         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); | ||||||
|  |  | ||||||
|         simpleExoPlayer.prepare(mediaSource); |         simpleExoPlayer.prepare(mediaSource); | ||||||
|         seekToDefault(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -825,16 +830,24 @@ public abstract class BasePlayer implements | |||||||
|  |  | ||||||
|         if (simpleExoPlayer == null) return; |         if (simpleExoPlayer == null) return; | ||||||
|         final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); |         final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); | ||||||
|  |         final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); | ||||||
|         // Check if on wrong window |         // Check if on wrong window | ||||||
|         if (currentPlayQueueIndex != playQueue.getIndex()) { |         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 + "], " + |                     "index=[" + currentPlayQueueIndex + "], " + | ||||||
|                     "queue index=[" + playQueue.getIndex() + "]"); |                     "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()) { |         } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { | ||||||
|             final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; |             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 + "]," + |                     " window=[" + currentPlayQueueIndex + "]," + | ||||||
|                     " at=[" + getTimeString((int)startPos) + "]," + |                     " at=[" + getTimeString((int)startPos) + "]," + | ||||||
|                     " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); |                     " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); | ||||||
| @@ -858,6 +871,11 @@ public abstract class BasePlayer implements | |||||||
|     @Nullable |     @Nullable | ||||||
|     @Override |     @Override | ||||||
|     public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { |     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()) { |         if (!info.getHlsUrl().isEmpty()) { | ||||||
|             return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); |             return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); | ||||||
|         } else if (!info.getDashMpdUrl().isEmpty()) { |         } 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 (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); | ||||||
|         if (playWhenReady) audioReactor.requestAudioFocus(); |         if (playWhenReady) audioReactor.requestAudioFocus(); | ||||||
|         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); |         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); | ||||||
|  |  | ||||||
|  |         // On live prepared | ||||||
|  |         if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onVideoPlayPause() { |     public void onVideoPlayPause() { | ||||||
| @@ -945,14 +966,15 @@ public abstract class BasePlayer implements | |||||||
|         if (simpleExoPlayer == null || playQueue == null) return; |         if (simpleExoPlayer == null || playQueue == null) return; | ||||||
|         if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); |         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 | ||||||
|         /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. |         * is the first in a queue.*/ | ||||||
|         * Also restart the track if the current track is the first in a queue.*/ |         if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || | ||||||
|         if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { |                 playQueue.getIndex() == 0) { | ||||||
|             final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); |             seekToDefault(); | ||||||
|             simpleExoPlayer.seekTo(startPos); |             playQueue.offsetIndex(0); | ||||||
|         } else { |         } else { | ||||||
|  |             savePlaybackState(); | ||||||
|             playQueue.offsetIndex(-1); |             playQueue.offsetIndex(-1); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -962,7 +984,6 @@ public abstract class BasePlayer implements | |||||||
|         if (DEBUG) Log.d(TAG, "onPlayNext() called"); |         if (DEBUG) Log.d(TAG, "onPlayNext() called"); | ||||||
|  |  | ||||||
|         savePlaybackState(); |         savePlaybackState(); | ||||||
|  |  | ||||||
|         playQueue.offsetIndex(+1); |         playQueue.offsetIndex(+1); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -975,8 +996,9 @@ public abstract class BasePlayer implements | |||||||
|         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { |         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { | ||||||
|             seekToDefault(); |             seekToDefault(); | ||||||
|         } else { |         } else { | ||||||
|             playQueue.setIndex(index); |             savePlaybackState(); | ||||||
|         } |         } | ||||||
|  |         playQueue.setIndex(index); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void seekBy(int milliSeconds) { |     public void seekBy(int milliSeconds) { | ||||||
| @@ -1015,8 +1037,11 @@ public abstract class BasePlayer implements | |||||||
|  |  | ||||||
|     protected void reload() { |     protected void reload() { | ||||||
|         if (playbackManager != null) { |         if (playbackManager != null) { | ||||||
|             playbackManager.reset(); |             playbackManager.dispose(); | ||||||
|             playbackManager.load(); |         } | ||||||
|  |  | ||||||
|  |         if (playQueue != null) { | ||||||
|  |             playbackManager = new MediaSourceManager(this, playQueue); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -62,6 +62,7 @@ import org.schabi.newpipe.playlist.PlayQueue; | |||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemHolder; | import org.schabi.newpipe.playlist.PlayQueueItemHolder; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; | ||||||
| import org.schabi.newpipe.util.AnimationUtils; | import org.schabi.newpipe.util.AnimationUtils; | ||||||
| import org.schabi.newpipe.util.ListHelper; | import org.schabi.newpipe.util.ListHelper; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | 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.BasePlayer.STATE_PLAYING; | ||||||
| import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; | 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.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.AnimationUtils.animateView; | ||||||
| import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; | 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); |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); | ||||||
|         setVolumeControlStream(AudioManager.STREAM_MUSIC); |         setVolumeControlStream(AudioManager.STREAM_MUSIC); | ||||||
|  |  | ||||||
|         changeSystemUi(); |         hideSystemUi(); | ||||||
|         setContentView(R.layout.activity_main_player); |         setContentView(R.layout.activity_main_player); | ||||||
|         playerImpl = new VideoPlayerImpl(this); |         playerImpl = new VideoPlayerImpl(this); | ||||||
|         playerImpl.setup(findViewById(android.R.id.content)); |         playerImpl.setup(findViewById(android.R.id.content)); | ||||||
| @@ -597,28 +600,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|             updatePlaybackButtons(); |             updatePlaybackButtons(); | ||||||
|  |  | ||||||
|             getControlsRoot().setVisibility(View.INVISIBLE); |             getControlsRoot().setVisibility(View.INVISIBLE); | ||||||
|             queueLayout.setVisibility(View.VISIBLE); |             animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true, | ||||||
|  |                     DEFAULT_CONTROLS_DURATION); | ||||||
|  |  | ||||||
|             itemsList.scrollToPosition(playQueue.getIndex()); |             itemsList.scrollToPosition(playQueue.getIndex()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private void onQueueClosed() { |         private void onQueueClosed() { | ||||||
|             queueLayout.setVisibility(View.GONE); |             animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false, | ||||||
|  |                     DEFAULT_CONTROLS_DURATION); | ||||||
|             queueVisible = false; |             queueVisible = false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private void onMoreOptionsClicked() { |         private void onMoreOptionsClicked() { | ||||||
|             if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); |             if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); | ||||||
|  |  | ||||||
|             if (secondaryControls.getVisibility() == View.VISIBLE) { |             final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; | ||||||
|                 moreOptionsButton.setImageDrawable(getResources().getDrawable( |  | ||||||
|                         R.drawable.ic_expand_more_white_24dp)); |             animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, | ||||||
|                 animateView(secondaryControls, false, 200); |                     isMoreControlsVisible ? 0 : 180); | ||||||
|             } else { |             animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, | ||||||
|                 moreOptionsButton.setImageDrawable(getResources().getDrawable( |                     DEFAULT_CONTROLS_DURATION); | ||||||
|                         R.drawable.ic_expand_less_white_24dp)); |  | ||||||
|                 animateView(secondaryControls, true, 200); |  | ||||||
|             } |  | ||||||
|             showControls(DEFAULT_CONTROLS_DURATION); |             showControls(DEFAULT_CONTROLS_DURATION); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -696,7 +698,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|                 animatePlayButtons(true, 200); |                 animatePlayButtons(true, 200); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             changeSystemUi(); |  | ||||||
|             getRootView().setKeepScreenOn(true); |             getRootView().setKeepScreenOn(true); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -798,31 +799,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         private ItemTouchHelper.SimpleCallback getItemTouchCallback() { |         private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||||
|             return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { |             return new PlayQueueItemTouchCallback() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { |                 public void onMove(int sourceIndex, int targetIndex) { | ||||||
|                     if (source.getItemViewType() != target.getItemViewType()) { |                     if (playQueue != null) playQueue.move(sourceIndex, targetIndex); | ||||||
|                         return false; |  | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                     final int sourceIndex = source.getLayoutPosition(); |  | ||||||
|                     final int targetIndex = target.getLayoutPosition(); |  | ||||||
|                     playQueue.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) {} |  | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ import org.schabi.newpipe.player.event.PlayerEventListener; | |||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemHolder; | import org.schabi.newpipe.playlist.PlayQueueItemHolder; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | 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 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 View rootView; | ||||||
|  |  | ||||||
|     private RecyclerView itemsList; |     private RecyclerView itemsList; | ||||||
| @@ -398,43 +396,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { |     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||||
|         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { |         return new PlayQueueItemTouchCallback() { | ||||||
|             @Override |             @Override | ||||||
|             public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, |             public void onMove(int sourceIndex, int targetIndex) { | ||||||
|                                                     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(); |  | ||||||
|                 if (player != null) player.getPlayQueue().move(sourceIndex, 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) {} |  | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent; | |||||||
| import org.schabi.newpipe.playlist.events.PlayQueueEvent; | import org.schabi.newpipe.playlist.events.PlayQueueEvent; | ||||||
| import org.schabi.newpipe.playlist.events.RemoveEvent; | import org.schabi.newpipe.playlist.events.RemoveEvent; | ||||||
| import org.schabi.newpipe.playlist.events.ReorderEvent; | import org.schabi.newpipe.playlist.events.ReorderEvent; | ||||||
|  | import org.schabi.newpipe.util.ServiceHelper; | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.List; |  | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
|  | import io.reactivex.Observable; | ||||||
| import io.reactivex.Single; | import io.reactivex.Single; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
| @@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject; | |||||||
| import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | ||||||
|  |  | ||||||
| public class MediaSourceManager { | 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. |      * 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; |     @NonNull private final PlayQueue playQueue; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing |      * Determines the gap time between the playback position and the playback duration which | ||||||
|      * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure |      * the {@link #getEdgeIntervalSignal()} begins to request loading. | ||||||
|      * the {@link StreamInfo} used in subsequent playback is up-to-date. |  | ||||||
|      * <br><br> |  | ||||||
|      * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to |  | ||||||
|      * replace the expired one on whereupon {@link #loadImmediate()} is called. |  | ||||||
|      * |      * | ||||||
|      * @see #loadImmediate() |      * @see #progressUpdateIntervalMillis | ||||||
|      * @see #isCorrectionNeeded(PlayQueueItem) |  | ||||||
|      * */ |      * */ | ||||||
|     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<Long> nearEndIntervalSignal; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Process only the last load order when receiving a stream of load orders (lessens I/O). |      * 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, |     public MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                               @NonNull final PlayQueue playQueue) { |                               @NonNull final PlayQueue playQueue) { | ||||||
|         this(listener, playQueue, |         this(listener, playQueue, /*loadDebounceMillis=*/400L, | ||||||
|                 /*loadDebounceMillis=*/400L, |                 /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS), | ||||||
|                 /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); |                 /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private MediaSourceManager(@NonNull final PlaybackListener listener, |     private MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                                @NonNull final PlayQueue playQueue, |                                @NonNull final PlayQueue playQueue, | ||||||
|                                final long loadDebounceMillis, |                                final long loadDebounceMillis, | ||||||
|                                final long windowRefreshTimeMillis) { |                                final long playbackNearEndGapMillis, | ||||||
|  |                                final long progressUpdateIntervalMillis) { | ||||||
|         if (playQueue.getBroadcastReceiver() == null) { |         if (playQueue.getBroadcastReceiver() == null) { | ||||||
|             throw new IllegalArgumentException("Play Queue has not been initialized."); |             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.playbackListener = listener; | ||||||
|         this.playQueue = playQueue; |         this.playQueue = playQueue; | ||||||
|  |  | ||||||
|         this.windowRefreshTimeMillis = windowRefreshTimeMillis; |         this.playbackNearEndGapMillis = playbackNearEndGapMillis; | ||||||
|  |         this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; | ||||||
|  |         this.nearEndIntervalSignal = getEdgeIntervalSignal(); | ||||||
|  |  | ||||||
|         this.loadDebounceMillis = loadDebounceMillis; |         this.loadDebounceMillis = loadDebounceMillis; | ||||||
|         this.debouncedSignal = PublishSubject.create(); |         this.debouncedSignal = PublishSubject.create(); | ||||||
| @@ -161,28 +170,6 @@ public class MediaSourceManager { | |||||||
|         sources.releaseSource(); |         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 |     // Event Reactor | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -219,11 +206,13 @@ public class MediaSourceManager { | |||||||
|         switch (event.type()) { |         switch (event.type()) { | ||||||
|             case INIT: |             case INIT: | ||||||
|             case ERROR: |             case ERROR: | ||||||
|                 reset(); |                 maybeBlock(); | ||||||
|                 break; |  | ||||||
|             case APPEND: |             case APPEND: | ||||||
|                 populateSources(); |                 populateSources(); | ||||||
|                 break; |                 break; | ||||||
|  |             case SELECT: | ||||||
|  |                 maybeRenewCurrentIndex(); | ||||||
|  |                 break; | ||||||
|             case REMOVE: |             case REMOVE: | ||||||
|                 final RemoveEvent removeEvent = (RemoveEvent) event; |                 final RemoveEvent removeEvent = (RemoveEvent) event; | ||||||
|                 remove(removeEvent.getRemoveIndex()); |                 remove(removeEvent.getRemoveIndex()); | ||||||
| @@ -238,7 +227,6 @@ public class MediaSourceManager { | |||||||
|                 final ReorderEvent reorderEvent = (ReorderEvent) event; |                 final ReorderEvent reorderEvent = (ReorderEvent) event; | ||||||
|                 move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); |                 move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); | ||||||
|                 break; |                 break; | ||||||
|             case SELECT: |  | ||||||
|             case RECOVERY: |             case RECOVERY: | ||||||
|             default: |             default: | ||||||
|                 break; |                 break; | ||||||
| @@ -347,8 +335,13 @@ public class MediaSourceManager { | |||||||
|     // MediaSource Loading |     // MediaSource Loading | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private Observable<Long> getEdgeIntervalSignal() { | ||||||
|  |         return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) | ||||||
|  |                 .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private Disposable getDebouncedLoader() { |     private Disposable getDebouncedLoader() { | ||||||
|         return debouncedSignal |         return debouncedSignal.mergeWith(nearEndIntervalSignal) | ||||||
|                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) |                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(timestamp -> loadImmediate()); |                 .subscribe(timestamp -> loadImmediate()); | ||||||
| @@ -359,13 +352,14 @@ public class MediaSourceManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void loadImmediate() { |     private void loadImmediate() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); | ||||||
|         // The current item has higher priority |         // The current item has higher priority | ||||||
|         final int currentIndex = playQueue.getIndex(); |         final int currentIndex = playQueue.getIndex(); | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); |         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); | ||||||
|         if (currentItem == null) return; |         if (currentItem == null) return; | ||||||
|  |  | ||||||
|         // Evict the items being loaded to free up memory |         // 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(); |             loaderReactor.clear(); | ||||||
|             loadingItems.clear(); |             loadingItems.clear(); | ||||||
|         } |         } | ||||||
| @@ -377,7 +371,7 @@ public class MediaSourceManager { | |||||||
|         final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); |         final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); | ||||||
|         final int rightLimit = currentIndex + WINDOW_SIZE + 1; |         final int rightLimit = currentIndex + WINDOW_SIZE + 1; | ||||||
|         final int rightBound = Math.min(playQueue.size(), rightLimit); |         final int rightBound = Math.min(playQueue.size(), rightLimit); | ||||||
|         final List<PlayQueueItem> items = new ArrayList<>( |         final Set<PlayQueueItem> items = new HashSet<>( | ||||||
|                 playQueue.getStreams().subList(leftBound,rightBound)); |                 playQueue.getStreams().subList(leftBound,rightBound)); | ||||||
|  |  | ||||||
|         // Do a round robin |         // Do a round robin | ||||||
| @@ -385,6 +379,7 @@ public class MediaSourceManager { | |||||||
|         if (excess >= 0) { |         if (excess >= 0) { | ||||||
|             items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); |             items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); | ||||||
|         } |         } | ||||||
|  |         items.remove(currentItem); | ||||||
|  |  | ||||||
|         for (final PlayQueueItem item : items) { |         for (final PlayQueueItem item : items) { | ||||||
|             maybeLoadItem(item); |             maybeLoadItem(item); | ||||||
| @@ -405,10 +400,10 @@ public class MediaSourceManager { | |||||||
|                     /* No exception handling since getLoadedMediaSource guarantees nonnull return */ |                     /* No exception handling since getLoadedMediaSource guarantees nonnull return */ | ||||||
|                     .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); |                     .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); | ||||||
|             loaderReactor.add(loader); |             loaderReactor.add(loader); | ||||||
|         } |         } else { | ||||||
|  |  | ||||||
|             maybeSynchronizePlayer(); |             maybeSynchronizePlayer(); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { |     private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { | ||||||
|         return stream.getStream().map(streamInfo -> { |         return stream.getStream().map(streamInfo -> { | ||||||
| @@ -423,7 +418,8 @@ public class MediaSourceManager { | |||||||
|                 return new FailedMediaSource(stream, exception); |                 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); |             return new LoadedMediaSource(source, stream, expiration); | ||||||
|         }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); |         }).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 |     // MediaSource Playlist Helpers | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -476,6 +490,7 @@ public class MediaSourceManager { | |||||||
|  |  | ||||||
|         this.sources.releaseSource(); |         this.sources.releaseSource(); | ||||||
|         this.sources = new DynamicConcatenatingMediaSource(false, |         this.sources = new DynamicConcatenatingMediaSource(false, | ||||||
|  |                 // Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order | ||||||
|                 new ShuffleOrder.UnshuffledShuffleOrder(0)); |                 new ShuffleOrder.UnshuffledShuffleOrder(0)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem; | |||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| public interface PlaybackListener { | 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. |      * 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 |      * Signals to the listener to block the player from playing anything and notify the source | ||||||
|   | |||||||
| @@ -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) {} | ||||||
|  | } | ||||||
| @@ -1,12 +1,35 @@ | |||||||
| package org.schabi.newpipe.settings; | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
| import android.os.Bundle; | 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.R; | ||||||
|  | import org.schabi.newpipe.util.InfoCache; | ||||||
|  |  | ||||||
| public class HistorySettingsFragment extends BasePreferenceFragment { | 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 |     @Override | ||||||
|     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { | ||||||
|         addPreferencesFromResource(R.xml.history_settings); |         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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -43,7 +43,6 @@ public final class InfoCache { | |||||||
|      * Trim the cache to this size |      * Trim the cache to this size | ||||||
|      */ |      */ | ||||||
|     private static final int TRIM_CACHE_TO = 30; |     private static final int TRIM_CACHE_TO = 30; | ||||||
|     private static final int DEFAULT_TIMEOUT_HOURS = 4; |  | ||||||
|  |  | ||||||
|     private static final LruCache<String, CacheData> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); |     private static final LruCache<String, CacheData> 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) { |     public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { | ||||||
|         if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); |         if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); | ||||||
|  |  | ||||||
|         final long expirationMillis; |         final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); | ||||||
|         if (info.getServiceId() == SoundCloud.getServiceId()) { |  | ||||||
|             expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); |  | ||||||
|         } else { |  | ||||||
|             expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         synchronized (lruCache) { |         synchronized (lruCache) { | ||||||
|             final CacheData data = new CacheData(info, expirationMillis); |             final CacheData data = new CacheData(info, expirationMillis); | ||||||
|             lruCache.put(keyOf(serviceId, url), data); |             lruCache.put(keyOf(serviceId, url), data); | ||||||
|   | |||||||
| @@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList; | |||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  |  | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; | ||||||
|  |  | ||||||
| public class ServiceHelper { | public class ServiceHelper { | ||||||
|     private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; |     private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; | ||||||
|  |  | ||||||
| @@ -98,4 +102,12 @@ public class ServiceHelper { | |||||||
|         PreferenceManager.getDefaultSharedPreferences(context).edit(). |         PreferenceManager.getDefaultSharedPreferences(context).edit(). | ||||||
|                 putString(context.getString(R.string.current_service_key), serviceName).apply(); |                 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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -301,9 +301,13 @@ | |||||||
|             android:id="@+id/live_sync" |             android:id="@+id/live_sync" | ||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|  |             android:paddingLeft="4dp" | ||||||
|  |             android:paddingRight="4dp" | ||||||
|             android:gravity="center" |             android:gravity="center" | ||||||
|             android:text="@string/live_sync" |             android:text="@string/duration_live" | ||||||
|  |             android:textAllCaps="true" | ||||||
|             android:textColor="?attr/colorAccent" |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:maxLength="4" | ||||||
|             android:background="?attr/selectableItemBackground" |             android:background="?attr/selectableItemBackground" | ||||||
|             android:visibility="gone"/> |             android:visibility="gone"/> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|   | |||||||
| @@ -308,7 +308,7 @@ | |||||||
|                     android:id="@+id/toggleOrientation" |                     android:id="@+id/toggleOrientation" | ||||||
|                     android:layout_width="30dp" |                     android:layout_width="30dp" | ||||||
|                     android:layout_height="30dp" |                     android:layout_height="30dp" | ||||||
|                     android:layout_marginLeft="2dp" |                     android:layout_marginLeft="4dp" | ||||||
|                     android:layout_marginRight="2dp" |                     android:layout_marginRight="2dp" | ||||||
|                     android:layout_alignParentRight="true" |                     android:layout_alignParentRight="true" | ||||||
|                     android:layout_centerVertical="true" |                     android:layout_centerVertical="true" | ||||||
| @@ -325,8 +325,8 @@ | |||||||
|                     android:id="@+id/switchPopup" |                     android:id="@+id/switchPopup" | ||||||
|                     android:layout_width="30dp" |                     android:layout_width="30dp" | ||||||
|                     android:layout_height="30dp" |                     android:layout_height="30dp" | ||||||
|                     android:layout_marginLeft="2dp" |                     android:layout_marginLeft="4dp" | ||||||
|                     android:layout_marginRight="2dp" |                     android:layout_marginRight="4dp" | ||||||
|                     android:layout_toLeftOf="@id/toggleOrientation" |                     android:layout_toLeftOf="@id/toggleOrientation" | ||||||
|                     android:layout_centerVertical="true" |                     android:layout_centerVertical="true" | ||||||
|                     android:clickable="true" |                     android:clickable="true" | ||||||
| @@ -341,8 +341,8 @@ | |||||||
|                     android:id="@+id/switchBackground" |                     android:id="@+id/switchBackground" | ||||||
|                     android:layout_width="30dp" |                     android:layout_width="30dp" | ||||||
|                     android:layout_height="30dp" |                     android:layout_height="30dp" | ||||||
|                     android:layout_marginLeft="2dp" |                     android:layout_marginLeft="4dp" | ||||||
|                     android:layout_marginRight="2dp" |                     android:layout_marginRight="4dp" | ||||||
|                     android:layout_toLeftOf="@id/switchPopup" |                     android:layout_toLeftOf="@id/switchPopup" | ||||||
|                     android:layout_centerVertical="true" |                     android:layout_centerVertical="true" | ||||||
|                     android:clickable="true" |                     android:clickable="true" | ||||||
| @@ -403,9 +403,13 @@ | |||||||
|                     android:id="@+id/playbackLiveSync" |                     android:id="@+id/playbackLiveSync" | ||||||
|                     android:layout_width="wrap_content" |                     android:layout_width="wrap_content" | ||||||
|                     android:layout_height="match_parent" |                     android:layout_height="match_parent" | ||||||
|  |                     android:paddingLeft="4dp" | ||||||
|  |                     android:paddingRight="4dp" | ||||||
|                     android:gravity="center" |                     android:gravity="center" | ||||||
|                     android:text="@string/live_sync" |                     android:text="@string/duration_live" | ||||||
|  |                     android:textAllCaps="true" | ||||||
|                     android:textColor="@android:color/white" |                     android:textColor="@android:color/white" | ||||||
|  |                     android:maxLength="4" | ||||||
|                     android:visibility="gone" |                     android:visibility="gone" | ||||||
|                     android:background="?attr/selectableItemBackground" |                     android:background="?attr/selectableItemBackground" | ||||||
|                     tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> |                     tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> | ||||||
|   | |||||||
| @@ -151,9 +151,13 @@ | |||||||
|             android:id="@+id/live_sync" |             android:id="@+id/live_sync" | ||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|  |             android:paddingLeft="4dp" | ||||||
|  |             android:paddingRight="4dp" | ||||||
|             android:gravity="center" |             android:gravity="center" | ||||||
|             android:text="@string/live_sync" |             android:text="@string/duration_live" | ||||||
|  |             android:textAllCaps="true" | ||||||
|             android:textColor="?attr/colorAccent" |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:maxLength="4" | ||||||
|             android:background="?attr/selectableItemBackground" |             android:background="?attr/selectableItemBackground" | ||||||
|             android:visibility="gone"/> |             android:visibility="gone"/> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|   | |||||||
| @@ -195,9 +195,13 @@ | |||||||
|                 android:id="@+id/playbackLiveSync" |                 android:id="@+id/playbackLiveSync" | ||||||
|                 android:layout_width="wrap_content" |                 android:layout_width="wrap_content" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|  |                 android:paddingLeft="4dp" | ||||||
|  |                 android:paddingRight="4dp" | ||||||
|                 android:gravity="center_vertical" |                 android:gravity="center_vertical" | ||||||
|                 android:text="@string/live_sync" |                 android:text="@string/duration_live" | ||||||
|  |                 android:textAllCaps="true" | ||||||
|                 android:textColor="@android:color/white" |                 android:textColor="@android:color/white" | ||||||
|  |                 android:maxLength="4" | ||||||
|                 android:visibility="gone" |                 android:visibility="gone" | ||||||
|                 android:background="?attr/selectableItemBackground" |                 android:background="?attr/selectableItemBackground" | ||||||
|                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> |                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> | ||||||
|   | |||||||
| @@ -160,6 +160,10 @@ | |||||||
|     <string name="import_data">import_data</string> |     <string name="import_data">import_data</string> | ||||||
|     <string name="export_data">export_data</string> |     <string name="export_data">export_data</string> | ||||||
|  |  | ||||||
|  |     <string name="download_thumbnail_key" translatable="false">download_thumbnail_key</string> | ||||||
|  |  | ||||||
|  |     <string name="metadata_cache_wipe_key" translatable="false">cache_wipe_key</string> | ||||||
|  |  | ||||||
|     <!-- FileName Downloads  --> |     <!-- FileName Downloads  --> | ||||||
|     <string name="settings_file_charset_key" translatable="false">file_rename</string> |     <string name="settings_file_charset_key" translatable="false">file_rename</string> | ||||||
|     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> |     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> | ||||||
|   | |||||||
| @@ -74,6 +74,11 @@ | |||||||
|     <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> |     <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> | ||||||
|     <string name="use_inexact_seek_title">Use fast inexact seek</string> |     <string name="use_inexact_seek_title">Use fast inexact seek</string> | ||||||
|     <string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string> |     <string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string> | ||||||
|  |     <string name="download_thumbnail_title">Load thumbnails</string> | ||||||
|  |     <string name="download_thumbnail_summary">Disable to stop all non-cached thumbnail from loading and save on data and memory usage</string> | ||||||
|  |     <string name="metadata_cache_wipe_title">Wipe cached metadata</string> | ||||||
|  |     <string name="metadata_cache_wipe_summary">Remove all cached webpage data</string> | ||||||
|  |     <string name="metadata_cache_wipe_complete_notice">Metadata cache wiped</string> | ||||||
|     <string name="auto_queue_title">Auto-queue next stream</string> |     <string name="auto_queue_title">Auto-queue next stream</string> | ||||||
|     <string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string> |     <string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string> | ||||||
|     <string name="player_gesture_controls_title">Player gesture controls</string> |     <string name="player_gesture_controls_title">Player gesture controls</string> | ||||||
| @@ -89,7 +94,7 @@ | |||||||
|     <string name="download_dialog_title">Download</string> |     <string name="download_dialog_title">Download</string> | ||||||
|     <string name="next_video_title">Next video</string> |     <string name="next_video_title">Next video</string> | ||||||
|     <string name="show_next_and_similar_title">Show next and similar videos</string> |     <string name="show_next_and_similar_title">Show next and similar videos</string> | ||||||
|     <string name="show_hold_to_append_title">Show Hold to Append Tip</string> |     <string name="show_hold_to_append_title">Show hold to append tip</string> | ||||||
|     <string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string> |     <string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string> | ||||||
|     <string name="url_not_supported_toast">URL not supported</string> |     <string name="url_not_supported_toast">URL not supported</string> | ||||||
|     <string name="default_content_country_title">Default content country</string> |     <string name="default_content_country_title">Default content country</string> | ||||||
| @@ -98,7 +103,7 @@ | |||||||
|     <string name="settings_category_player_title">Player</string> |     <string name="settings_category_player_title">Player</string> | ||||||
|     <string name="settings_category_player_behavior_title">Behavior</string> |     <string name="settings_category_player_behavior_title">Behavior</string> | ||||||
|     <string name="settings_category_video_audio_title">Video & Audio</string> |     <string name="settings_category_video_audio_title">Video & Audio</string> | ||||||
|     <string name="settings_category_history_title">History</string> |     <string name="settings_category_history_title">History & Cache</string> | ||||||
|     <string name="settings_category_popup_title">Popup</string> |     <string name="settings_category_popup_title">Popup</string> | ||||||
|     <string name="settings_category_appearance_title">Appearance</string> |     <string name="settings_category_appearance_title">Appearance</string> | ||||||
|     <string name="settings_category_other_title">Other</string> |     <string name="settings_category_other_title">Other</string> | ||||||
| @@ -418,18 +423,16 @@ | |||||||
|     <string name="resize_zoom">ZOOM</string> |     <string name="resize_zoom">ZOOM</string> | ||||||
|  |  | ||||||
|     <string name="caption_auto_generated">Auto-generated</string> |     <string name="caption_auto_generated">Auto-generated</string> | ||||||
|     <string name="caption_font_size_settings_title">Caption Font Size</string> |     <string name="caption_font_size_settings_title">Caption font size</string> | ||||||
|     <string name="smaller_caption_font_size">Smaller Font</string> |     <string name="smaller_caption_font_size">Smaller font</string> | ||||||
|     <string name="normal_caption_font_size">Normal Font</string> |     <string name="normal_caption_font_size">Normal font</string> | ||||||
|     <string name="larger_caption_font_size">Larger Font</string> |     <string name="larger_caption_font_size">Larger font</string> | ||||||
|  |  | ||||||
|     <string name="live_sync">SYNC</string> |  | ||||||
|  |  | ||||||
|     <!-- Debug Settings --> |     <!-- Debug Settings --> | ||||||
|     <string name="enable_leak_canary_title">Enable LeakCanary</string> |     <string name="enable_leak_canary_title">Enable LeakCanary</string> | ||||||
|     <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> |     <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> | ||||||
|  |  | ||||||
|     <string name="enable_disposed_exceptions_title">Report Out-of-Lifecycle Errors</string> |     <string name="enable_disposed_exceptions_title">Report Out-of-lifecycle errors</string> | ||||||
|     <string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string> |     <string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string> | ||||||
|  |  | ||||||
|     <!-- Subscriptions import/export --> |     <!-- Subscriptions import/export --> | ||||||
|   | |||||||
| @@ -37,6 +37,12 @@ | |||||||
|         android:summary="@string/auto_queue_summary" |         android:summary="@string/auto_queue_summary" | ||||||
|         android:title="@string/auto_queue_title"/> |         android:title="@string/auto_queue_title"/> | ||||||
|  |  | ||||||
|  |     <SwitchPreference | ||||||
|  |         android:defaultValue="true" | ||||||
|  |         android:key="@string/download_thumbnail_key" | ||||||
|  |         android:title="@string/download_thumbnail_title" | ||||||
|  |         android:summary="@string/download_thumbnail_summary"/> | ||||||
|  |  | ||||||
|     <ListPreference |     <ListPreference | ||||||
|         android:defaultValue="@string/kiosk_page_key" |         android:defaultValue="@string/kiosk_page_key" | ||||||
|         android:entries="@array/main_page_content_names" |         android:entries="@array/main_page_content_names" | ||||||
|   | |||||||
| @@ -16,4 +16,9 @@ | |||||||
|         android:summary="@string/enable_search_history_summary" |         android:summary="@string/enable_search_history_summary" | ||||||
|         android:title="@string/enable_search_history_title"/> |         android:title="@string/enable_search_history_title"/> | ||||||
|  |  | ||||||
|  |     <Preference | ||||||
|  |         android:key="@string/metadata_cache_wipe_key" | ||||||
|  |         android:summary="@string/metadata_cache_wipe_summary" | ||||||
|  |         android:title="@string/metadata_cache_wipe_title"/> | ||||||
|  |  | ||||||
| </PreferenceScreen> | </PreferenceScreen> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 John Zhen Mo
					John Zhen Mo