@@ -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