1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package android.support.v17.leanback.widget;
15 
16 import android.support.v17.leanback.R;
17 import android.util.TypedValue;
18 import android.content.Context;
19 import android.content.res.TypedArray;
20 import android.graphics.Bitmap;
21 import android.graphics.BitmapFactory;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.PorterDuff;
26 import android.graphics.PorterDuffColorFilter;
27 import android.graphics.drawable.BitmapDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.view.KeyEvent;
30 
31 /**
32  * A {@link Row} of playback controls to be displayed by a {@link PlaybackControlsRowPresenter}.
33  *
34  * This row consists of some optional item detail, a series of primary actions,
35  * and an optional series of secondary actions.
36  *
37  * <p>
38  * Controls are specified via an {@link ObjectAdapter} containing one or more
39  * {@link Action}s.
40  * </p>
41  * <p>
42  * Adapters should have their {@link PresenterSelector} set to an instance of
43  * {@link ControlButtonPresenterSelector}.
44  * </p>
45  */
46 public class PlaybackControlsRow extends Row {
47 
48     /**
49      * Base class for an action comprised of a series of icons.
50      */
51     public static abstract class MultiAction extends Action {
52         private int mIndex;
53         private Drawable[] mDrawables;
54         private String[] mLabels;
55         private String[] mLabels2;
56 
57         /**
58          * Constructor
59          * @param id The id of the Action.
60          */
MultiAction(int id)61         public MultiAction(int id) {
62             super(id);
63         }
64 
65         /**
66          * Sets the array of drawables.  The size of the array defines the range
67          * of valid indices for this action.
68          */
setDrawables(Drawable[] drawables)69         public void setDrawables(Drawable[] drawables) {
70             mDrawables = drawables;
71             setIndex(0);
72         }
73 
74         /**
75          * Sets the array of strings used as labels.  The size of the array defines the range
76          * of valid indices for this action.  The labels are used to define the accessibility
77          * content description unless secondary labels are provided.
78          */
setLabels(String[] labels)79         public void setLabels(String[] labels) {
80             mLabels = labels;
81             setIndex(0);
82         }
83 
84         /**
85          * Sets the array of strings used as secondary labels.  These labels are used
86          * in place of the primary labels for accessibility content description only.
87          */
setSecondaryLabels(String[] labels)88         public void setSecondaryLabels(String[] labels) {
89             mLabels2 = labels;
90             setIndex(0);
91         }
92 
93         /**
94          * Returns the number of actions.
95          */
getActionCount()96         public int getActionCount() {
97             if (mDrawables != null) {
98                 return mDrawables.length;
99             }
100             if (mLabels != null) {
101                 return mLabels.length;
102             }
103             return 0;
104         }
105 
106         /**
107          * Returns the drawable at the given index.
108          */
getDrawable(int index)109         public Drawable getDrawable(int index) {
110             return mDrawables == null ? null : mDrawables[index];
111         }
112 
113         /**
114          * Returns the label at the given index.
115          */
getLabel(int index)116         public String getLabel(int index) {
117             return mLabels == null ? null : mLabels[index];
118         }
119 
120         /**
121          * Returns the secondary label at the given index.
122          */
getSecondaryLabel(int index)123         public String getSecondaryLabel(int index) {
124             return mLabels2 == null ? null : mLabels2[index];
125         }
126 
127         /**
128          * Increments the index, wrapping to zero once the end is reached.
129          */
nextIndex()130         public void nextIndex() {
131             setIndex(mIndex < getActionCount() - 1 ? mIndex + 1 : 0);
132         }
133 
134         /**
135          * Sets the current index.
136          */
137         public void setIndex(int index) {
138             mIndex = index;
139             if (mDrawables != null) {
140                 setIcon(mDrawables[mIndex]);
141             }
142             if (mLabels != null) {
143                 setLabel1(mLabels[mIndex]);
144             }
145             if (mLabels2 != null) {
146                 setLabel2(mLabels2[mIndex]);
147             }
148         }
149 
150         /**
151          * Returns the current index.
152          */
153         public int getIndex() {
154             return mIndex;
155         }
156     }
157 
158     /**
159      * An action displaying icons for play and pause.
160      */
161     public static class PlayPauseAction extends MultiAction {
162         /**
163          * Action index for the play icon.
164          */
165         public static int PLAY = 0;
166 
167         /**
168          * Action index for the pause icon.
169          */
170         public static int PAUSE = 1;
171 
172         /**
173          * Constructor
174          * @param context Context used for loading resources.
175          */
176         public PlayPauseAction(Context context) {
177             super(R.id.lb_control_play_pause);
178             Drawable[] drawables = new Drawable[2];
179             drawables[PLAY] = getStyledDrawable(context,
180                     R.styleable.lbPlaybackControlsActionIcons_play);
181             drawables[PAUSE] = getStyledDrawable(context,
182                     R.styleable.lbPlaybackControlsActionIcons_pause);
183             setDrawables(drawables);
184 
185             String[] labels = new String[drawables.length];
186             labels[PLAY] = context.getString(R.string.lb_playback_controls_play);
187             labels[PAUSE] = context.getString(R.string.lb_playback_controls_pause);
188             setLabels(labels);
189             addKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
190             addKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY);
191             addKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE);
192         }
193     }
194 
195     /**
196      * An action displaying an icon for fast forward.
197      */
198     public static class FastForwardAction extends MultiAction {
199         /**
200          * Constructor
201          * @param context Context used for loading resources.
202          */
203         public FastForwardAction(Context context) {
204             this(context, 1);
205         }
206 
207         /**
208          * Constructor
209          * @param context Context used for loading resources.
210          * @param numSpeeds Number of supported fast forward speeds.
211          */
212         public FastForwardAction(Context context, int numSpeeds) {
213             super(R.id.lb_control_fast_forward);
214 
215             if (numSpeeds < 1) {
216                 throw new IllegalArgumentException("numSpeeds must be > 0");
217             }
218             Drawable[] drawables = new Drawable[numSpeeds];
219             drawables[0] = getStyledDrawable(context,
220                     R.styleable.lbPlaybackControlsActionIcons_fast_forward);
221             setDrawables(drawables);
222 
223             String[] labels = new String[getActionCount()];
224             labels[0] = context.getString(R.string.lb_playback_controls_fast_forward);
225 
226             String[] labels2 = new String[getActionCount()];
227             labels2[0] = labels[0];
228 
229             for (int i = 1; i < numSpeeds; i++) {
230                 int multiplier = i + 1;
231                 labels[i] = context.getResources().getString(
232                         R.string.lb_control_display_fast_forward_multiplier, multiplier);
233                 labels2[i] = context.getResources().getString(
234                         R.string.lb_playback_controls_fast_forward_multiplier, multiplier);
235             }
236             setLabels(labels);
237             setSecondaryLabels(labels2);
238             addKeyCode(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
239         }
240     }
241 
242     /**
243      * An action displaying an icon for rewind.
244      */
245     public static class RewindAction extends MultiAction {
246         /**
247          * Constructor
248          * @param context Context used for loading resources.
249          */
250         public RewindAction(Context context) {
251             this(context, 1);
252         }
253 
254         /**
255          * Constructor
256          * @param context Context used for loading resources.
257          * @param numSpeeds Number of supported fast forward speeds.
258          */
259         public RewindAction(Context context, int numSpeeds) {
260             super(R.id.lb_control_fast_rewind);
261 
262             if (numSpeeds < 1) {
263                 throw new IllegalArgumentException("numSpeeds must be > 0");
264             }
265             Drawable[] drawables = new Drawable[numSpeeds];
266             drawables[0] = getStyledDrawable(context,
267                     R.styleable.lbPlaybackControlsActionIcons_rewind);
268             setDrawables(drawables);
269 
270             String[] labels = new String[getActionCount()];
271             labels[0] = context.getString(R.string.lb_playback_controls_rewind);
272 
273             String[] labels2 = new String[getActionCount()];
274             labels2[0] = labels[0];
275 
276             for (int i = 1; i < numSpeeds; i++) {
277                 int multiplier = i + 1;
278                 labels[i] = labels[i] = context.getResources().getString(
279                         R.string.lb_control_display_rewind_multiplier, multiplier);
280                 labels2[i] = context.getResources().getString(
281                         R.string.lb_playback_controls_rewind_multiplier, multiplier);
282             }
283             setLabels(labels);
284             setSecondaryLabels(labels2);
285             addKeyCode(KeyEvent.KEYCODE_MEDIA_REWIND);
286         }
287     }
288 
289     /**
290      * An action displaying an icon for skip next.
291      */
292     public static class SkipNextAction extends Action {
293         /**
294          * Constructor
295          * @param context Context used for loading resources.
296          */
297         public SkipNextAction(Context context) {
298             super(R.id.lb_control_skip_next);
299             setIcon(getStyledDrawable(context,
300                     R.styleable.lbPlaybackControlsActionIcons_skip_next));
301             setLabel1(context.getString(R.string.lb_playback_controls_skip_next));
302             addKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT);
303         }
304     }
305 
306     /**
307      * An action displaying an icon for skip previous.
308      */
309     public static class SkipPreviousAction extends Action {
310         /**
311          * Constructor
312          * @param context Context used for loading resources.
313          */
314         public SkipPreviousAction(Context context) {
315             super(R.id.lb_control_skip_previous);
316             setIcon(getStyledDrawable(context,
317                     R.styleable.lbPlaybackControlsActionIcons_skip_previous));
318             setLabel1(context.getString(R.string.lb_playback_controls_skip_previous));
319             addKeyCode(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
320         }
321     }
322 
323     /**
324      * An action displaying an icon for picture-in-picture.
325      */
326     public static class PictureInPictureAction extends Action {
327         /**
328          * Constructor
329          * @param context Context used for loading resources.
330          */
331         public PictureInPictureAction(Context context) {
332             super(R.id.lb_control_picture_in_picture);
333             setIcon(getStyledDrawable(context,
334                     R.styleable.lbPlaybackControlsActionIcons_picture_in_picture));
335             setLabel1(context.getString(R.string.lb_playback_controls_picture_in_picture));
336             addKeyCode(KeyEvent.KEYCODE_WINDOW);
337         }
338     }
339 
340     /**
341      * An action displaying an icon for "more actions".
342      */
343     public static class MoreActions extends Action {
344         /**
345          * Constructor
346          * @param context Context used for loading resources.
347          */
348         public MoreActions(Context context) {
349             super(R.id.lb_control_more_actions);
350             setIcon(context.getResources().getDrawable(R.drawable.lb_ic_more));
351             setLabel1(context.getString(R.string.lb_playback_controls_more_actions));
352         }
353     }
354 
355     /**
356      * A base class for displaying a thumbs action.
357      */
358     public static abstract class ThumbsAction extends MultiAction {
359         /**
360          * Action index for the solid thumb icon.
361          */
362         public static int SOLID = 0;
363 
364         /**
365          * Action index for the outline thumb icon.
366          */
367         public static int OUTLINE = 1;
368 
369         /**
370          * Constructor
371          * @param context Context used for loading resources.
372          */
373         public ThumbsAction(int id, Context context, int solidIconIndex, int outlineIconIndex) {
374             super(id);
375             Drawable[] drawables = new Drawable[2];
376             drawables[SOLID] = getStyledDrawable(context, solidIconIndex);
377             drawables[OUTLINE] = getStyledDrawable(context, outlineIconIndex);
378             setDrawables(drawables);
379         }
380     }
381 
382     /**
383      * An action displaying an icon for thumbs up.
384      */
385     public static class ThumbsUpAction extends ThumbsAction {
386         public ThumbsUpAction(Context context) {
387             super(R.id.lb_control_thumbs_up, context,
388                     R.styleable.lbPlaybackControlsActionIcons_thumb_up,
389                     R.styleable.lbPlaybackControlsActionIcons_thumb_up_outline);
390             String[] labels = new String[getActionCount()];
391             labels[SOLID] = context.getString(R.string.lb_playback_controls_thumb_up);
392             labels[OUTLINE] = context.getString(R.string.lb_playback_controls_thumb_up_outline);
393             setLabels(labels);
394         }
395     }
396 
397     /**
398      * An action displaying an icon for thumbs down.
399      */
400     public static class ThumbsDownAction extends ThumbsAction {
401         public ThumbsDownAction(Context context) {
402             super(R.id.lb_control_thumbs_down, context,
403                     R.styleable.lbPlaybackControlsActionIcons_thumb_down,
404                     R.styleable.lbPlaybackControlsActionIcons_thumb_down_outline);
405             String[] labels = new String[getActionCount()];
406             labels[SOLID] = context.getString(R.string.lb_playback_controls_thumb_down);
407             labels[OUTLINE] = context.getString(R.string.lb_playback_controls_thumb_down_outline);
408             setLabels(labels);
409         }
410     }
411 
412     /**
413      * An action for displaying three repeat states: none, one, or all.
414      */
415     public static class RepeatAction extends MultiAction {
416         /**
417          * Action index for the repeat-none icon.
418          */
419         public static int NONE = 0;
420 
421         /**
422          * Action index for the repeat-all icon.
423          */
424         public static int ALL = 1;
425 
426         /**
427          * Action index for the repeat-one icon.
428          */
429         public static int ONE = 2;
430 
431         /**
432          * Constructor
433          * @param context Context used for loading resources.
434          */
435         public RepeatAction(Context context) {
436             this(context, getIconHighlightColor(context));
437         }
438 
439         /**
440          * Constructor
441          * @param context Context used for loading resources
442          * @param highlightColor Color to display the repeat-all and repeat0one icons.
443          */
444         public RepeatAction(Context context, int highlightColor) {
445             this(context, highlightColor, highlightColor);
446         }
447 
448         /**
449          * Constructor
450          * @param context Context used for loading resources
451          * @param repeatAllColor Color to display the repeat-all icon.
452          * @param repeatOneColor Color to display the repeat-one icon.
453          */
454         public RepeatAction(Context context, int repeatAllColor, int repeatOneColor) {
455             super(R.id.lb_control_repeat);
456             Drawable[] drawables = new Drawable[3];
457             BitmapDrawable repeatDrawable = (BitmapDrawable) getStyledDrawable(context,
458                     R.styleable.lbPlaybackControlsActionIcons_repeat);
459             BitmapDrawable repeatOneDrawable = (BitmapDrawable) getStyledDrawable(context,
460                     R.styleable.lbPlaybackControlsActionIcons_repeat_one);
461             drawables[NONE] = repeatDrawable;
462             drawables[ALL] = repeatDrawable == null ? null
463                     : new BitmapDrawable(context.getResources(),
464                             createBitmap(repeatDrawable.getBitmap(), repeatAllColor));
465             drawables[ONE] = repeatOneDrawable == null ? null
466                     : new BitmapDrawable(context.getResources(),
467                             createBitmap(repeatOneDrawable.getBitmap(), repeatOneColor));
468             setDrawables(drawables);
469 
470             String[] labels = new String[drawables.length];
471             // Note, labels denote the action taken when clicked
472             labels[NONE] = context.getString(R.string.lb_playback_controls_repeat_all);
473             labels[ALL] = context.getString(R.string.lb_playback_controls_repeat_one);
474             labels[ONE] = context.getString(R.string.lb_playback_controls_repeat_none);
475             setLabels(labels);
476         }
477     }
478 
479     /**
480      * An action for displaying a shuffle icon.
481      */
482     public static class ShuffleAction extends MultiAction {
483         public static int OFF = 0;
484         public static int ON = 1;
485 
486         /**
487          * Constructor
488          * @param context Context used for loading resources.
489          */
490         public ShuffleAction(Context context) {
491             this(context, getIconHighlightColor(context));
492         }
493 
494         /**
495          * Constructor
496          * @param context Context used for loading resources.
497          * @param highlightColor Color for the highlighted icon state.
498          */
499         public ShuffleAction(Context context, int highlightColor) {
500             super(R.id.lb_control_shuffle);
501             BitmapDrawable uncoloredDrawable = (BitmapDrawable) getStyledDrawable(context,
502                     R.styleable.lbPlaybackControlsActionIcons_shuffle);
503             Drawable[] drawables = new Drawable[2];
504             drawables[OFF] = uncoloredDrawable;
505             drawables[ON] = new BitmapDrawable(context.getResources(),
506                     createBitmap(uncoloredDrawable.getBitmap(), highlightColor));
507             setDrawables(drawables);
508 
509             String[] labels = new String[drawables.length];
510             labels[OFF] = context.getString(R.string.lb_playback_controls_shuffle_enable);
511             labels[ON] = context.getString(R.string.lb_playback_controls_shuffle_disable);
512             setLabels(labels);
513         }
514     }
515 
516     /**
517      * An action for displaying a HQ (High Quality) icon.
518      */
519     public static class HighQualityAction extends MultiAction {
520         public static int OFF = 0;
521         public static int ON = 1;
522 
523         /**
524          * Constructor
525          * @param context Context used for loading resources.
526          */
527         public HighQualityAction(Context context) {
528             this(context, getIconHighlightColor(context));
529         }
530 
531         /**
532          * Constructor
533          * @param context Context used for loading resources.
534          * @param highlightColor Color for the highlighted icon state.
535          */
536         public HighQualityAction(Context context, int highlightColor) {
537             super(R.id.lb_control_high_quality);
538             BitmapDrawable uncoloredDrawable = (BitmapDrawable) getStyledDrawable(context,
539                     R.styleable.lbPlaybackControlsActionIcons_high_quality);
540             Drawable[] drawables = new Drawable[2];
541             drawables[OFF] = uncoloredDrawable;
542             drawables[ON] = new BitmapDrawable(context.getResources(),
543                     createBitmap(uncoloredDrawable.getBitmap(), highlightColor));
544             setDrawables(drawables);
545 
546             String[] labels = new String[drawables.length];
547             labels[OFF] = context.getString(R.string.lb_playback_controls_high_quality_enable);
548             labels[ON] = context.getString(R.string.lb_playback_controls_high_quality_disable);
549             setLabels(labels);
550         }
551     }
552 
553     /**
554      * An action for displaying a CC (Closed Captioning) icon.
555      */
556     public static class ClosedCaptioningAction extends MultiAction {
557         public static int OFF = 0;
558         public static int ON = 1;
559 
560         /**
561          * Constructor
562          * @param context Context used for loading resources.
563          */
564         public ClosedCaptioningAction(Context context) {
565             this(context, getIconHighlightColor(context));
566         }
567 
568         /**
569          * Constructor
570          * @param context Context used for loading resources.
571          * @param highlightColor Color for the highlighted icon state.
572          */
573         public ClosedCaptioningAction(Context context, int highlightColor) {
574             super(R.id.lb_control_closed_captioning);
575             BitmapDrawable uncoloredDrawable = (BitmapDrawable) getStyledDrawable(context,
576                     R.styleable.lbPlaybackControlsActionIcons_closed_captioning);
577             Drawable[] drawables = new Drawable[2];
578             drawables[OFF] = uncoloredDrawable;
579             drawables[ON] = new BitmapDrawable(context.getResources(),
580                     createBitmap(uncoloredDrawable.getBitmap(), highlightColor));
581             setDrawables(drawables);
582 
583             String[] labels = new String[drawables.length];
584             labels[OFF] = context.getString(R.string.lb_playback_controls_closed_captioning_enable);
585             labels[ON] = context.getString(R.string.lb_playback_controls_closed_captioning_disable);
586             setLabels(labels);
587         }
588     }
589 
590     private static Bitmap createBitmap(Bitmap bitmap, int color) {
591         Bitmap dst = bitmap.copy(bitmap.getConfig(), true);
592         Canvas canvas = new Canvas(dst);
593         Paint paint = new Paint();
594         paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
595         canvas.drawBitmap(bitmap, 0, 0, paint);
596         return dst;
597     }
598 
599     private static int getIconHighlightColor(Context context) {
600         TypedValue outValue = new TypedValue();
601         if (context.getTheme().resolveAttribute(R.attr.playbackControlsIconHighlightColor,
602                 outValue, true)) {
603             return outValue.data;
604         }
605         return context.getResources().getColor(R.color.lb_playback_icon_highlight_no_theme);
606     }
607 
608     private static Drawable getStyledDrawable(Context context, int index) {
609         TypedValue outValue = new TypedValue();
610         if (!context.getTheme().resolveAttribute(
611                 R.attr.playbackControlsActionIcons, outValue, false)) {
612             return null;
613         }
614         TypedArray array = context.getTheme().obtainStyledAttributes(outValue.data,
615                 R.styleable.lbPlaybackControlsActionIcons);
616         Drawable drawable = array.getDrawable(index);
617         array.recycle();
618         return drawable;
619     }
620 
621     private Object mItem;
622     private Drawable mImageDrawable;
623     private ObjectAdapter mPrimaryActionsAdapter;
624     private ObjectAdapter mSecondaryActionsAdapter;
625     private int mTotalTimeMs;
626     private int mCurrentTimeMs;
627     private int mBufferedProgressMs;
628     private OnPlaybackStateChangedListener mListener;
629 
630     /**
631      * Constructor for a PlaybackControlsRow that displays some details from
632      * the given item.
633      *
634      * @param item The main item for the row.
635      */
636     public PlaybackControlsRow(Object item) {
637         mItem = item;
638     }
639 
640     /**
641      * Constructor for a PlaybackControlsRow that has no item details.
642      */
643     public PlaybackControlsRow() {
644     }
645 
646     /**
647      * Returns the main item for the details page.
648      */
649     public final Object getItem() {
650         return mItem;
651     }
652 
653     /**
654      * Sets a {link @Drawable} image for this row.
655      * <p>If set after the row has been bound to a view, the adapter must be notified that
656      * this row has changed.</p>
657      *
658      * @param drawable The drawable to set.
659      */
660     public final void setImageDrawable(Drawable drawable) {
661         mImageDrawable = drawable;
662     }
663 
664     /**
665      * Sets a {@link Bitmap} for this row.
666      * <p>If set after the row has been bound to a view, the adapter must be notified that
667      * this row has changed.</p>
668      *
669      * @param context The context to retrieve display metrics from.
670      * @param bm The bitmap to set.
671      */
672     public final void setImageBitmap(Context context, Bitmap bm) {
673         mImageDrawable = new BitmapDrawable(context.getResources(), bm);
674     }
675 
676     /**
677      * Returns the image {@link Drawable} of this row.
678      *
679      * @return The overview's image drawable, or null if no drawable has been
680      *         assigned.
681      */
682     public final Drawable getImageDrawable() {
683         return mImageDrawable;
684     }
685 
686     /**
687      * Sets the primary actions {@link ObjectAdapter}.
688      * <p>If set after the row has been bound to a view, the adapter must be notified that
689      * this row has changed.</p>
690      */
691     public final void setPrimaryActionsAdapter(ObjectAdapter adapter) {
692         mPrimaryActionsAdapter = adapter;
693     }
694 
695     /**
696      * Sets the secondary actions {@link ObjectAdapter}.
697      * <p>If set after the row has been bound to a view, the adapter must be notified that
698      * this row has changed.</p>
699      */
700     public final void setSecondaryActionsAdapter(ObjectAdapter adapter) {
701         mSecondaryActionsAdapter = adapter;
702     }
703 
704     /**
705      * Returns the primary actions {@link ObjectAdapter}.
706      */
707     public final ObjectAdapter getPrimaryActionsAdapter() {
708         return mPrimaryActionsAdapter;
709     }
710 
711     /**
712      * Returns the secondary actions {@link ObjectAdapter}.
713      */
714     public final ObjectAdapter getSecondaryActionsAdapter() {
715         return mSecondaryActionsAdapter;
716     }
717 
718     /**
719      * Sets the total time in milliseconds for the playback controls row.
720      * <p>If set after the row has been bound to a view, the adapter must be notified that
721      * this row has changed.</p>
722      */
723     public void setTotalTime(int ms) {
724         mTotalTimeMs = ms;
725     }
726 
727     /**
728      * Returns the total time in milliseconds for the playback controls row.
729      */
730     public int getTotalTime() {
731         return mTotalTimeMs;
732     }
733 
734     /**
735      * Sets the current time in milliseconds for the playback controls row.
736      * If this row is bound to a view, the view will automatically
737      * be updated to reflect the new value.
738      */
739     public void setCurrentTime(int ms) {
740         if (mCurrentTimeMs != ms) {
741             mCurrentTimeMs = ms;
742             currentTimeChanged();
743         }
744     }
745 
746     /**
747      * Returns the current time in milliseconds for the playback controls row.
748      */
749     public int getCurrentTime() {
750         return mCurrentTimeMs;
751     }
752 
753     /**
754      * Sets the buffered progress for the playback controls row.
755      * If this row is bound to a view, the view will automatically
756      * be updated to reflect the new value.
757      */
758     public void setBufferedProgress(int ms) {
759         if (mBufferedProgressMs != ms) {
760             mBufferedProgressMs = ms;
761             bufferedProgressChanged();
762         }
763     }
764 
765     /**
766      * Returns the buffered progress for the playback controls row.
767      */
768     public int getBufferedProgress() {
769         return mBufferedProgressMs;
770     }
771 
772     /**
773      * Returns the Action associated with the given keycode, or null if no associated action exists.
774      * Searches the primary adapter first, then the secondary adapter.
775      */
776     public Action getActionForKeyCode(int keyCode) {
777         Action action = getActionForKeyCode(getPrimaryActionsAdapter(), keyCode);
778         if (action != null) {
779             return action;
780         }
781         return getActionForKeyCode(getSecondaryActionsAdapter(), keyCode);
782     }
783 
784     /**
785      * Returns the Action associated with the given keycode, or null if no associated action exists.
786      */
787     public Action getActionForKeyCode(ObjectAdapter adapter, int keyCode) {
788         if (adapter != mPrimaryActionsAdapter && adapter != mSecondaryActionsAdapter) {
789             throw new IllegalArgumentException("Invalid adapter");
790         }
791         for (int i = 0; i < adapter.size(); i++) {
792             Action action = (Action) adapter.get(i);
793             if (action.respondsToKeyCode(keyCode)) {
794                 return action;
795             }
796         }
797         return null;
798     }
799 
800     interface OnPlaybackStateChangedListener {
801         public void onCurrentTimeChanged(int currentTimeMs);
802         public void onBufferedProgressChanged(int bufferedProgressMs);
803     }
804 
805     /**
806      * Sets a listener to be called when the playback state changes.
807      */
808     void setOnPlaybackStateChangedListener(OnPlaybackStateChangedListener listener) {
809         mListener = listener;
810     }
811 
812     /**
813      * Returns the playback state listener.
814      */
815     OnPlaybackStateChangedListener getOnPlaybackStateChangedListener() {
816         return mListener;
817     }
818 
819     private void currentTimeChanged() {
820         if (mListener != null) {
821             mListener.onCurrentTimeChanged(mCurrentTimeMs);
822         }
823     }
824 
825     private void bufferedProgressChanged() {
826         if (mListener != null) {
827             mListener.onBufferedProgressChanged(mBufferedProgressMs);
828         }
829     }
830 }
831