diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 847153e122a..99d7b45750d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -31,6 +31,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO> getAll(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index bbd1a315d39..b4f4b942a47 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -38,6 +38,7 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; @@ -49,6 +50,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; @@ -71,6 +74,7 @@ import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -95,6 +99,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Single; @@ -134,8 +139,10 @@ public class VideoDetailFragment private StreamInfo currentInfo; private Disposable currentWorker; + @NonNull private CompositeDisposable disposables = new CompositeDisposable(); + private HistoryRecordManager recordManager; private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -153,6 +160,7 @@ public class VideoDetailFragment private View thumbnailBackgroundButton; private ImageView thumbnailImageView; private ImageView thumbnailPlayButton; + private ProgressBar positionView; private View videoTitleRoot; private TextView videoTitleTextView; @@ -165,6 +173,7 @@ public class VideoDetailFragment private TextView detailControlsDownload; private TextView appendControlsDetail; private TextView detailDurationView; + private TextView detailPositionView; private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; @@ -259,6 +268,8 @@ public void onResume() { // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { selectAndLoadVideo(serviceId, url, name); + } else if (currentInfo != null) { + updatePositionInfo(currentInfo); } } @@ -289,10 +300,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK) { NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name); - } else Log.e(TAG, "ReCaptcha failed"); + } else if (DEBUG) Log.e(TAG, "ReCaptcha failed"); break; default: - Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); + if (DEBUG) Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); break; } } @@ -385,7 +396,7 @@ public void onClick(View v) { break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { - Log.w(TAG, "Can't open channel because we got no channel URL"); + if (DEBUG) Log.w(TAG, "Can't open channel because we got no channel URL"); } else { try { NavigationHelper.openChannelFragment( @@ -455,6 +466,7 @@ protected void initViews(View rootView, Bundle savedInstanceState) { thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view); thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button); + positionView = rootView.findViewById(R.id.position_view); contentRootLayoutHiding = rootView.findViewById(R.id.detail_content_root_hiding); @@ -469,6 +481,7 @@ protected void initViews(View rootView, Bundle savedInstanceState) { detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); detailDurationView = rootView.findViewById(R.id.detail_duration_view); + detailPositionView = rootView.findViewById(R.id.detail_position_view); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); @@ -672,8 +685,10 @@ private static void showInstallKoreDialog(final Context context) { } private void setupActionBarOnError(final String url) { - if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]"); - Log.e("-----", "missing code"); + if (DEBUG) { + Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]"); + Log.e("-----", "missing code"); + } } private void setupActionBar(final StreamInfo info) { @@ -724,10 +739,10 @@ public void pushToStack(int serviceId, String videoUrl, String name) { if (stack.size() > 0 && stack.peek().getServiceId() == serviceId && stack.peek().getUrl().equals(videoUrl)) { - Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + if (DEBUG) Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); return; - } else { + } else if (DEBUG) { Log.d(TAG, "pushToStack() wasn't equal"); } @@ -944,11 +959,15 @@ private void startOnExternalPlayer(@NonNull final Context context, NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(), currentInfo.getUploaderName(), selectedStream); - final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); + if (recordManager == null) { + recordManager = new HistoryRecordManager(requireContext()); + } disposables.add(recordManager.onViewed(info).onErrorComplete() .subscribe( ignored -> {/* successful */}, - error -> Log.e(TAG, "Register view failure: ", error) + error -> { + if (DEBUG) Log.e(TAG, "Register view failure: ", error); + } )); } @@ -1032,6 +1051,8 @@ public void showLoading() { animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); + animateView(detailPositionView, false, 100); + animateView(positionView, false, 50); videoTitleTextView.setText(name != null ? name : ""); videoTitleTextView.setMaxLines(1); @@ -1146,6 +1167,7 @@ public void handleResult(@NonNull StreamInfo info) { videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate())); } prepareDescription(info.getDescription()); + updatePositionInfo(info); animateView(spinnerToolbar, true, 500); setupActionBar(info); @@ -1250,4 +1272,46 @@ public void onBlockedByGemaError() { showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } + + private void updatePositionInfo(final StreamInfo info) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + final boolean playbackResumeEnabled = + prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); + if (!playbackResumeEnabled || info.getDuration() <= 0) { + positionView.setVisibility(View.INVISIBLE); + detailPositionView.setVisibility(View.GONE); + return; + } + if (recordManager == null) { + recordManager = new HistoryRecordManager(requireContext()); + } + disposables.add(recordManager.loadStreamState(info) + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + state -> { + final int seconds = (int) (state.getProgressTime() / 1000.f); + if (seconds > BasePlayer.PLAYBACK_SAVE_THRESHOLD_START_SECONDS && + seconds < info.getDuration() - BasePlayer.PLAYBACK_SAVE_THRESHOLD_END_SECONDS) { + positionView.setMax((int) info.getDuration()); + positionView.setProgress(seconds); + detailPositionView.setText(Localization.getDurationString(seconds)); + animateView(positionView, true, 500); + animateView(detailPositionView, true, 500); + } else { + animateView(positionView, false, 500); + animateView(detailPositionView, false, 500); + } + }, + error -> { + if (DEBUG) Log.e(TAG, "Player resume failure: ", error); + }, + () -> { + animateView(positionView, false, 500); + animateView(detailPositionView, false, 500); + } + )); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 56453773a7f..e762a70651e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -107,6 +107,13 @@ public Flowable> getStreamHistory() { return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); } + public Maybe getStreamHistory(final StreamInfo info) { + return Maybe.fromCallable(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.getLatestEntry(streamId); + }).subscribeOn(Schedulers.io()); + } + public Flowable> getStreamStatistics() { return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); } @@ -195,6 +202,13 @@ public Maybe saveStreamState(@NonNull final StreamInfo info, final long pr })).subscribeOn(Schedulers.io()); } + public Maybe resetStreamState(@NonNull final StreamInfo info) { + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamStateTable.deleteState(streamId); + })).subscribeOn(Schedulers.io()); + } + /////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 3989581fdd3..c954885419f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -150,6 +150,7 @@ private void onClose() { lockManager.releaseWifiAndCpu(); } if (basePlayerImpl != null) { + basePlayerImpl.savePlaybackState(); basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 22c69fdd452..7254df6c73e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -23,9 +23,12 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; +import android.preference.PreferenceManager; +import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -75,6 +78,7 @@ import java.io.IOException; import java.net.UnknownHostException; +import java.util.Queue; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; @@ -97,7 +101,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListener, ImageLoadingListener { + @SuppressWarnings("ConstantConditions") public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); + @NonNull public static final String TAG = "BasePlayer"; @@ -176,7 +182,12 @@ public abstract class BasePlayer implements protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; - protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds + /** Playback state will not be saved, if playback time less than this threshold */ + public static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5; + public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLIS = TimeUnit.SECONDS.toMillis(PLAYBACK_SAVE_THRESHOLD_START_SECONDS); + /** Playback state will not be saved, if time left less than this threshold */ + public static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10; + public static final long PLAYBACK_SAVE_THRESHOLD_END_MILLIS = TimeUnit.SECONDS.toMillis(PLAYBACK_SAVE_THRESHOLD_END_SECONDS); protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; @@ -355,7 +366,7 @@ public void onLoadingStarted(String imageUri, View view) { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", + if (DEBUG) Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", failReason.getCause()); currentThumbnail = null; } @@ -390,6 +401,7 @@ protected void setupBroadcastReceiver(IntentFilter intentFilter) { public void onBroadcastReceived(Intent intent) { if (intent == null || intent.getAction() == null) return; + //noinspection SwitchStatementWithTooFewBranches switch (intent.getAction()) { case AudioManager.ACTION_AUDIO_BECOMING_NOISY: onPause(); @@ -407,7 +419,7 @@ protected void unregisterBroadcastReceiver() { try { context.unregisterReceiver(broadcastReceiver); } catch (final IllegalArgumentException unregisteredException) { - Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")"); + if (DEBUG) Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")"); } } @@ -425,6 +437,7 @@ protected void unregisterBroadcastReceiver() { protected int currentState = STATE_PREFLIGHT; + @CallSuper public void changeState(int state) { if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); currentState = state; @@ -450,11 +463,13 @@ public void changeState(int state) { } } + @CallSuper public void onBlocked() { if (DEBUG) Log.d(TAG, "onBlocked() called"); if (!isProgressLoopRunning()) startProgressLoop(); } + @CallSuper public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); if (!isProgressLoopRunning()) startProgressLoop(); @@ -463,15 +478,23 @@ public void onPlaying() { public void onBuffering() { } + @CallSuper public void onPaused() { - if (isProgressLoopRunning()) stopProgressLoop(); + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + savePlaybackState(); } public void onPausedSeek() { } + @CallSuper public void onCompleted() { if (DEBUG) Log.d(TAG, "onCompleted() called"); + if (currentMetadata != null) { + resetPlaybackState(currentMetadata.getMetadata()); + } if (playQueue.getIndex() < playQueue.size() - 1) playQueue.offsetIndex(+1); if (isProgressLoopRunning()) stopProgressLoop(); } @@ -536,7 +559,9 @@ private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> triggerProgressUpdate(), - error -> Log.e(TAG, "Progress update failure: ", error)); + error -> { + if (DEBUG) Log.e(TAG, "Progress update failure: ", error); + }); } /*////////////////////////////////////////////////////////////////////////// @@ -605,9 +630,9 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { break; case Player.STATE_READY: //3 maybeUpdateCurrentMetadata(); - maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; + maybeCorrectSeekPosition(); onPrepared(playWhenReady); break; } @@ -633,6 +658,23 @@ private void maybeCorrectSeekPosition() { if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + "position=[" + presetStartPositionMillis + "]"); seekTo(presetStartPositionMillis); + } else if (isPlaybackResumeEnabled()) { + final Disposable stateLoader = recordManager.loadStreamState(currentMetadata.getMetadata()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + state -> { + if (state.getProgressTime() > PLAYBACK_SAVE_THRESHOLD_START_MILLIS && + state.getProgressTime() < simpleExoPlayer.getDuration() - + PLAYBACK_SAVE_THRESHOLD_END_MILLIS) { + seekTo(state.getProgressTime()); + onPositionRestored(state.getProgressTime()); + } + }, + error -> { + if (DEBUG) Log.e(TAG, "Player resume failure: ", error); + } + ); + databaseUpdateReactor.add(stateLoader); } } @@ -721,6 +763,9 @@ public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: if (playQueue.getIndex() != newWindowIndex) { + if (currentMetadata != null) { + resetPlaybackState(currentMetadata.getMetadata()); + } playQueue.setIndex(newWindowIndex); } break; @@ -807,14 +852,14 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + + if (DEBUG) Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + "index=[" + currentPlayQueueIndex + "], " + "queue index=[" + playQueue.getIndex() + "]"); // Check if bad seek position } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) || currentPlayQueueIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to invalid " + + if (DEBUG) Log.e(TAG, "Playback - Trying to seek to invalid " + "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); @@ -881,12 +926,16 @@ public void showUnrecoverableError(Exception exception) { errorToast.show(); } - public void onPrepared(boolean playWhenReady) { + public void onPrepared(final boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (playWhenReady) audioReactor.requestAudioFocus(); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); } + public void onPositionRestored(long position) { + } + + @CallSuper public void onPlay() { if (DEBUG) Log.d(TAG, "onPlay() called"); if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return; @@ -904,6 +953,7 @@ public void onPlay() { simpleExoPlayer.setPlayWhenReady(true); } + @CallSuper public void onPause() { if (DEBUG) Log.d(TAG, "onPause() called"); if (audioReactor == null || simpleExoPlayer == null) return; @@ -912,6 +962,7 @@ public void onPause() { simpleExoPlayer.setPlayWhenReady(false); } + @CallSuper public void onPlayPause() { if (DEBUG) Log.d(TAG, "onPlayPause() called"); @@ -922,16 +973,19 @@ public void onPlayPause() { } } + @CallSuper public void onFastRewind() { if (DEBUG) Log.d(TAG, "onFastRewind() called"); seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS); } + @CallSuper public void onFastForward() { if (DEBUG) Log.d(TAG, "onFastForward() called"); seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS); } + @CallSuper public void onPlayPrevious() { if (simpleExoPlayer == null || playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); @@ -949,6 +1003,7 @@ public void onPlayPrevious() { } } + @CallSuper public void onPlayNext() { if (playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayNext() called"); @@ -1002,12 +1057,15 @@ private void registerView() { final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() .subscribe( ignored -> {/* successful */}, - error -> Log.e(TAG, "Player onViewed() failure: ", error) + error -> { + if (DEBUG) Log.e(TAG, "Player onViewed() failure: ", error); + } ); databaseUpdateReactor.add(viewRegister); } protected void reload() { + savePlaybackState(); if (playbackManager != null) { playbackManager.dispose(); } @@ -1017,26 +1075,47 @@ protected void reload() { } } - protected void savePlaybackState(final StreamInfo info, final long progress) { + private void savePlaybackState(final StreamInfo info, final long progress) { if (info == null) return; + if (DEBUG) Log.d(TAG, "savePlaybackState() called"); final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() .subscribe( ignored -> {/* successful */}, - error -> Log.e(TAG, "savePlaybackState() failure: ", error) + error -> { + if (DEBUG) Log.e(TAG, "savePlaybackState() failure: ", error); + } ); databaseUpdateReactor.add(stateSaver); } - private void savePlaybackState() { + private void resetPlaybackState(final StreamInfo info) { + if (info == null) return; + if (DEBUG) Log.d(TAG, "resetPlaybackState() called"); + final Disposable d = recordManager.resetStreamState(info) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorComplete() + .subscribe( + ignored -> {/* successful */}, + error -> { + if (DEBUG) Log.e(TAG, "resetPlaybackState() failure: ", error); + } + ); + databaseUpdateReactor.add(d); + } + + public void savePlaybackState() { if (simpleExoPlayer == null || currentMetadata == null) return; final StreamInfo currentInfo = currentMetadata.getMetadata(); - if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && - simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) { - savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); + if (simpleExoPlayer.getCurrentPosition() > PLAYBACK_SAVE_THRESHOLD_START_MILLIS) { + if (simpleExoPlayer.getCurrentPosition() < simpleExoPlayer.getDuration() - + PLAYBACK_SAVE_THRESHOLD_END_MILLIS) { + savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); + } else { + resetPlaybackState(currentInfo); + } } } @@ -1135,11 +1214,13 @@ public boolean isLive() { if (simpleExoPlayer == null) return false; try { return simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull IndexOutOfBoundsException ignored) { + } catch (@NonNull IndexOutOfBoundsException e) { // Why would this even happen =( // But lets log it anyway. Save is save - if (DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage()); - if (DEBUG) ignored.printStackTrace(); + if (DEBUG) { + Log.d(TAG, "Could not update metadata: " + e.getMessage()); + e.printStackTrace(); + } return false; } } @@ -1225,4 +1306,10 @@ public void setRecovery(final int queuePos, final long windowPos) { public boolean gotDestroyed() { return simpleExoPlayer == null; } + + private boolean isPlaybackResumeEnabled() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index cf179917df7..bad087fb55b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -165,6 +165,12 @@ protected void onNewIntent(Intent intent) { } } + @Override + protected void onPause() { + playerImpl.savePlaybackState(); + super.onPause(); + } + @Override protected void onResume() { if (DEBUG) Log.d(TAG, "onResume() called"); @@ -819,7 +825,6 @@ public void onPausedSeek() { getRootView().setKeepScreenOn(true); } - @Override public void onCompleted() { animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 7578c444c92..2be298713f7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -325,6 +325,7 @@ public void closePopup() { isPopupClosing = true; if (playerImpl != null) { + playerImpl.savePlaybackState(); if (playerImpl.getRootView() != null) { windowManager.removeView(playerImpl.getRootView()); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index fb60ac4734a..b264ec616cb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -430,6 +430,7 @@ public void onBuffering() { @Override public void onPaused() { + super.onPaused(); if (DEBUG) Log.d(TAG, "onPaused() called"); showControls(400); loadingPanel.setVisibility(View.GONE); @@ -441,6 +442,15 @@ public void onPausedSeek() { showAndAnimateControl(-1, true); } + @Override + public void onPositionRestored(long position) { + super.onPositionRestored(position); + if (!isControlsVisible()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); + } + } + @Override public void onCompleted() { super.onCompleted(); diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml new file mode 100644 index 00000000000..1ec9f67b6d2 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml new file mode 100644 index 00000000000..c326c5c04d7 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml new file mode 100644 index 00000000000..404410f9829 --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_light.xml b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml new file mode 100644 index 00000000000..120a6e5fb37 --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 8cdc2f307fe..2d20b49e2b0 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -1,7 +1,7 @@ @@ -67,10 +68,10 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:background="#64000000" - android:paddingBottom="10dp" android:paddingLeft="30dp" - android:paddingRight="30dp" android:paddingTop="10dp" + android:paddingRight="30dp" + android:paddingBottom="10dp" android:text="@string/hold_to_append" android:textColor="@android:color/white" android:textSize="20sp" @@ -84,17 +85,42 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="8dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:alpha=".6" android:background="#23000000" android:gravity="center" - android:paddingBottom="2dp" android:paddingLeft="6dp" + android:paddingTop="2dp" android:paddingRight="6dp" + android:paddingBottom="2dp" + android:textAllCaps="true" + android:textColor="@android:color/white" + android:textSize="12sp" + android:textStyle="bold" + android:visibility="gone" + tools:ignore="RtlHardcoded" + tools:text="12:38" + tools:visibility="visible" /> + + + + + @@ -201,8 +241,8 @@ android:id="@+id/detail_uploader_root_layout" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_toLeftOf="@id/details_panel" android:layout_toStartOf="@id/details_panel" + android:layout_toLeftOf="@id/details_panel" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:orientation="horizontal" @@ -261,8 +301,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" - android:layout_marginBottom="6dp" android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" android:lines="1" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/video_item_detail_views_text_size" @@ -354,8 +394,8 @@ android:drawableTop="?attr/ic_playlist_add" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_add_to_playlist_title" android:textSize="12sp" /> @@ -371,8 +411,8 @@ android:drawableTop="?attr/audio" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_background_title" android:textSize="12sp" /> @@ -388,8 +428,8 @@ android:drawableTop="?attr/popup" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_popup_title" android:textSize="12sp" /> @@ -405,8 +445,8 @@ android:drawableTop="?attr/download" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/download" android:textSize="12sp" /> @@ -444,10 +484,10 @@ android:id="@+id/detail_description_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="3dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:textAppearance="?android:attr/textAppearanceMedium" android:textIsSelectable="true" android:textSize="@dimen/video_item_detail_description_text_size" @@ -490,7 +530,7 @@ @@ -101,10 +102,48 @@ tools:ignore="RtlHardcoded" tools:text="12:38" tools:visibility="visible" /> + + + + + Что нового История поиска Хранить запросы поиска локально - История и кэш + История просмотров Запоминать воспроизведённые потоки Возобновить при фокусе Возобновлять воспроизведение после перерывов (например, телефонных звонков) @@ -471,4 +471,7 @@ Не удалось получить данные с сервера Пост-обработка не удалась Останавливать скачивание при переходе на мобильную сеть - \ No newline at end of file + Продолжать воспроизведение + Восстанавливать с последней позиции + Очистить данные + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index eaca5719a8c..a8a6df49f8f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -153,7 +153,7 @@ Показувати пропозиції під час пошуку Історія пошуків Зберігати пошукові запити локально - Історія та кеш + Історія переглядiв Вести облік перегляду відеозаписів Відновити відтворення Продовжувати відтворення опісля переривання (наприклад телефонного дзвінка) diff --git a/app/src/main/res/values-v21/styles_services.xml b/app/src/main/res/values-v21/styles_services.xml index 6c118bc0913..d51b0531a0c 100644 --- a/app/src/main/res/values-v21/styles_services.xml +++ b/app/src/main/res/values-v21/styles_services.xml @@ -17,18 +17,21 @@ @color/light_soundcloud_primary_color @color/light_soundcloud_statusbar_color @color/light_soundcloud_accent_color + @drawable/progress_soundcloud_horizontal_light diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 865b68c24c4..43aa3bce06f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -47,6 +47,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 3861f53d59d..ec8c2c65fa0 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -149,6 +149,7 @@ use_tor enable_search_history enable_watch_history + enable_playback_resume main_page_content import_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98a32d9e648..2bc2998fbf6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,7 +95,7 @@ Show suggestions when searching Search history Store search queries locally - History & Cache + Watch history Keep track of watched videos Resume on focus gain Continue playing after interruptions (e.g. phone calls) @@ -601,5 +601,7 @@ Maximum number of attempts before canceling the download Pause on switching to mobile data Downloads that can not be paused will be restarted - + Resume playback + Restore last playback position + Clear data \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a7686dedc48..902e390f7f5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -64,6 +64,7 @@ @color/light_queue_background_color @drawable/toolbar_shadow_light @drawable/light_selector + @drawable/progress_youtube_horizontal_light @color/light_ripple_color @style/PreferenceThemeOverlay.v14.Material @@ -127,6 +128,7 @@ @color/dark_queue_background_color @drawable/toolbar_shadow_dark @drawable/dark_selector + @drawable/progress_youtube_horizontal_dark @color/dark_ripple_color @style/PreferenceThemeOverlay.v14.Material diff --git a/app/src/main/res/values/styles_services.xml b/app/src/main/res/values/styles_services.xml index 257b1905db7..d6ab239e4e3 100644 --- a/app/src/main/res/values/styles_services.xml +++ b/app/src/main/res/values/styles_services.xml @@ -15,18 +15,21 @@ @color/light_soundcloud_primary_color @color/light_soundcloud_dark_color @color/light_soundcloud_accent_color + @drawable/progress_soundcloud_horizontal_light diff --git a/app/src/main/res/xml/history_settings.xml b/app/src/main/res/xml/history_settings.xml index a7428d34047..db8fc36de6b 100644 --- a/app/src/main/res/xml/history_settings.xml +++ b/app/src/main/res/xml/history_settings.xml @@ -1,40 +1,54 @@ - + android:title="@string/enable_watch_history_title" + app:iconSpaceReserved="false" /> + + - - - - - - + android:title="@string/enable_search_history_title" + app:iconSpaceReserved="false" /> + + + + + + + + + +