Home | History | Annotate | Download | only in media
      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 
     17 package androidx.leanback.media;
     18 
     19 import android.content.Context;
     20 import android.graphics.drawable.Drawable;
     21 import android.text.TextUtils;
     22 import android.util.Log;
     23 import android.view.KeyEvent;
     24 import android.view.View;
     25 
     26 import androidx.annotation.CallSuper;
     27 import androidx.leanback.widget.Action;
     28 import androidx.leanback.widget.ArrayObjectAdapter;
     29 import androidx.leanback.widget.ControlButtonPresenterSelector;
     30 import androidx.leanback.widget.OnActionClickedListener;
     31 import androidx.leanback.widget.PlaybackControlsRow;
     32 import androidx.leanback.widget.PlaybackRowPresenter;
     33 import androidx.leanback.widget.PlaybackTransportRowPresenter;
     34 import androidx.leanback.widget.Presenter;
     35 
     36 import java.util.List;
     37 
     38 /**
     39  * A base abstract class for managing a {@link PlaybackControlsRow} being displayed in
     40  * {@link PlaybackGlueHost}. It supports standard playback control actions play/pause and
     41  * skip next/previous. This helper class is a glue layer that manages interaction between the
     42  * leanback UI components {@link PlaybackControlsRow} {@link PlaybackRowPresenter}
     43  * and a functional {@link PlayerAdapter} which represents the underlying
     44  * media player.
     45  *
     46  * <p>The app must pass a {@link PlayerAdapter} in constructor for a specific
     47  * implementation e.g. a {@link MediaPlayerAdapter}.
     48  * </p>
     49  *
     50  * <p>The glue has two action bars: primary action bars and secondary action bars. Apps
     51  * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or
     52  * {@link #onCreateSecondaryActions} and respond to actions by overriding
     53  * {@link #onActionClicked(Action)}.
     54  * </p>
     55  *
     56  * <p>The subclass is responsible for implementing the "repeat mode" in
     57  * {@link #onPlayCompleted()}.
     58  * </p>
     59  *
     60  * @param <T> Type of {@link PlayerAdapter} passed in constructor.
     61  */
     62 public abstract class PlaybackBaseControlGlue<T extends PlayerAdapter> extends PlaybackGlue
     63         implements OnActionClickedListener, View.OnKeyListener {
     64 
     65     /**
     66      * The adapter key for the first custom control on the left side
     67      * of the predefined primary controls.
     68      */
     69     public static final int ACTION_CUSTOM_LEFT_FIRST = 0x1;
     70 
     71     /**
     72      * The adapter key for the skip to previous control.
     73      */
     74     public static final int ACTION_SKIP_TO_PREVIOUS = 0x10;
     75 
     76     /**
     77      * The adapter key for the rewind control.
     78      */
     79     public static final int ACTION_REWIND = 0x20;
     80 
     81     /**
     82      * The adapter key for the play/pause control.
     83      */
     84     public static final int ACTION_PLAY_PAUSE = 0x40;
     85 
     86     /**
     87      * The adapter key for the fast forward control.
     88      */
     89     public static final int ACTION_FAST_FORWARD = 0x80;
     90 
     91     /**
     92      * The adapter key for the skip to next control.
     93      */
     94     public static final int ACTION_SKIP_TO_NEXT = 0x100;
     95 
     96     /**
     97      * The adapter key for the repeat control.
     98      */
     99     public static final int ACTION_REPEAT = 0x200;
    100 
    101     /**
    102      * The adapter key for the shuffle control.
    103      */
    104     public static final int ACTION_SHUFFLE = 0x400;
    105 
    106     /**
    107      * The adapter key for the first custom control on the right side
    108      * of the predefined primary controls.
    109      */
    110     public static final int ACTION_CUSTOM_RIGHT_FIRST = 0x1000;
    111 
    112     static final String TAG = "PlaybackTransportGlue";
    113     static final boolean DEBUG = false;
    114 
    115     final T mPlayerAdapter;
    116     PlaybackControlsRow mControlsRow;
    117     PlaybackRowPresenter mControlsRowPresenter;
    118     PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
    119     boolean mIsPlaying = false;
    120     boolean mFadeWhenPlaying = true;
    121 
    122     CharSequence mSubtitle;
    123     CharSequence mTitle;
    124     Drawable mCover;
    125 
    126     PlaybackGlueHost.PlayerCallback mPlayerCallback;
    127     boolean mBuffering = false;
    128     int mVideoWidth = 0;
    129     int mVideoHeight = 0;
    130     boolean mErrorSet = false;
    131     int mErrorCode;
    132     String mErrorMessage;
    133 
    134     final PlayerAdapter.Callback mAdapterCallback = new PlayerAdapter
    135             .Callback() {
    136 
    137         @Override
    138         public void onPlayStateChanged(PlayerAdapter wrapper) {
    139             if (DEBUG) Log.v(TAG, "onPlayStateChanged");
    140             PlaybackBaseControlGlue.this.onPlayStateChanged();
    141         }
    142 
    143         @Override
    144         public void onCurrentPositionChanged(PlayerAdapter wrapper) {
    145             if (DEBUG) Log.v(TAG, "onCurrentPositionChanged");
    146             PlaybackBaseControlGlue.this.onUpdateProgress();
    147         }
    148 
    149         @Override
    150         public void onBufferedPositionChanged(PlayerAdapter wrapper) {
    151             if (DEBUG) Log.v(TAG, "onBufferedPositionChanged");
    152             PlaybackBaseControlGlue.this.onUpdateBufferedProgress();
    153         }
    154 
    155         @Override
    156         public void onDurationChanged(PlayerAdapter wrapper) {
    157             if (DEBUG) Log.v(TAG, "onDurationChanged");
    158             PlaybackBaseControlGlue.this.onUpdateDuration();
    159         }
    160 
    161         @Override
    162         public void onPlayCompleted(PlayerAdapter wrapper) {
    163             if (DEBUG) Log.v(TAG, "onPlayCompleted");
    164             PlaybackBaseControlGlue.this.onPlayCompleted();
    165         }
    166 
    167         @Override
    168         public void onPreparedStateChanged(PlayerAdapter wrapper) {
    169             if (DEBUG) Log.v(TAG, "onPreparedStateChanged");
    170             PlaybackBaseControlGlue.this.onPreparedStateChanged();
    171         }
    172 
    173         @Override
    174         public void onVideoSizeChanged(PlayerAdapter wrapper, int width, int height) {
    175             mVideoWidth = width;
    176             mVideoHeight = height;
    177             if (mPlayerCallback != null) {
    178                 mPlayerCallback.onVideoSizeChanged(width, height);
    179             }
    180         }
    181 
    182         @Override
    183         public void onError(PlayerAdapter wrapper, int errorCode, String errorMessage) {
    184             mErrorSet = true;
    185             mErrorCode = errorCode;
    186             mErrorMessage = errorMessage;
    187             if (mPlayerCallback != null) {
    188                 mPlayerCallback.onError(errorCode, errorMessage);
    189             }
    190         }
    191 
    192         @Override
    193         public void onBufferingStateChanged(PlayerAdapter wrapper, boolean start) {
    194             mBuffering = start;
    195             if (mPlayerCallback != null) {
    196                 mPlayerCallback.onBufferingStateChanged(start);
    197             }
    198         }
    199 
    200         @Override
    201         public void onMetadataChanged(PlayerAdapter wrapper) {
    202             PlaybackBaseControlGlue.this.onMetadataChanged();
    203         }
    204     };
    205 
    206     /**
    207      * Constructor for the glue.
    208      *
    209      * @param context
    210      * @param impl Implementation to underlying media player.
    211      */
    212     public PlaybackBaseControlGlue(Context context, T impl) {
    213         super(context);
    214         mPlayerAdapter = impl;
    215         mPlayerAdapter.setCallback(mAdapterCallback);
    216     }
    217 
    218     public final T getPlayerAdapter() {
    219         return mPlayerAdapter;
    220     }
    221 
    222     @Override
    223     protected void onAttachedToHost(PlaybackGlueHost host) {
    224         super.onAttachedToHost(host);
    225         host.setOnKeyInterceptListener(this);
    226         host.setOnActionClickedListener(this);
    227         onCreateDefaultControlsRow();
    228         onCreateDefaultRowPresenter();
    229         host.setPlaybackRowPresenter(getPlaybackRowPresenter());
    230         host.setPlaybackRow(getControlsRow());
    231 
    232         mPlayerCallback = host.getPlayerCallback();
    233         onAttachHostCallback();
    234         mPlayerAdapter.onAttachedToHost(host);
    235     }
    236 
    237     void onAttachHostCallback() {
    238         if (mPlayerCallback != null) {
    239             if (mVideoWidth != 0 && mVideoHeight != 0) {
    240                 mPlayerCallback.onVideoSizeChanged(mVideoWidth, mVideoHeight);
    241             }
    242             if (mErrorSet) {
    243                 mPlayerCallback.onError(mErrorCode, mErrorMessage);
    244             }
    245             mPlayerCallback.onBufferingStateChanged(mBuffering);
    246         }
    247     }
    248 
    249     void onDetachHostCallback() {
    250         mErrorSet = false;
    251         mErrorCode = 0;
    252         mErrorMessage = null;
    253         if (mPlayerCallback != null) {
    254             mPlayerCallback.onBufferingStateChanged(false);
    255         }
    256     }
    257 
    258     @Override
    259     protected void onHostStart() {
    260         mPlayerAdapter.setProgressUpdatingEnabled(true);
    261     }
    262 
    263     @Override
    264     protected void onHostStop() {
    265         mPlayerAdapter.setProgressUpdatingEnabled(false);
    266     }
    267 
    268     @Override
    269     protected void onDetachedFromHost() {
    270         onDetachHostCallback();
    271         mPlayerCallback = null;
    272         mPlayerAdapter.onDetachedFromHost();
    273         mPlayerAdapter.setProgressUpdatingEnabled(false);
    274         super.onDetachedFromHost();
    275     }
    276 
    277     void onCreateDefaultControlsRow() {
    278         if (mControlsRow == null) {
    279             PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
    280             setControlsRow(controlsRow);
    281         }
    282     }
    283 
    284     void onCreateDefaultRowPresenter() {
    285         if (mControlsRowPresenter == null) {
    286             setPlaybackRowPresenter(onCreateRowPresenter());
    287         }
    288     }
    289 
    290     protected abstract PlaybackRowPresenter onCreateRowPresenter();
    291 
    292     /**
    293      * Sets the controls to auto hide after a timeout when media is playing.
    294      * @param enable True to enable auto hide after a timeout when media is playing.
    295      * @see PlaybackGlueHost#setControlsOverlayAutoHideEnabled(boolean)
    296      */
    297     public void setControlsOverlayAutoHideEnabled(boolean enable) {
    298         mFadeWhenPlaying = enable;
    299         if (!mFadeWhenPlaying && getHost() != null) {
    300             getHost().setControlsOverlayAutoHideEnabled(false);
    301         }
    302     }
    303 
    304     /**
    305      * Returns true if the controls auto hides after a timeout when media is playing.
    306      * @see PlaybackGlueHost#isControlsOverlayAutoHideEnabled()
    307      */
    308     public boolean isControlsOverlayAutoHideEnabled() {
    309         return mFadeWhenPlaying;
    310     }
    311 
    312     /**
    313      * Sets the controls row to be managed by the glue layer. If
    314      * {@link PlaybackControlsRow#getPrimaryActionsAdapter()} is not provided, a default
    315      * {@link ArrayObjectAdapter} will be created and initialized in
    316      * {@link #onCreatePrimaryActions(ArrayObjectAdapter)}. If
    317      * {@link PlaybackControlsRow#getSecondaryActionsAdapter()} is not provided, a default
    318      * {@link ArrayObjectAdapter} will be created and initialized in
    319      * {@link #onCreateSecondaryActions(ArrayObjectAdapter)}.
    320      * The primary actions and playback state related aspects of the row
    321      * are updated by the glue.
    322      */
    323     public void setControlsRow(PlaybackControlsRow controlsRow) {
    324         mControlsRow = controlsRow;
    325         mControlsRow.setCurrentPosition(-1);
    326         mControlsRow.setDuration(-1);
    327         mControlsRow.setBufferedPosition(-1);
    328         if (mControlsRow.getPrimaryActionsAdapter() == null) {
    329             ArrayObjectAdapter adapter = new ArrayObjectAdapter(
    330                     new ControlButtonPresenterSelector());
    331             onCreatePrimaryActions(adapter);
    332             mControlsRow.setPrimaryActionsAdapter(adapter);
    333         }
    334         // Add secondary actions
    335         if (mControlsRow.getSecondaryActionsAdapter() == null) {
    336             ArrayObjectAdapter secondaryActions = new ArrayObjectAdapter(
    337                     new ControlButtonPresenterSelector());
    338             onCreateSecondaryActions(secondaryActions);
    339             getControlsRow().setSecondaryActionsAdapter(secondaryActions);
    340         }
    341         updateControlsRow();
    342     }
    343 
    344     /**
    345      * Sets the controls row Presenter to be managed by the glue layer.
    346      */
    347     public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
    348         mControlsRowPresenter = presenter;
    349     }
    350 
    351     /**
    352      * Returns the playback controls row managed by the glue layer.
    353      */
    354     public PlaybackControlsRow getControlsRow() {
    355         return mControlsRow;
    356     }
    357 
    358     /**
    359      * Returns the playback controls row Presenter managed by the glue layer.
    360      */
    361     public PlaybackRowPresenter getPlaybackRowPresenter() {
    362         return mControlsRowPresenter;
    363     }
    364 
    365     /**
    366      * Handles action clicks.  A subclass may override this add support for additional actions.
    367      */
    368     @Override
    369     public abstract void onActionClicked(Action action);
    370 
    371     /**
    372      * Handles key events and returns true if handled.  A subclass may override this to provide
    373      * additional support.
    374      */
    375     @Override
    376     public abstract boolean onKey(View v, int keyCode, KeyEvent event);
    377 
    378     private void updateControlsRow() {
    379         onMetadataChanged();
    380     }
    381 
    382     @Override
    383     public final boolean isPlaying() {
    384         return mPlayerAdapter.isPlaying();
    385     }
    386 
    387     @Override
    388     public void play() {
    389         mPlayerAdapter.play();
    390     }
    391 
    392     @Override
    393     public void pause() {
    394         mPlayerAdapter.pause();
    395     }
    396 
    397     @Override
    398     public void next() {
    399         mPlayerAdapter.next();
    400     }
    401 
    402     @Override
    403     public void previous() {
    404         mPlayerAdapter.previous();
    405     }
    406 
    407     protected static void notifyItemChanged(ArrayObjectAdapter adapter, Object object) {
    408         int index = adapter.indexOf(object);
    409         if (index >= 0) {
    410             adapter.notifyArrayItemRangeChanged(index, 1);
    411         }
    412     }
    413 
    414     /**
    415      * May be overridden to add primary actions to the adapter. Default implementation add
    416      * {@link PlaybackControlsRow.PlayPauseAction}.
    417      *
    418      * @param primaryActionsAdapter The adapter to add primary {@link Action}s.
    419      */
    420     protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
    421     }
    422 
    423     /**
    424      * May be overridden to add secondary actions to the adapter.
    425      *
    426      * @param secondaryActionsAdapter The adapter you need to add the {@link Action}s to.
    427      */
    428     protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
    429     }
    430 
    431     @CallSuper
    432     protected void onUpdateProgress() {
    433         if (mControlsRow != null) {
    434             mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared()
    435                     ? getCurrentPosition() : -1);
    436         }
    437     }
    438 
    439     @CallSuper
    440     protected void onUpdateBufferedProgress() {
    441         if (mControlsRow != null) {
    442             mControlsRow.setBufferedPosition(mPlayerAdapter.getBufferedPosition());
    443         }
    444     }
    445 
    446     @CallSuper
    447     protected void onUpdateDuration() {
    448         if (mControlsRow != null) {
    449             mControlsRow.setDuration(
    450                     mPlayerAdapter.isPrepared() ? mPlayerAdapter.getDuration() : -1);
    451         }
    452     }
    453 
    454     /**
    455      * @return The duration of the media item in milliseconds.
    456      */
    457     public final long getDuration() {
    458         return mPlayerAdapter.getDuration();
    459     }
    460 
    461     /**
    462      * @return The current position of the media item in milliseconds.
    463      */
    464     public long getCurrentPosition() {
    465         return mPlayerAdapter.getCurrentPosition();
    466     }
    467 
    468     /**
    469      * @return The current buffered position of the media item in milliseconds.
    470      */
    471     public final long getBufferedPosition() {
    472         return mPlayerAdapter.getBufferedPosition();
    473     }
    474 
    475     @Override
    476     public final boolean isPrepared() {
    477         return mPlayerAdapter.isPrepared();
    478     }
    479 
    480     /**
    481      * Event when ready state for play changes.
    482      */
    483     @CallSuper
    484     protected void onPreparedStateChanged() {
    485         onUpdateDuration();
    486         List<PlayerCallback> callbacks = getPlayerCallbacks();
    487         if (callbacks != null) {
    488             for (int i = 0, size = callbacks.size(); i < size; i++) {
    489                 callbacks.get(i).onPreparedStateChanged(this);
    490             }
    491         }
    492     }
    493 
    494     /**
    495      * Sets the drawable representing cover image. The drawable will be rendered by default
    496      * description presenter in
    497      * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
    498      * @param cover The drawable representing cover image.
    499      */
    500     public void setArt(Drawable cover) {
    501         if (mCover == cover) {
    502             return;
    503         }
    504         this.mCover = cover;
    505         mControlsRow.setImageDrawable(mCover);
    506         if (getHost() != null) {
    507             getHost().notifyPlaybackRowChanged();
    508         }
    509     }
    510 
    511     /**
    512      * @return The drawable representing cover image.
    513      */
    514     public Drawable getArt() {
    515         return mCover;
    516     }
    517 
    518     /**
    519      * Sets the media subtitle. The subtitle will be rendered by default description presenter
    520      * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
    521      * @param subtitle Subtitle to set.
    522      */
    523     public void setSubtitle(CharSequence subtitle) {
    524         if (TextUtils.equals(subtitle, mSubtitle)) {
    525             return;
    526         }
    527         mSubtitle = subtitle;
    528         if (getHost() != null) {
    529             getHost().notifyPlaybackRowChanged();
    530         }
    531     }
    532 
    533     /**
    534      * Return The media subtitle.
    535      */
    536     public CharSequence getSubtitle() {
    537         return mSubtitle;
    538     }
    539 
    540     /**
    541      * Sets the media title. The title will be rendered by default description presenter
    542      * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
    543      */
    544     public void setTitle(CharSequence title) {
    545         if (TextUtils.equals(title, mTitle)) {
    546             return;
    547         }
    548         mTitle = title;
    549         if (getHost() != null) {
    550             getHost().notifyPlaybackRowChanged();
    551         }
    552     }
    553 
    554     /**
    555      * Returns the title of the media item.
    556      */
    557     public CharSequence getTitle() {
    558         return mTitle;
    559     }
    560 
    561     /**
    562      * Event when metadata changed
    563      */
    564     protected void onMetadataChanged() {
    565         if (mControlsRow == null) {
    566             return;
    567         }
    568 
    569         if (DEBUG) Log.v(TAG, "updateRowMetadata");
    570 
    571         mControlsRow.setImageDrawable(getArt());
    572         mControlsRow.setDuration(getDuration());
    573         mControlsRow.setCurrentPosition(getCurrentPosition());
    574 
    575         if (getHost() != null) {
    576             getHost().notifyPlaybackRowChanged();
    577         }
    578     }
    579 
    580     /**
    581      * Event when play state changed.
    582      */
    583     @CallSuper
    584     protected void onPlayStateChanged() {
    585         List<PlayerCallback> callbacks = getPlayerCallbacks();
    586         if (callbacks != null) {
    587             for (int i = 0, size = callbacks.size(); i < size; i++) {
    588                 callbacks.get(i).onPlayStateChanged(this);
    589             }
    590         }
    591     }
    592 
    593     /**
    594      * Event when play finishes, subclass may handling repeat mode here.
    595      */
    596     @CallSuper
    597     protected void onPlayCompleted() {
    598         List<PlayerCallback> callbacks = getPlayerCallbacks();
    599         if (callbacks != null) {
    600             for (int i = 0, size = callbacks.size(); i < size; i++) {
    601                 callbacks.get(i).onPlayCompleted(this);
    602             }
    603         }
    604     }
    605 
    606     /**
    607      * Seek media to a new position.
    608      * @param position New position.
    609      */
    610     public final void seekTo(long position) {
    611         mPlayerAdapter.seekTo(position);
    612     }
    613 
    614     /**
    615      * Returns a bitmask of actions supported by the media player.
    616      */
    617     public long getSupportedActions() {
    618         return mPlayerAdapter.getSupportedActions();
    619     }
    620 }
    621