PlaybackTransportRowPresenter.java revision 3bcad88cbf4488e747d84893c35f2351b8f84afe
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.support.v17.leanback.widget;
17
18import android.content.Context;
19import android.graphics.Bitmap;
20import android.graphics.Color;
21import android.os.Build;
22import android.support.annotation.ColorInt;
23import android.support.v17.leanback.R;
24import android.support.v17.leanback.widget.ControlBarPresenter.OnControlClickedListener;
25import android.support.v17.leanback.widget.ControlBarPresenter.OnControlSelectedListener;
26import android.util.TypedValue;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.ImageView;
32import android.widget.TextView;
33
34import java.util.Arrays;
35
36/**
37 * A PlaybackTransportRowPresenter renders a {@link PlaybackControlsRow} to display a
38 * series of playback control buttons. Typically this row will be the first row in a fragment
39 * such as the {@link android.support.v17.leanback.app.PlaybackSupportFragment}.
40 *
41 * <p>The detailed description is rendered using a {@link Presenter} passed in
42 * {@link #setDescriptionPresenter(Presenter)}.  This can be an instance of
43 * {@link AbstractDetailsDescriptionPresenter}.  The application can access the
44 * detailed description ViewHolder from {@link ViewHolder#getDescriptionViewHolder()}.
45 * </p>
46 */
47public class PlaybackTransportRowPresenter extends PlaybackRowPresenter {
48
49    static class BoundData extends PlaybackControlsPresenter.BoundData {
50        ViewHolder mRowViewHolder;
51    }
52
53    /**
54     * A ViewHolder for the PlaybackControlsRow supporting seek UI.
55     */
56    public class ViewHolder extends PlaybackRowPresenter.ViewHolder implements PlaybackSeekUi {
57        final Presenter.ViewHolder mDescriptionViewHolder;
58        final ImageView mImageView;
59        final ViewGroup mDescriptionDock;
60        final ViewGroup mControlsDock;
61        final ViewGroup mSecondaryControlsDock;
62        final TextView mTotalTime;
63        final TextView mCurrentTime;
64        final SeekBar mProgressBar;
65        final ThumbsBar mThumbsBar;
66        long mTotalTimeInMs = Long.MIN_VALUE;
67        long mCurrentTimeInMs = Long.MIN_VALUE;
68        long mSecondaryProgressInMs;
69        final StringBuilder mTempBuilder = new StringBuilder();
70        ControlBarPresenter.ViewHolder mControlsVh;
71        ControlBarPresenter.ViewHolder mSecondaryControlsVh;
72        BoundData mControlsBoundData = new BoundData();
73        BoundData mSecondaryBoundData = new BoundData();
74        Presenter.ViewHolder mSelectedViewHolder;
75        Object mSelectedItem;
76        PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
77        int mThumbHeroIndex = -1;
78
79        Client mSeekClient;
80        boolean mInSeek;
81        PlaybackSeekDataProvider mSeekDataProvider;
82        long[] mPositions;
83        int mPositionsLength;
84
85        final PlaybackControlsRow.OnPlaybackProgressCallback mListener =
86                new PlaybackControlsRow.OnPlaybackProgressCallback() {
87            @Override
88            public void onCurrentPositionChanged(PlaybackControlsRow row, long ms) {
89                setCurrentPosition(ms);
90            }
91
92            @Override
93            public void onDurationChanged(PlaybackControlsRow row, long ms) {
94                setTotalTime(ms);
95            }
96
97            @Override
98            public void onBufferedPositionChanged(PlaybackControlsRow row, long ms) {
99                setBufferedPosition(ms);
100            }
101        };
102
103        void updateProgressInSeek(boolean forward) {
104            long newPos;
105            long pos = mCurrentTimeInMs;
106            if (mPositionsLength > 0) {
107                int index = Arrays.binarySearch(mPositions, 0, mPositionsLength, pos);
108                int thumbHeroIndex;
109                if (forward) {
110                    if (index >= 0) {
111                        // found it, seek to neighbour key position at higher side
112                        if (index < mPositionsLength - 1) {
113                            newPos = mPositions[index + 1];
114                            thumbHeroIndex = index + 1;
115                        } else {
116                            newPos = mTotalTimeInMs;
117                            thumbHeroIndex = index;
118                        }
119                    } else {
120                        // not found, seek to neighbour key position at higher side.
121                        int insertIndex = -1 - index;
122                        if (insertIndex <= mPositionsLength - 1) {
123                            newPos = mPositions[insertIndex];
124                            thumbHeroIndex = insertIndex;
125                        } else {
126                            newPos = mTotalTimeInMs;
127                            thumbHeroIndex = insertIndex > 0 ? insertIndex - 1 : 0;
128                        }
129                    }
130                } else {
131                    if (index >= 0) {
132                        // found it, seek to neighbour key position at lower side.
133                        if (index > 0) {
134                            newPos = mPositions[index - 1];
135                            thumbHeroIndex = index - 1;
136                        } else {
137                            newPos = 0;
138                            thumbHeroIndex = 0;
139                        }
140                    } else {
141                        // not found, seek to neighbour key position at lower side.
142                        int insertIndex = -1 - index;
143                        if (insertIndex > 0) {
144                            newPos = mPositions[insertIndex - 1];
145                            thumbHeroIndex = insertIndex - 1;
146                        } else {
147                            newPos = 0;
148                            thumbHeroIndex = 0;
149                        }
150                    }
151                }
152                updateThumbsInSeek(thumbHeroIndex, forward);
153            } else {
154                long interval = (long) (mTotalTimeInMs * getDefaultSeekIncrement());
155                newPos = pos + (forward ? interval : -interval);
156                if (newPos > mTotalTimeInMs) {
157                    newPos = mTotalTimeInMs;
158                } else if (newPos < 0) {
159                    newPos = 0;
160                }
161            }
162            double ratio = (double) newPos / mTotalTimeInMs;     // Range: [0, 1]
163            mProgressBar.setProgress((int) (ratio * Integer.MAX_VALUE)); // Could safely cast to int
164            mSeekClient.onSeekPositionChanged(newPos);
165        }
166
167        void updateThumbsInSeek(int thumbHeroIndex, boolean forward) {
168            if (mThumbHeroIndex == thumbHeroIndex) {
169                return;
170            }
171
172            final int totalNum = mThumbsBar.getChildCount();
173            if (totalNum < 0 || (totalNum & 1) == 0) {
174                throw new RuntimeException();
175            }
176            final int heroChildIndex = totalNum / 2;
177            final int start = Math.max(thumbHeroIndex - (totalNum / 2), 0);
178            final int end = Math.min(thumbHeroIndex + (totalNum / 2), mPositionsLength - 1);
179            final int newRequestStart;
180            final int newRequestEnd;
181
182            if (mThumbHeroIndex < 0) {
183                // first time
184                newRequestStart = start;
185                newRequestEnd = end;
186            } else {
187                forward = thumbHeroIndex > mThumbHeroIndex;
188                final int oldStart = Math.max(mThumbHeroIndex - (totalNum / 2), 0);
189                final int oldEnd = Math.min(mThumbHeroIndex + (totalNum / 2),
190                        mPositionsLength - 1);
191                if (forward) {
192                    newRequestStart = Math.max(oldEnd + 1, start);
193                    newRequestEnd = end;
194                    // overlapping area directly assign bitmap from previous result
195                    for (int i = start; i <= newRequestStart - 1; i++) {
196                        mThumbsBar.setThumbBitmap(heroChildIndex + (i - thumbHeroIndex),
197                                mThumbsBar.getThumbBitmap(heroChildIndex + (i - mThumbHeroIndex)));
198                    }
199                } else {
200                    newRequestEnd = Math.min(oldStart - 1, end);
201                    newRequestStart = start;
202                    // overlapping area directly assign bitmap from previous result in backward
203                    for (int i = end; i >= newRequestEnd + 1; i--) {
204                        mThumbsBar.setThumbBitmap(heroChildIndex + (i - thumbHeroIndex),
205                                mThumbsBar.getThumbBitmap(heroChildIndex + (i - mThumbHeroIndex)));
206                    }
207                }
208            }
209            // processing new requests with mThumbHeroIndex updated
210            mThumbHeroIndex = thumbHeroIndex;
211            if (forward) {
212                for (int i = newRequestStart; i <= newRequestEnd; i++) {
213                    mSeekDataProvider.getThumbnail(i, mThumbResult);
214                }
215            } else {
216                for (int i = newRequestEnd; i >= newRequestStart; i--) {
217                    mSeekDataProvider.getThumbnail(i, mThumbResult);
218                }
219            }
220            // set thumb bitmaps outside (start , end) to null
221            for (int childIndex = 0; childIndex < heroChildIndex - mThumbHeroIndex + start;
222                    childIndex++) {
223                mThumbsBar.setThumbBitmap(childIndex, null);
224            }
225            for (int childIndex = heroChildIndex + end - mThumbHeroIndex + 1;
226                    childIndex < totalNum; childIndex++) {
227                mThumbsBar.setThumbBitmap(childIndex, null);
228            }
229        }
230
231        PlaybackSeekDataProvider.ResultCallback mThumbResult =
232                new PlaybackSeekDataProvider.ResultCallback() {
233                    @Override
234                    public void onThumbnailLoaded(Bitmap bitmap, int index) {
235                        int childIndex = index - (mThumbHeroIndex - mThumbsBar.getChildCount() / 2);
236                        if (childIndex < 0 || childIndex >= mThumbsBar.getChildCount()) {
237                            return;
238                        }
239                        mThumbsBar.setThumbBitmap(childIndex, bitmap);
240                    }
241        };
242
243        boolean onForward() {
244            if (!startSeek()) {
245                return false;
246            }
247            updateProgressInSeek(true);
248            return true;
249        }
250
251        boolean onBackward() {
252            if (!startSeek()) {
253                return false;
254            }
255            updateProgressInSeek(false);
256            return true;
257        }
258        /**
259         * Constructor of ViewHolder of PlaybackTransportRowPresenter
260         * @param rootView Root view of the ViewHolder.
261         * @param descriptionPresenter The presenter that will be used to create description
262         *                             ViewHolder. The description view will be added into tree.
263         */
264        public ViewHolder(View rootView, Presenter descriptionPresenter) {
265            super(rootView);
266            mImageView = (ImageView) rootView.findViewById(R.id.image);
267            mDescriptionDock = (ViewGroup) rootView.findViewById(R.id.description_dock);
268            mCurrentTime = (TextView) rootView.findViewById(R.id.current_time);
269            mTotalTime = (TextView) rootView.findViewById(R.id.total_time);
270            mProgressBar = (SeekBar) rootView.findViewById(R.id.playback_progress);
271            mProgressBar.setOnClickListener(new View.OnClickListener() {
272                @Override
273                public void onClick(View view) {
274                    onProgressBarClicked(ViewHolder.this);
275                }
276            });
277            mProgressBar.setOnKeyListener(new View.OnKeyListener() {
278
279                @Override
280                public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
281                    // when in seek only allow this keys
282                    switch (keyCode) {
283                        case KeyEvent.KEYCODE_DPAD_UP:
284                        case KeyEvent.KEYCODE_DPAD_DOWN:
285                            // eat DPAD UP/DOWN in seek mode
286                            return mInSeek;
287                        case KeyEvent.KEYCODE_DPAD_LEFT:
288                        case KeyEvent.KEYCODE_MINUS:
289                        case KeyEvent.KEYCODE_MEDIA_REWIND:
290                            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
291                                onBackward();
292                            }
293                            return true;
294                        case KeyEvent.KEYCODE_DPAD_RIGHT:
295                        case KeyEvent.KEYCODE_PLUS:
296                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
297                            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
298                                onForward();
299                            }
300                            return true;
301                        case KeyEvent.KEYCODE_DPAD_CENTER:
302                        case KeyEvent.KEYCODE_ENTER:
303                            if (!mInSeek) {
304                                return false;
305                            }
306                            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
307                                stopSeek(false);
308                            }
309                            return true;
310                        case KeyEvent.KEYCODE_BACK:
311                        case KeyEvent.KEYCODE_ESCAPE:
312                            if (!mInSeek) {
313                                return false;
314                            }
315                            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
316                                // SeekBar does not support cancel in accessibility mode, so always
317                                // "confirm" if accessibility is on.
318                                stopSeek(Build.VERSION.SDK_INT >= 21
319                                        ? !mProgressBar.isAccessibilityFocused() : true);
320                            }
321                            return true;
322                    }
323                    return false;
324                }
325            });
326            mProgressBar.setAccessibilitySeekListener(new SeekBar.AccessibilitySeekListener() {
327                @Override
328                public boolean onAccessibilitySeekForward() {
329                    return onForward();
330                }
331
332                @Override
333                public boolean onAccessibilitySeekBackward() {
334                    return onBackward();
335                }
336            });
337            mProgressBar.setMax(Integer.MAX_VALUE); //current progress will be a fraction of this
338            mControlsDock = (ViewGroup) rootView.findViewById(R.id.controls_dock);
339            mSecondaryControlsDock =
340                    (ViewGroup) rootView.findViewById(R.id.secondary_controls_dock);
341            mDescriptionViewHolder = descriptionPresenter == null ? null :
342                    descriptionPresenter.onCreateViewHolder(mDescriptionDock);
343            if (mDescriptionViewHolder != null) {
344                mDescriptionDock.addView(mDescriptionViewHolder.view);
345            }
346            mThumbsBar = (ThumbsBar) rootView.findViewById(R.id.thumbs_row);
347        }
348
349        /**
350         * @return The ViewHolder for description.
351         */
352        public final Presenter.ViewHolder getDescriptionViewHolder() {
353            return mDescriptionViewHolder;
354        }
355
356        @Override
357        public void setPlaybackSeekUiClient(Client client) {
358            mSeekClient = client;
359        }
360
361        boolean startSeek() {
362            if (mInSeek) {
363                return true;
364            }
365            if (mSeekClient == null || !mSeekClient.isSeekEnabled()
366                    || mTotalTimeInMs <= 0) {
367                return false;
368            }
369            mInSeek = true;
370            mSeekClient.onSeekStarted();
371            mSeekDataProvider = mSeekClient.getPlaybackSeekDataProvider();
372            mPositions = mSeekDataProvider != null ? mSeekDataProvider.getSeekPositions() : null;
373            if (mPositions != null) {
374                int pos = Arrays.binarySearch(mPositions, mTotalTimeInMs);
375                if (pos >= 0) {
376                    mPositionsLength = pos + 1;
377                } else {
378                    mPositionsLength = -1 - pos;
379                }
380            } else {
381                mPositionsLength = 0;
382            }
383            mControlsVh.view.setVisibility(View.INVISIBLE);
384            mSecondaryControlsVh.view.setVisibility(View.INVISIBLE);
385            mDescriptionViewHolder.view.setVisibility(View.INVISIBLE);
386            mThumbsBar.setVisibility(View.VISIBLE);
387            return true;
388        }
389
390        void stopSeek(boolean cancelled) {
391            if (!mInSeek) {
392                return;
393            }
394            mInSeek = false;
395            mSeekClient.onSeekFinished(cancelled);
396            if (mSeekDataProvider != null) {
397                mSeekDataProvider.reset();
398            }
399            mThumbHeroIndex = -1;
400            mThumbsBar.clearThumbBitmaps();
401            mSeekDataProvider = null;
402            mPositions = null;
403            mPositionsLength = 0;
404            mControlsVh.view.setVisibility(View.VISIBLE);
405            mSecondaryControlsVh.view.setVisibility(View.VISIBLE);
406            mDescriptionViewHolder.view.setVisibility(View.VISIBLE);
407            mThumbsBar.setVisibility(View.INVISIBLE);
408        }
409
410        void dispatchItemSelection() {
411            if (!isSelected()) {
412                return;
413            }
414            if (mSelectedViewHolder == null) {
415                if (getOnItemViewSelectedListener() != null) {
416                    getOnItemViewSelectedListener().onItemSelected(null, null,
417                            ViewHolder.this, getRow());
418                }
419            } else {
420                if (getOnItemViewSelectedListener() != null) {
421                    getOnItemViewSelectedListener().onItemSelected(mSelectedViewHolder,
422                            mSelectedItem, ViewHolder.this, getRow());
423                }
424            }
425        };
426
427        Presenter getPresenter(boolean primary) {
428            ObjectAdapter adapter = primary
429                    ? ((PlaybackControlsRow) getRow()).getPrimaryActionsAdapter()
430                    : ((PlaybackControlsRow) getRow()).getSecondaryActionsAdapter();
431            if (adapter == null) {
432                return null;
433            }
434            if (adapter.getPresenterSelector() instanceof ControlButtonPresenterSelector) {
435                ControlButtonPresenterSelector selector =
436                        (ControlButtonPresenterSelector) adapter.getPresenterSelector();
437                return selector.getSecondaryPresenter();
438            }
439            return adapter.getPresenter(adapter.size() > 0 ? adapter.get(0) : null);
440        }
441
442        /**
443         * Returns the TextView that showing total time label. This method might be used in
444         * {@link #onSetDurationLabel}.
445         * @return The TextView that showing total time label.
446         */
447        public final TextView getDurationView() {
448            return mTotalTime;
449        }
450
451        /**
452         * Called to update total time label. Default implementation updates the TextView
453         * {@link #getDurationView()}. Subclass might override.
454         * @param totalTimeMs Total duration of the media in milliseconds.
455         */
456        protected void onSetDurationLabel(long totalTimeMs) {
457            if (mTotalTime != null) {
458                formatTime(totalTimeMs, mTempBuilder);
459                mTotalTime.setText(mTempBuilder.toString());
460            }
461        }
462
463        void setTotalTime(long totalTimeMs) {
464            if (mTotalTimeInMs != totalTimeMs) {
465                mTotalTimeInMs = totalTimeMs;
466                onSetDurationLabel(totalTimeMs);
467            }
468        }
469
470        /**
471         * Returns the TextView that showing current position label. This method might be used in
472         * {@link #onSetCurrentPositionLabel}.
473         * @return The TextView that showing current position label.
474         */
475        public final TextView getCurrentPositionView() {
476            return mCurrentTime;
477        }
478
479        /**
480         * Called to update current time label. Default implementation updates the TextView
481         * {@link #getCurrentPositionView}. Subclass might override.
482         * @param currentTimeMs Current playback position in milliseconds.
483         */
484        protected void onSetCurrentPositionLabel(long currentTimeMs) {
485            if (mCurrentTime != null) {
486                formatTime(currentTimeMs, mTempBuilder);
487                mCurrentTime.setText(mTempBuilder.toString());
488            }
489        }
490
491        void setCurrentPosition(long currentTimeMs) {
492            if (currentTimeMs != mCurrentTimeInMs) {
493                mCurrentTimeInMs = currentTimeMs;
494                onSetCurrentPositionLabel(currentTimeMs);
495            }
496            if (!mInSeek) {
497                int progressRatio = 0;
498                if (mTotalTimeInMs > 0) {
499                    // Use ratio to represent current progres
500                    double ratio = (double) mCurrentTimeInMs / mTotalTimeInMs;     // Range: [0, 1]
501                    progressRatio = (int) (ratio * Integer.MAX_VALUE);  // Could safely cast to int
502                }
503                mProgressBar.setProgress((int) progressRatio);
504            }
505        }
506
507        void setBufferedPosition(long progressMs) {
508            mSecondaryProgressInMs = progressMs;
509            // Solve the progress bar by using ratio
510            double ratio = (double) progressMs / mTotalTimeInMs;           // Range: [0, 1]
511            double progressRatio = ratio * Integer.MAX_VALUE;   // Could safely cast to int
512            mProgressBar.setSecondaryProgress((int) progressRatio);
513        }
514    }
515
516    static void formatTime(long ms, StringBuilder sb) {
517        sb.setLength(0);
518        if (ms < 0) {
519            sb.append("--");
520            return;
521        }
522        long seconds = ms / 1000;
523        long minutes = seconds / 60;
524        long hours = minutes / 60;
525        seconds -= minutes * 60;
526        minutes -= hours * 60;
527
528        if (hours > 0) {
529            sb.append(hours).append(':');
530            if (minutes < 10) {
531                sb.append('0');
532            }
533        }
534        sb.append(minutes).append(':');
535        if (seconds < 10) {
536            sb.append('0');
537        }
538        sb.append(seconds);
539    }
540
541    float mDefaultSeekIncrement = 0.01f;
542    int mProgressColor = Color.TRANSPARENT;
543    boolean mProgressColorSet;
544    Presenter mDescriptionPresenter;
545    ControlBarPresenter mPlaybackControlsPresenter;
546    ControlBarPresenter mSecondaryControlsPresenter;
547    OnActionClickedListener mOnActionClickedListener;
548
549    private final OnControlSelectedListener mOnControlSelectedListener =
550            new OnControlSelectedListener() {
551        @Override
552        public void onControlSelected(Presenter.ViewHolder itemViewHolder, Object item,
553                ControlBarPresenter.BoundData data) {
554            ViewHolder vh = ((BoundData) data).mRowViewHolder;
555            if (vh.mSelectedViewHolder != itemViewHolder || vh.mSelectedItem != item) {
556                vh.mSelectedViewHolder = itemViewHolder;
557                vh.mSelectedItem = item;
558                vh.dispatchItemSelection();
559            }
560        }
561    };
562
563    private final OnControlClickedListener mOnControlClickedListener =
564            new OnControlClickedListener() {
565        @Override
566        public void onControlClicked(Presenter.ViewHolder itemViewHolder, Object item,
567                ControlBarPresenter.BoundData data) {
568            ViewHolder vh = ((BoundData) data).mRowViewHolder;
569            if (vh.getOnItemViewClickedListener() != null) {
570                vh.getOnItemViewClickedListener().onItemClicked(itemViewHolder, item,
571                        vh, vh.getRow());
572            }
573            if (mOnActionClickedListener != null && item instanceof Action) {
574                mOnActionClickedListener.onActionClicked((Action) item);
575            }
576        }
577    };
578
579    public PlaybackTransportRowPresenter() {
580        setHeaderPresenter(null);
581        setSelectEffectEnabled(false);
582
583        mPlaybackControlsPresenter = new ControlBarPresenter(R.layout.lb_control_bar);
584        mPlaybackControlsPresenter.setDefaultFocusToMiddle(false);
585        mSecondaryControlsPresenter = new ControlBarPresenter(R.layout.lb_control_bar);
586        mSecondaryControlsPresenter.setDefaultFocusToMiddle(false);
587
588        mPlaybackControlsPresenter.setOnControlSelectedListener(mOnControlSelectedListener);
589        mSecondaryControlsPresenter.setOnControlSelectedListener(mOnControlSelectedListener);
590        mPlaybackControlsPresenter.setOnControlClickedListener(mOnControlClickedListener);
591        mSecondaryControlsPresenter.setOnControlClickedListener(mOnControlClickedListener);
592    }
593
594    /**
595     * @param descriptionPresenter Presenter for displaying item details.
596     */
597    public void setDescriptionPresenter(Presenter descriptionPresenter) {
598        mDescriptionPresenter = descriptionPresenter;
599    }
600
601    /**
602     * Sets the listener for {@link Action} click events.
603     */
604    public void setOnActionClickedListener(OnActionClickedListener listener) {
605        mOnActionClickedListener = listener;
606    }
607
608    /**
609     * Returns the listener for {@link Action} click events.
610     */
611    public OnActionClickedListener getOnActionClickedListener() {
612        return mOnActionClickedListener;
613    }
614
615    /**
616     * Sets the primary color for the progress bar.  If not set, a default from
617     * the theme will be used.
618     */
619    public void setProgressColor(@ColorInt int color) {
620        mProgressColor = color;
621        mProgressColorSet = true;
622    }
623
624    /**
625     * Returns the primary color for the progress bar.  If no color was set, transparent
626     * is returned.
627     */
628    @ColorInt
629    public int getProgressColor() {
630        return mProgressColor;
631    }
632
633    @Override
634    public void onReappear(RowPresenter.ViewHolder rowViewHolder) {
635        ViewHolder vh = (ViewHolder) rowViewHolder;
636        if (vh.view.hasFocus()) {
637            vh.mProgressBar.requestFocus();
638        }
639    }
640
641    private int getDefaultProgressColor(Context context) {
642        TypedValue outValue = new TypedValue();
643        if (context.getTheme()
644                .resolveAttribute(R.attr.playbackProgressPrimaryColor, outValue, true)) {
645            return context.getResources().getColor(outValue.resourceId);
646        }
647        return context.getResources().getColor(R.color.lb_playback_progress_color_no_theme);
648    }
649
650    @Override
651    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
652        View v = LayoutInflater.from(parent.getContext()).inflate(
653                R.layout.lb_playback_transport_controls_row, parent, false);
654        ViewHolder vh = new ViewHolder(v, mDescriptionPresenter);
655        initRow(vh);
656        return vh;
657    }
658
659    private void initRow(final ViewHolder vh) {
660        vh.mControlsVh = (ControlBarPresenter.ViewHolder) mPlaybackControlsPresenter
661                .onCreateViewHolder(vh.mControlsDock);
662        vh.mProgressBar.setProgressColor(mProgressColorSet ? mProgressColor
663                : getDefaultProgressColor(vh.mControlsDock.getContext()));
664        vh.mControlsDock.addView(vh.mControlsVh.view);
665
666        vh.mSecondaryControlsVh = (ControlBarPresenter.ViewHolder) mSecondaryControlsPresenter
667                .onCreateViewHolder(vh.mSecondaryControlsDock);
668        vh.mSecondaryControlsDock.addView(vh.mSecondaryControlsVh.view);
669        ((PlaybackTransportRowView) vh.view).setOnUnhandledKeyListener(
670                new PlaybackTransportRowView.OnUnhandledKeyListener() {
671                @Override
672                public boolean onUnhandledKey(KeyEvent event) {
673                    if (vh.getOnKeyListener() != null) {
674                        if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) {
675                            return true;
676                        }
677                    }
678                    return false;
679                }
680            });
681    }
682
683    @Override
684    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
685        super.onBindRowViewHolder(holder, item);
686
687        ViewHolder vh = (ViewHolder) holder;
688        PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow();
689
690        if (row.getItem() == null) {
691            vh.mDescriptionDock.setVisibility(View.GONE);
692        } else {
693            vh.mDescriptionDock.setVisibility(View.VISIBLE);
694            if (vh.mDescriptionViewHolder != null) {
695                mDescriptionPresenter.onBindViewHolder(vh.mDescriptionViewHolder, row.getItem());
696            }
697        }
698
699        if (row.getImageDrawable() == null) {
700            vh.mImageView.setVisibility(View.GONE);
701        } else {
702            vh.mImageView.setVisibility(View.VISIBLE);
703        }
704        vh.mImageView.setImageDrawable(row.getImageDrawable());
705
706        vh.mControlsBoundData.adapter = row.getPrimaryActionsAdapter();
707        vh.mControlsBoundData.presenter = vh.getPresenter(true);
708        vh.mControlsBoundData.mRowViewHolder = vh;
709        mPlaybackControlsPresenter.onBindViewHolder(vh.mControlsVh, vh.mControlsBoundData);
710
711        vh.mSecondaryBoundData.adapter = row.getSecondaryActionsAdapter();
712        vh.mSecondaryBoundData.presenter = vh.getPresenter(false);
713        vh.mSecondaryBoundData.mRowViewHolder = vh;
714        mSecondaryControlsPresenter.onBindViewHolder(vh.mSecondaryControlsVh,
715                vh.mSecondaryBoundData);
716
717        vh.setTotalTime(row.getDuration());
718        vh.setCurrentPosition(row.getCurrentPosition());
719        vh.setBufferedPosition(row.getBufferedPosition());
720        row.setOnPlaybackProgressChangedListener(vh.mListener);
721    }
722
723    @Override
724    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
725        ViewHolder vh = (ViewHolder) holder;
726        PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow();
727
728        if (vh.mDescriptionViewHolder != null) {
729            mDescriptionPresenter.onUnbindViewHolder(vh.mDescriptionViewHolder);
730        }
731        mPlaybackControlsPresenter.onUnbindViewHolder(vh.mControlsVh);
732        mSecondaryControlsPresenter.onUnbindViewHolder(vh.mSecondaryControlsVh);
733        row.setOnPlaybackProgressChangedListener(null);
734
735        super.onUnbindRowViewHolder(holder);
736    }
737
738    /**
739     * Client of progress bar is clicked, default implementation delegate click to
740     * PlayPauseAction.
741     *
742     * @param vh ViewHolder of PlaybackTransportRowPresenter
743     */
744    protected void onProgressBarClicked(ViewHolder vh) {
745        if (vh != null) {
746            if (vh.mPlayPauseAction == null) {
747                vh.mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(vh.view.getContext());
748            }
749            if (vh.getOnItemViewClickedListener() != null) {
750                vh.getOnItemViewClickedListener().onItemClicked(vh, vh.mPlayPauseAction,
751                        vh, vh.getRow());
752            }
753            if (mOnActionClickedListener != null) {
754                mOnActionClickedListener.onActionClicked(vh.mPlayPauseAction);
755            }
756        }
757    }
758
759    /**
760     * Set default seek increment if {@link PlaybackSeekDataProvider} is null.
761     * @param ratio float value between 0(inclusive) and 1(inclusive).
762     */
763    public void setDefaultSeekIncrement(float ratio) {
764        mDefaultSeekIncrement = ratio;
765    }
766
767    /**
768     * Get default seek increment if {@link PlaybackSeekDataProvider} is null.
769     * @return float value between 0(inclusive) and 1(inclusive).
770     */
771    public float getDefaultSeekIncrement() {
772        return mDefaultSeekIncrement;
773    }
774
775    @Override
776    protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) {
777        super.onRowViewSelected(vh, selected);
778        if (selected) {
779            ((ViewHolder) vh).dispatchItemSelection();
780        }
781    }
782
783    @Override
784    protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) {
785        super.onRowViewAttachedToWindow(vh);
786        if (mDescriptionPresenter != null) {
787            mDescriptionPresenter.onViewAttachedToWindow(
788                    ((ViewHolder) vh).mDescriptionViewHolder);
789        }
790    }
791
792    @Override
793    protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) {
794        super.onRowViewDetachedFromWindow(vh);
795        if (mDescriptionPresenter != null) {
796            mDescriptionPresenter.onViewDetachedFromWindow(
797                    ((ViewHolder) vh).mDescriptionViewHolder);
798        }
799    }
800
801}
802