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