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
handleMessage(Message msg)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      */
PlaybackTransportControlGlue(Context context, T impl)121     public PlaybackTransportControlGlue(Context context, T impl) {
122         super(context, impl);
123     }
124 
125     @Override
setControlsRow(PlaybackControlsRow controlsRow)126     public void setControlsRow(PlaybackControlsRow controlsRow) {
127         super.setControlsRow(controlsRow);
128         sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
129         onUpdatePlaybackState();
130     }
131 
132     @Override
onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter)133     protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
134         primaryActionsAdapter.add(mPlayPauseAction =
135                 new PlaybackControlsRow.PlayPauseAction(getContext()));
136     }
137 
138     @Override
onCreateRowPresenter()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
onAttachedToHost(PlaybackGlueHost host)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
onDetachedFromHost()177     protected void onDetachedFromHost() {
178         super.onDetachedFromHost();
179 
180         if (getHost() instanceof PlaybackSeekUi) {
181             ((PlaybackSeekUi) getHost()).setPlaybackSeekUiClient(null);
182         }
183     }
184 
185     @Override
onUpdateProgress()186     protected void onUpdateProgress() {
187         if (!mPlaybackSeekUiClient.mIsSeek) {
188             super.onUpdateProgress();
189         }
190     }
191 
192     @Override
onActionClicked(Action action)193     public void onActionClicked(Action action) {
194         dispatchAction(action, null);
195     }
196 
197     @Override
onKey(View v, int keyCode, KeyEvent event)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 
onUpdatePlaybackStatusAfterUserAction()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      */
dispatchAction(Action action, KeyEvent keyEvent)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
onPlayStateChanged()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 
onUpdatePlaybackState()290     void onUpdatePlaybackState() {
291         mIsPlaying = mPlayerAdapter.isPlaying();
292         updatePlaybackState(mIsPlaying);
293     }
294 
updatePlaybackState(boolean isPlaying)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
getPlaybackSeekDataProvider()332         public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
333             return mSeekProvider;
334         }
335 
336         @Override
isSeekEnabled()337         public boolean isSeekEnabled() {
338             return mSeekProvider != null || mSeekEnabled;
339         }
340 
341         @Override
onSeekStarted()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
onSeekPositionChanged(long pos)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
onSeekFinished(boolean cancelled)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      */
setSeekProvider(PlaybackSeekDataProvider seekProvider)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      */
getSeekProvider()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      */
setSeekEnabled(boolean seekEnabled)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      */
isSeekEnabled()417     public final boolean isSeekEnabled() {
418         return mSeekEnabled;
419     }
420 }
421