Home | History | Annotate | Download | only in widget
      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 
    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 
    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 
    244         boolean onForward() {
    245             if (!startSeek()) {
    246                 return false;
    247             }
    248             updateProgressInSeek(true);
    249             return true;
    250         }
    251 
    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          */
    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          */
    353         public final Presenter.ViewHolder getDescriptionViewHolder() {
    354             return mDescriptionViewHolder;
    355         }
    356 
    357         @Override
    358         public void setPlaybackSeekUiClient(Client client) {
    359             mSeekClient = client;
    360         }
    361 
    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 
    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 
    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 
    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          */
    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          */
    457         protected void onSetDurationLabel(long totalTimeMs) {
    458             if (mTotalTime != null) {
    459                 formatTime(totalTimeMs, mTempBuilder);
    460                 mTotalTime.setText(mTempBuilder.toString());
    461             }
    462         }
    463 
    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          */
    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          */
    485         protected void onSetCurrentPositionLabel(long currentTimeMs) {
    486             if (mCurrentTime != null) {
    487                 formatTime(currentTimeMs, mTempBuilder);
    488                 mCurrentTime.setText(mTempBuilder.toString());
    489             }
    490         }
    491 
    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 
    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 
    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 
    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      */
    598     public void setDescriptionPresenter(Presenter descriptionPresenter) {
    599         mDescriptionPresenter = descriptionPresenter;
    600     }
    601 
    602     /**
    603      * Sets the listener for {@link Action} click events.
    604      */
    605     public void setOnActionClickedListener(OnActionClickedListener listener) {
    606         mOnActionClickedListener = listener;
    607     }
    608 
    609     /**
    610      * Returns the listener for {@link Action} click events.
    611      */
    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      */
    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
    630     public int getProgressColor() {
    631         return mProgressColor;
    632     }
    633 
    634     @Override
    635     public void onReappear(RowPresenter.ViewHolder rowViewHolder) {
    636         ViewHolder vh = (ViewHolder) rowViewHolder;
    637         if (vh.view.hasFocus()) {
    638             vh.mProgressBar.requestFocus();
    639         }
    640     }
    641 
    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
    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 
    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
    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
    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      */
    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      */
    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      */
    772     public float getDefaultSeekIncrement() {
    773         return mDefaultSeekIncrement;
    774     }
    775 
    776     @Override
    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
    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
    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