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