diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index b09593c1739..dd2cef04054 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -122,6 +122,7 @@ import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.image.CoilHelper; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Optional; import java.util.stream.IntStream; @@ -263,6 +264,9 @@ public final class Player implements PlaybackListener, Listener { @NonNull private final HistoryRecordManager recordManager; + java.util.Timer sleepTimer; + java.time.Instant sleepTimerEnd; + /*////////////////////////////////////////////////////////////////////////// // Constructor @@ -2262,6 +2266,44 @@ public void setAudioTrack(@Nullable final String audioTrackId) { } + public void setSleepTimer(@Nullable final long sleepMinutes) { + if (sleepTimer != null) { + sleepTimer.cancel(); + sleepTimer.purge(); + sleepTimer = null; + } + + sleepTimerEnd = java.time.Instant.now().plus(sleepMinutes, ChronoUnit.MINUTES); + + sleepTimer = new java.util.Timer(); + //final Player thisPlayer = this; + final java.util.TimerTask task = new java.util.TimerTask() { + + + @Override + public void run() { + if (java.time.Instant.now().compareTo(sleepTimerEnd) >= 0) { + UIs.call(playerUi -> playerUi.onSleepTimerUpdate(0)); + cancelSleepTimer(); + return; + } + + final long remainingMinutes = java.time.Instant.now().until(sleepTimerEnd, + ChronoUnit.MINUTES); + UIs.call(playerUi -> playerUi.onSleepTimerUpdate(remainingMinutes + 1)); + } + }; + sleepTimer.schedule(task, 1000, 1000); + } + + public void cancelSleepTimer() { + if (sleepTimer != null) { + sleepTimer.cancel(); + sleepTimer.purge(); + sleepTimer = null; + } + } + @NonNull public Context getContext() { return context; diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java index 57e2ec2a2cf..418436b2d26 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java @@ -209,4 +209,11 @@ public void onPlayQueueEdited() { */ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { } + + /** + * @param remainingTime the remaining sleep timer time, set to 0 to pause the player and + * disable the sleep timer + */ + public void onSleepTimerUpdate(final long remainingTime) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java index e96873de52c..91d9400932c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -128,12 +128,14 @@ private enum PlayButtonAction { private static final int POPUP_MENU_ID_AUDIO_TRACK = 70; private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; private static final int POPUP_MENU_ID_CAPTION = 89; + private static final int POPUP_MENU_ID_SLEEP_TIMER = 90; // TODO is 90 still available? protected boolean isSomePopupMenuVisible = false; private PopupMenu qualityPopupMenu; private PopupMenu audioTrackPopupMenu; protected PopupMenu playbackSpeedPopupMenu; private PopupMenu captionPopupMenu; + private PopupMenu sleepTimerPopupMenu; /*////////////////////////////////////////////////////////////////////////// @@ -186,6 +188,8 @@ private void initViews() { audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView); playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); + sleepTimerPopupMenu = new PopupMenu(themeWrapper, binding.sleepTimer); + buildSleepTimerMenu(); binding.progressBarLoadingPanel.getIndeterminateDrawable() .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); @@ -204,6 +208,9 @@ protected void initListeners() { binding.audioTrackTextView.setOnClickListener( makeOnClickListener(this::onAudioTracksClicked)); binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); + binding.sleepTimer.setOnClickListener(makeOnClickListener(this::onSleepTimerClicked)); + binding.sleepTimerCancel.setOnClickListener( + makeOnClickListener(this::onSleepTimerCancelClicked)); binding.playbackSeekBar.setOnSeekBarChangeListener(this); binding.captionTextView.setOnClickListener(makeOnClickListener(this::onCaptionClicked)); @@ -1239,6 +1246,49 @@ private void buildCaptionMenu(@NonNull final List availableLanguages) { } } + private void buildSleepTimerMenu() { + if (sleepTimerPopupMenu == null) { + return; + } + qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_SLEEP_TIMER); + + final Resources res = context.getResources(); + sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, 0, 0, + res.getString(R.string.sleep_timer_popup_title)); + + final String[] descriptions = context.getResources().getStringArray( + R.array.sleep_timer_description); + final int[] values = context.getResources().getIntArray( + R.array.sleep_timer_value); + for (int i = 0; i < descriptions.length && i < values.length; i++) { + String description = ""; + try { + final int hours = values[i] / 60; + final int minutes = values[i] % 60; + if (hours != 0) { + description += String.format(res.getQuantityString(R.plurals.hours, hours), + hours); + } + + if (minutes != 0) { + if (hours != 0) { + description += " "; + } + description += String.format(res.getQuantityString(R.plurals.minutes, minutes), + minutes); + } + } catch (final Resources.NotFoundException ignored) { + // if this happens, the translation is missing, + // and the english string will be displayed instead + description = descriptions[i]; + } + sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, i + 1, i + 1, description); + } + + sleepTimerPopupMenu.setOnMenuItemClickListener(this); + sleepTimerPopupMenu.setOnDismissListener(this); + } + protected abstract void onPlaybackSpeedClicked(); private void onQualityClicked() { @@ -1255,6 +1305,19 @@ private void onAudioTracksClicked() { isSomePopupMenuVisible = true; } + private void onSleepTimerClicked() { + sleepTimerPopupMenu.show(); + isSomePopupMenuVisible = true; + } + + private void onSleepTimerCancelClicked() { + player.cancelSleepTimer(); + + binding.sleepTimerCancel.setVisibility(View.INVISIBLE); + binding.sleepTimerTextView.setVisibility(View.INVISIBLE); + binding.sleepTimerTextView.setText("0:00"); + } + /** * Called when an item of the quality selector or the playback speed selector is selected. */ @@ -1278,8 +1341,12 @@ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) { player.setPlaybackSpeed(speed); binding.playbackSpeed.setText(formatSpeed(speed)); + } else if (menuItem.getGroupId() == POPUP_MENU_ID_SLEEP_TIMER) { + onSleepTimerItemClick(menuItem); + return true; } + return false; } @@ -1324,6 +1391,24 @@ private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) { binding.audioTrackTextView.setText(menuItem.getTitle()); } + private void onSleepTimerItemClick(@NonNull final MenuItem menuItem) { + final int menuItemIndex = menuItem.getItemId(); + if (menuItemIndex == 0) { + return; + } + + final int index = menuItemIndex - 1; + final int sleepTime = context.getResources().getIntArray(R.array.sleep_timer_value)[index]; + final long remainingTimeHours = sleepTime / 60; + final long remainingTimeMinutes = sleepTime % 60; + final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes); + + player.setSleepTimer(sleepTime); + binding.sleepTimerCancel.setVisibility(View.VISIBLE); + binding.sleepTimerTextView.setVisibility(View.VISIBLE); + binding.sleepTimerTextView.setText(text); + } + /** * Called when some popup menu is dismissed. */ @@ -1561,6 +1646,32 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { } //endregion + @Override + public void onSleepTimerUpdate(final long remainingTime) { + if (remainingTime == 0) { + binding.sleepTimerTextView.post(new Runnable() { + public void run() { + player.pause(); + binding.sleepTimerCancel.setVisibility(View.INVISIBLE); + binding.sleepTimerTextView.setVisibility(View.INVISIBLE); + binding.sleepTimerTextView.setText("0:00"); + } + }); + return; + } + + final long remainingTimeHours = remainingTime / 60; + final long remainingTimeMinutes = remainingTime % 60; + final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes); + + // Since this callback can/will be called from a different thread, we need to run set + // the code in the UI thread + binding.sleepTimerTextView.post(new Runnable() { + public void run() { + binding.sleepTimerTextView.setText(text); + } + }); + } /*////////////////////////////////////////////////////////////////////////// // SurfaceHolderCallback helpers diff --git a/app/src/main/res/drawable/ic_alarm.xml b/app/src/main/res/drawable/ic_alarm.xml new file mode 100644 index 00000000000..dfc893e1e96 --- /dev/null +++ b/app/src/main/res/drawable/ic_alarm.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_alarm_off.xml b/app/src/main/res/drawable/ic_alarm_off.xml new file mode 100644 index 00000000000..156a4320ccc --- /dev/null +++ b/app/src/main/res/drawable/ic_alarm_off.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index 99b514bb090..289cfa21c38 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -262,124 +262,188 @@ android:layout_height="wrap_content" android:gravity="top" android:visibility="invisible" - tools:ignore="RtlHardcoded" - tools:visibility="visible"> + android:orientation="vertical"> - - - + android:gravity="top" + tools:ignore="RtlHardcoded"> + tools:ignore="HardcodedText,RtlHardcoded" + tools:text="FIT" /> - + + + + + + + - + - + - + + + + - + - + - + + + + + + Gefällt mir SoundCloud-Top-50-Seite entfernt SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt. + Sleep Timer + Sleep Timer stellen + Sleep Timer beenden diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index d95d1270cc9..fec2403a6dc 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -50,6 +50,27 @@ 30000 + + + 5 minutes + 10 minutes + 20 minutes + 30 minutes + 45 minutes + 1 hour + 2 hours + + + 5 + 10 + 20 + 30 + 45 + 60 + 120 + + progressive_load_interval 64 exoplayer_default diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f28a9958da0..281aba8f8f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -889,4 +889,7 @@ Trending podcasts Trending movies and shows Trending music + Sleep timer + Set sleep timer + Cancel sleep timer