Skip to content

Commit a04710a

Browse files
committed
Add new feature: Sleep timer
1 parent 72f36e1 commit a04710a

7 files changed

Lines changed: 350 additions & 99 deletions

File tree

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
import org.schabi.newpipe.util.StreamTypeUtil;
123123
import org.schabi.newpipe.util.image.CoilHelper;
124124

125+
import java.time.temporal.ChronoUnit;
125126
import java.util.List;
126127
import java.util.Optional;
127128
import java.util.stream.IntStream;
@@ -263,6 +264,9 @@ public final class Player implements PlaybackListener, Listener {
263264
@NonNull
264265
private final HistoryRecordManager recordManager;
265266

267+
java.util.Timer sleepTimer;
268+
java.time.Instant sleepTimerEnd;
269+
266270

267271
/*//////////////////////////////////////////////////////////////////////////
268272
// Constructor
@@ -2262,6 +2266,44 @@ public void setAudioTrack(@Nullable final String audioTrackId) {
22622266
}
22632267

22642268

2269+
public void setSleepTimer(@Nullable final long sleepMinutes) {
2270+
if (sleepTimer != null) {
2271+
sleepTimer.cancel();
2272+
sleepTimer.purge();
2273+
sleepTimer = null;
2274+
}
2275+
2276+
sleepTimerEnd = java.time.Instant.now().plus(sleepMinutes, ChronoUnit.MINUTES);
2277+
2278+
sleepTimer = new java.util.Timer();
2279+
//final Player thisPlayer = this;
2280+
final java.util.TimerTask task = new java.util.TimerTask() {
2281+
2282+
2283+
@Override
2284+
public void run() {
2285+
if (java.time.Instant.now().compareTo(sleepTimerEnd) >= 0) {
2286+
UIs.call(playerUi -> playerUi.onSleepTimerUpdate(0));
2287+
cancelSleepTimer();
2288+
return;
2289+
}
2290+
2291+
final long remainingMinutes = java.time.Instant.now().until(sleepTimerEnd,
2292+
ChronoUnit.MINUTES);
2293+
UIs.call(playerUi -> playerUi.onSleepTimerUpdate(remainingMinutes + 1));
2294+
}
2295+
};
2296+
sleepTimer.schedule(task, 1000, 1000);
2297+
}
2298+
2299+
public void cancelSleepTimer() {
2300+
if (sleepTimer != null) {
2301+
sleepTimer.cancel();
2302+
sleepTimer.purge();
2303+
sleepTimer = null;
2304+
}
2305+
}
2306+
22652307
@NonNull
22662308
public Context getContext() {
22672309
return context;

app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,11 @@ public void onPlayQueueEdited() {
209209
*/
210210
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
211211
}
212+
213+
/**
214+
* @param remainingTime the remaining sleep timer time, set to 0 to pause the player and
215+
* disable the sleep timer
216+
*/
217+
public void onSleepTimerUpdate(final long remainingTime) {
218+
}
212219
}

