-[#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:
parent
1e57b5ea49
commit
61b422502b
18 changed files with 305 additions and 179 deletions
|
@ -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()) {
|
||||||
return downloader.stream(imageUri);
|
final Downloader downloader = (Downloader) NewPipe.getDownloader();
|
||||||
|
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
|
final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size();
|
||||||
timeline.getWindowCount() == playQueue.size()) {
|
// Ensure dynamic/livestream timeline changes does not cause negative position
|
||||||
playbackManager.load();
|
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,9 +400,9 @@ 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) {
|
||||||
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue