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