app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,14 @@ private enum PlayButtonAction {
128128
private static final int POPUP_MENU_ID_AUDIO_TRACK = 70;
129129
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
130130
private static final int POPUP_MENU_ID_CAPTION = 89;
131+
private static final int POPUP_MENU_ID_SLEEP_TIMER = 90; // TODO is 90 still available?
131132

132133
protected boolean isSomePopupMenuVisible = false;
133134
private PopupMenu qualityPopupMenu;
134135
private PopupMenu audioTrackPopupMenu;
135136
protected PopupMenu playbackSpeedPopupMenu;
136137
private PopupMenu captionPopupMenu;
138+
private PopupMenu sleepTimerPopupMenu;
137139

138140

139141
/*//////////////////////////////////////////////////////////////////////////
@@ -186,6 +188,8 @@ private void initViews() {
186188
audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
187189
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
188190
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
191+
sleepTimerPopupMenu = new PopupMenu(themeWrapper, binding.sleepTimer);
192+
buildSleepTimerMenu();
189193

190194
binding.progressBarLoadingPanel.getIndeterminateDrawable()
191195
.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
@@ -204,6 +208,9 @@ protected void initListeners() {
204208
binding.audioTrackTextView.setOnClickListener(
205209
makeOnClickListener(this::onAudioTracksClicked));
206210
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
211+
binding.sleepTimer.setOnClickListener(makeOnClickListener(this::onSleepTimerClicked));
212+
binding.sleepTimerCancel.setOnClickListener(
213+
makeOnClickListener(this::onSleepTimerCancelClicked));
207214

208215
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
209216
binding.captionTextView.setOnClickListener(makeOnClickListener(this::onCaptionClicked));
@@ -1239,6 +1246,49 @@ private void buildCaptionMenu(@NonNull final List<String> availableLanguages) {
12391246
}
12401247
}
12411248

1249+
private void buildSleepTimerMenu() {
1250+
if (sleepTimerPopupMenu == null) {
1251+
return;
1252+
}
1253+
qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_SLEEP_TIMER);
1254+
1255+
final Resources res = context.getResources();
1256+
sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, 0, 0,
1257+
res.getString(R.string.sleep_timer_popup_title));
1258+
1259+
final String[] descriptions = context.getResources().getStringArray(
1260+
R.array.sleep_timer_description);
1261+
final int[] values = context.getResources().getIntArray(
1262+
R.array.sleep_timer_value);
1263+
for (int i = 0; i < descriptions.length && i < values.length; i++) {
1264+
String description = "";
1265+
try {
1266+
final int hours = values[i] / 60;
1267+
final int minutes = values[i] % 60;
1268+
if (hours != 0) {
1269+
description += String.format(res.getQuantityString(R.plurals.hours, hours),
1270+
hours);
1271+
}
1272+
1273+
if (minutes != 0) {
1274+
if (hours != 0) {
1275+
description += " ";
1276+
}
1277+
description += String.format(res.getQuantityString(R.plurals.minutes, minutes),
1278+
minutes);
1279+
}
1280+
} catch (final Resources.NotFoundException ignored) {
1281+
// if this happens, the translation is missing,
1282+
// and the english string will be displayed instead
1283+
description = descriptions[i];
1284+
}
1285+
sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, i + 1, i + 1, description);
1286+
}
1287+
1288+
sleepTimerPopupMenu.setOnMenuItemClickListener(this);
1289+
sleepTimerPopupMenu.setOnDismissListener(this);
1290+
}
1291+
12421292
protected abstract void onPlaybackSpeedClicked();
12431293

12441294
private void onQualityClicked() {
@@ -1255,6 +1305,19 @@ private void onAudioTracksClicked() {
12551305
isSomePopupMenuVisible = true;
12561306
}
12571307

1308+
private void onSleepTimerClicked() {
1309+
sleepTimerPopupMenu.show();
1310+
isSomePopupMenuVisible = true;
1311+
}
1312+
1313+
private void onSleepTimerCancelClicked() {
1314+
player.cancelSleepTimer();
1315+
1316+
binding.sleepTimerCancel.setVisibility(View.INVISIBLE);
1317+
binding.sleepTimerTextView.setVisibility(View.INVISIBLE);
1318+
binding.sleepTimerTextView.setText("0:00");
1319+
}
1320+
12581321
/**
12591322
* Called when an item of the quality selector or the playback speed selector is selected.
12601323
*/
@@ -1278,8 +1341,12 @@ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
12781341

12791342
player.setPlaybackSpeed(speed);
12801343
binding.playbackSpeed.setText(formatSpeed(speed));
1344+
} else if (menuItem.getGroupId() == POPUP_MENU_ID_SLEEP_TIMER) {
1345+
onSleepTimerItemClick(menuItem);
1346+
return true;
12811347
}
12821348

1349+
12831350
return false;
12841351
}
12851352

@@ -1324,6 +1391,24 @@ private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
13241391
binding.audioTrackTextView.setText(menuItem.getTitle());
13251392
}
13261393

1394+
private void onSleepTimerItemClick(@NonNull final MenuItem menuItem) {
1395+
final int menuItemIndex = menuItem.getItemId();
1396+
if (menuItemIndex == 0) {
1397+
return;
1398+
}
1399+
1400+
final int index = menuItemIndex - 1;
1401+
final int sleepTime = context.getResources().getIntArray(R.array.sleep_timer_value)[index];
1402+
final long remainingTimeHours = sleepTime / 60;
1403+
final long remainingTimeMinutes = sleepTime % 60;
1404+
final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes);
1405+
1406+
player.setSleepTimer(sleepTime);
1407+
binding.sleepTimerCancel.setVisibility(View.VISIBLE);
1408+
binding.sleepTimerTextView.setVisibility(View.VISIBLE);
1409+
binding.sleepTimerTextView.setText(text);
1410+
}
1411+
13271412
/**
13281413
* Called when some popup menu is dismissed.
13291414
*/
@@ -1561,6 +1646,32 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
15611646
}
15621647
//endregion
15631648

1649+
@Override
1650+
public void onSleepTimerUpdate(final long remainingTime) {
1651+
if (remainingTime == 0) {
1652+
binding.sleepTimerTextView.post(new Runnable() {
1653+
public void run() {
1654+
player.pause();
1655+
binding.sleepTimerCancel.setVisibility(View.INVISIBLE);
1656+
binding.sleepTimerTextView.setVisibility(View.INVISIBLE);
1657+
binding.sleepTimerTextView.setText("0:00");
1658+
}
1659+
});
1660+
return;
1661+
}
1662+
1663+
final long remainingTimeHours = remainingTime / 60;
1664+
final long remainingTimeMinutes = remainingTime % 60;
1665+
final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes);
1666+
1667+
// Since this callback can/will be called from a different thread, we need to run set
1668+
// the code in the UI thread
1669+
binding.sleepTimerTextView.post(new Runnable() {
1670+
public void run() {
1671+
binding.sleepTimerTextView.setText(text);
1672+
}
1673+
});
1674+
}
15641675

15651676
/*//////////////////////////////////////////////////////////////////////////
15661677
// SurfaceHolderCallback helpers

0 commit comments

Comments
 (0)