Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2016 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.os.Handler;
     21 import android.os.Message;
     22 import android.util.Log;
     23 import android.view.KeyEvent;
     24 import android.view.View;
     25 
     26 import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
     27 import androidx.leanback.widget.Action;
     28 import androidx.leanback.widget.ArrayObjectAdapter;
     29 import androidx.leanback.widget.ObjectAdapter;
     30 import androidx.leanback.widget.PlaybackControlsRow;
     31 import androidx.leanback.widget.PlaybackRowPresenter;
     32 import androidx.leanback.widget.PlaybackSeekDataProvider;
     33 import androidx.leanback.widget.PlaybackSeekUi;
     34 import androidx.leanback.widget.PlaybackTransportRowPresenter;
     35 import androidx.leanback.widget.RowPresenter;
     36 
     37 import java.lang.ref.WeakReference;
     38 
     39 /**
     40  * A helper class for managing a {@link PlaybackControlsRow} being displayed in
     41  * {@link PlaybackGlueHost}, it supports standard playback control actions play/pause, and
     42  * skip next/previous. This helper class is a glue layer in that manages interaction between the
     43  * leanback UI components {@link PlaybackControlsRow} {@link PlaybackTransportRowPresenter}
     44  * and a functional {@link PlayerAdapter} which represents the underlying
     45  * media player.
     46  *
     47  * <p>App must pass a {@link PlayerAdapter} in constructor for a specific
     48  * implementation e.g. a {@link MediaPlayerAdapter}.
     49  * </p>
     50  *
     51  * <p>The glue has two actions bar: primary actions bar and secondary actions bar. App
     52  * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or
     53  * {@link #onCreateSecondaryActions} and respond to actions by override
     54  * {@link #onActionClicked(Action)}.
     55  * </p>
     56  *
     57  * <p> It's also subclass's responsibility to implement the "repeat mode" in
     58  * {@link #onPlayCompleted()}.
     59  * </p>
     60  *
     61  * <p>
     62  * Apps calls {@link #setSeekProvider(PlaybackSeekDataProvider)} to provide seek data. If the
     63  * {@link PlaybackGlueHost} is instance of {@link PlaybackSeekUi}, the provider will be passed to
     64  * PlaybackGlueHost to render thumb bitmaps.
     65  * </p>
     66  * Sample Code:
     67  * <pre><code>
     68  * public class MyVideoFragment extends VideoFragment {
     69  *     &#64;Override
     70  *     public void onCreate(Bundle savedInstanceState) {
     71  *         super.onCreate(savedInstanceState);
     72  *         PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue =
     73  *                 new PlaybackTransportControlGlue(getActivity(),
     74  *                         new MediaPlayerAdapter(getActivity()));
     75  *         playerGlue.setHost(new VideoFragmentGlueHost(this));
     76  *         playerGlue.setSubtitle("Leanback artist");
     77  *         playerGlue.setTitle("Leanback team at work");
     78  *         String uriPath = "android.resource://com.example.android.leanback/raw/video";
     79  *         playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath));
     80  *         playerGlue.playWhenPrepared();
     81  *     }
     82  * }
     83  * </code></pre>
     84  * @param <T> Type of {@link PlayerAdapter} passed in constructor.
     85  */
     86 public class PlaybackTransportControlGlue<T extends PlayerAdapter>
     87         extends PlaybackBaseControlGlue<T> {
     88 
     89     static final String TAG = "PlaybackTransportGlue";
     90     static final boolean DEBUG = false;
     91 
     92     static final int MSG_UPDATE_PLAYBACK_STATE = 100;
     93     static final int UPDATE_PLAYBACK_STATE_DELAY_MS = 2000;
     94 
     95     PlaybackSeekDataProvider mSeekProvider;
     96     boolean mSeekEnabled;
     97 
     98     static class UpdatePlaybackStateHandler extends Handler {
     99         @Override
    100         public void handleMessage(Message msg) {
    101             if (msg.what == MSG_UPDATE_PLAYBACK_STATE) {
    102                 PlaybackTransportControlGlue glue =
    103                         ((WeakReference<PlaybackTransportControlGlue>) msg.obj).get();
    104                 if (glue != null) {
    105                     glue.onUpdatePlaybackState();
    106                 }
    107             }
    108         }
    109     }
    110 
    111     static final Handler sHandler = new UpdatePlaybackStateHandler();
    112 
    113     final WeakReference<PlaybackBaseControlGlue> mGlueWeakReference =  new WeakReference(this);
    114 
    115     /**
    116      * Constructor for the glue.
    117      *
    118      * @param context
    119      * @param impl Implementation to underlying media player.
    120      */
    121     public PlaybackTransportControlGlue(Context context, T impl) {
    122         super(context, impl);
    123     }
    124 
    125     @Override
    126     public void setControlsRow(PlaybackControlsRow controlsRow) {
    127         super.setControlsRow(controlsRow);
    128         sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
    129         onUpdatePlaybackState();
    130     }
    131 
    132     @Override
    133     protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
    134         primaryActionsAdapter.add(mPlayPauseAction =
    135                 new PlaybackControlsRow.PlayPauseAction(getContext()));
    136     }
    137 
    138     @Override
    139     protected PlaybackRowPresenter onCreateRowPresenter() {
    140         final AbstractDetailsDescriptionPresenter detailsPresenter =
    141                 new AbstractDetailsDescriptionPresenter() {
    142                     @Override
    143                     protected void onBindDescription(ViewHolder
    144                             viewHolder, Object obj) {
    145                         PlaybackBaseControlGlue glue = (PlaybackBaseControlGlue) obj;
    146                         viewHolder.getTitle().setText(glue.getTitle());
    147                         viewHolder.getSubtitle().setText(glue.getSubtitle());
    148                     }
    149                 };
    150 
    151         PlaybackTransportRowPresenter rowPresenter = new PlaybackTransportRowPresenter() {
    152             @Override
    153             protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
    154                 super.onBindRowViewHolder(vh, item);
    155                 vh.setOnKeyListener(PlaybackTransportControlGlue.this);
    156             }
    157             @Override
    158             protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
    159                 super.onUnbindRowViewHolder(vh);
    160                 vh.setOnKeyListener(null);
    161             }
    162         };
    163         rowPresenter.setDescriptionPresenter(detailsPresenter);
    164         return rowPresenter;
    165     }
    166 
    167     @Override
    168     protected void onAttachedToHost(PlaybackGlueHost host) {
    169         super.onAttachedToHost(host);
    170 
    171         if (host instanceof PlaybackSeekUi) {
    172             ((PlaybackSeekUi) host).setPlaybackSeekUiClient(mPlaybackSeekUiClient);
    173         }
    174     }
    175 
    176     @Override
    177     protected void onDetachedFromHost() {
    178         super.onDetachedFromHost();
    179 
    180         if (getHost() instanceof PlaybackSeekUi) {
    181             ((PlaybackSeekUi) getHost()).setPlaybackSeekUiClient(null);
    182         }
    183     }
    184 
    185     @Override
    186     protected void onUpdateProgress() {
    187         if (!mPlaybackSeekUiClient.mIsSeek) {
    188             super.onUpdateProgress();
    189         }
    190     }
    191 
    192     @Override
    193     public void onActionClicked(Action action) {
    194         dispatchAction(action, null);
    195     }
    196 
    197     @Override
    198     public boolean onKey(View v, int keyCode, KeyEvent event) {
    199         switch (keyCode) {
    200             case KeyEvent.KEYCODE_DPAD_UP:
    201             case KeyEvent.KEYCODE_DPAD_DOWN:
    202             case KeyEvent.KEYCODE_DPAD_RIGHT:
    203             case KeyEvent.KEYCODE_DPAD_LEFT:
    204             case KeyEvent.KEYCODE_BACK:
    205             case KeyEvent.KEYCODE_ESCAPE:
    206                 return false;
    207         }
    208 
    209         final ObjectAdapter primaryActionsAdapter = mControlsRow.getPrimaryActionsAdapter();
    210         Action action = mControlsRow.getActionForKeyCode(primaryActionsAdapter, keyCode);
    211         if (action == null) {
    212             action = mControlsRow.getActionForKeyCode(mControlsRow.getSecondaryActionsAdapter(),
    213                     keyCode);
    214         }
    215 
    216         if (action != null) {
    217             if (event.getAction() == KeyEvent.ACTION_DOWN) {
    218                 dispatchAction(action, event);
    219             }
    220             return true;
    221         }
    222         return false;
    223     }
    224 
    225     void onUpdatePlaybackStatusAfterUserAction() {
    226         updatePlaybackState(mIsPlaying);
    227 
    228         // Sync playback state after a delay
    229         sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
    230         sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE,
    231                 mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS);
    232     }
    233 
    234     /**
    235      * Called when the given action is invoked, either by click or keyevent.
    236      */
    237     boolean dispatchAction(Action action, KeyEvent keyEvent) {
    238         boolean handled = false;
    239         if (action instanceof PlaybackControlsRow.PlayPauseAction) {
    240             boolean canPlay = keyEvent == null
    241                     || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
    242                     || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY;
    243             boolean canPause = keyEvent == null
    244                     || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
    245                     || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE;
    246             //            PLAY_PAUSE    PLAY      PAUSE
    247             // playing    paused                  paused
    248             // paused     playing       playing
    249             // ff/rw      playing       playing   paused
    250             if (canPause && mIsPlaying) {
    251                 mIsPlaying = false;
    252                 pause();
    253             } else if (canPlay && !mIsPlaying) {
    254                 mIsPlaying = true;
    255                 play();
    256             }
    257             onUpdatePlaybackStatusAfterUserAction();
    258             handled = true;
    259         } else if (action instanceof PlaybackControlsRow.SkipNextAction) {
    260             next();
    261             handled = true;
    262         } else if (action instanceof PlaybackControlsRow.SkipPreviousAction) {
    263             previous();
    264             handled = true;
    265         }
    266         return handled;
    267     }
    268 
    269     @Override
    270     protected void onPlayStateChanged() {
    271         if (DEBUG) Log.v(TAG, "onStateChanged");
    272 
    273         if (sHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference)) {
    274             sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
    275             if (mPlayerAdapter.isPlaying() != mIsPlaying) {
    276                 if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update");
    277                 sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE,
    278                         mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS);
    279             } else {
    280                 if (DEBUG) Log.v(TAG, "Update state matches expectation");
    281                 onUpdatePlaybackState();
    282             }
    283         } else {
    284             onUpdatePlaybackState();
    285         }
    286 
    287         super.onPlayStateChanged();
    288     }
    289 
    290     void onUpdatePlaybackState() {
    291         mIsPlaying = mPlayerAdapter.isPlaying();
    292         updatePlaybackState(mIsPlaying);
    293     }
    294 
    295     private void updatePlaybackState(boolean isPlaying) {
    296         if (mControlsRow == null) {
    297             return;
    298         }
    299 
    300         if (!isPlaying) {
    301             onUpdateProgress();
    302             mPlayerAdapter.setProgressUpdatingEnabled(mPlaybackSeekUiClient.mIsSeek);
    303         } else {
    304             mPlayerAdapter.setProgressUpdatingEnabled(true);
    305         }
    306 
    307         if (mFadeWhenPlaying && getHost() != null) {
    308             getHost().setControlsOverlayAutoHideEnabled(isPlaying);
    309         }
    310 
    311         if (mPlayPauseAction != null) {
    312             int index = !isPlaying
    313                     ? PlaybackControlsRow.PlayPauseAction.INDEX_PLAY
    314                     : PlaybackControlsRow.PlayPauseAction.INDEX_PAUSE;
    315             if (mPlayPauseAction.getIndex() != index) {
    316                 mPlayPauseAction.setIndex(index);
    317                 notifyItemChanged((ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(),
    318                         mPlayPauseAction);
    319             }
    320         }
    321     }
    322 
    323     final SeekUiClient mPlaybackSeekUiClient = new SeekUiClient();
    324 
    325     class SeekUiClient extends PlaybackSeekUi.Client {
    326         boolean mPausedBeforeSeek;
    327         long mPositionBeforeSeek;
    328         long mLastUserPosition;
    329         boolean mIsSeek;
    330 
    331         @Override
    332         public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
    333             return mSeekProvider;
    334         }
    335 
    336         @Override
    337         public boolean isSeekEnabled() {
    338             return mSeekProvider != null || mSeekEnabled;
    339         }
    340 
    341         @Override
    342         public void onSeekStarted() {
    343             mIsSeek = true;
    344             mPausedBeforeSeek = !isPlaying();
    345             mPlayerAdapter.setProgressUpdatingEnabled(true);
    346             // if we seek thumbnails, we don't need save original position because current
    347             // position is not changed during seeking.
    348             // otherwise we will call seekTo() and may need to restore the original position.
    349             mPositionBeforeSeek = mSeekProvider == null ? mPlayerAdapter.getCurrentPosition() : -1;
    350             mLastUserPosition = -1;
    351             pause();
    352         }
    353 
    354         @Override
    355         public void onSeekPositionChanged(long pos) {
    356             if (mSeekProvider == null) {
    357                 mPlayerAdapter.seekTo(pos);
    358             } else {
    359                 mLastUserPosition = pos;
    360             }
    361             if (mControlsRow != null) {
    362                 mControlsRow.setCurrentPosition(pos);
    363             }
    364         }
    365 
    366         @Override
    367         public void onSeekFinished(boolean cancelled) {
    368             if (!cancelled) {
    369                 if (mLastUserPosition >= 0) {
    370                     seekTo(mLastUserPosition);
    371                 }
    372             } else {
    373                 if (mPositionBeforeSeek >= 0) {
    374                     seekTo(mPositionBeforeSeek);
    375                 }
    376             }
    377             mIsSeek = false;
    378             if (!mPausedBeforeSeek) {
    379                 play();
    380             } else {
    381                 mPlayerAdapter.setProgressUpdatingEnabled(false);
    382                 // we neeed update UI since PlaybackControlRow still saves previous position.
    383                 onUpdateProgress();
    384             }
    385         }
    386     };
    387 
    388     /**
    389      * Set seek data provider used during user seeking.
    390      * @param seekProvider Seek data provider used during user seeking.
    391      */
    392     public final void setSeekProvider(PlaybackSeekDataProvider seekProvider) {
    393         mSeekProvider = seekProvider;
    394     }
    395 
    396     /**
    397      * Get seek data provider used during user seeking.
    398      * @return Seek data provider used during user seeking.
    399      */
    400     public final PlaybackSeekDataProvider getSeekProvider() {
    401         return mSeekProvider;
    402     }
    403 
    404     /**
    405      * Enable or disable seek when {@link #getSeekProvider()} is null. When true,
    406      * {@link PlayerAdapter#seekTo(long)} will be called during user seeking.
    407      *
    408      * @param seekEnabled True to enable seek, false otherwise
    409      */
    410     public final void setSeekEnabled(boolean seekEnabled) {
    411         mSeekEnabled = seekEnabled;
    412     }
    413 
    414     /**
    415      * @return True if seek is enabled without {@link PlaybackSeekDataProvider}, false otherwise.
    416      */
    417     public final boolean isSeekEnabled() {
    418         return mSeekEnabled;
    419     }
    420 }
    421