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
17package android.support.v17.leanback.media;
18
19import android.content.Context;
20import android.os.Handler;
21import android.os.Message;
22import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
23import android.support.v17.leanback.widget.Action;
24import android.support.v17.leanback.widget.ArrayObjectAdapter;
25import android.support.v17.leanback.widget.ObjectAdapter;
26import android.support.v17.leanback.widget.PlaybackControlsRow;
27import android.support.v17.leanback.widget.PlaybackRowPresenter;
28import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
29import android.support.v17.leanback.widget.PlaybackSeekUi;
30import android.support.v17.leanback.widget.PlaybackTransportRowPresenter;
31import android.support.v17.leanback.widget.RowPresenter;
32import android.util.Log;
33import android.view.KeyEvent;
34import android.view.View;
35
36import 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 */
93public 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
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     */
128    public PlaybackTransportControlGlue(Context context, T impl) {
129        super(context, impl);
130    }
131
132    @Override
133    public void setControlsRow(PlaybackControlsRow controlsRow) {
134        super.setControlsRow(controlsRow);
135        sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
136        onUpdatePlaybackState();
137    }
138
139    @Override
140    protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
141        primaryActionsAdapter.add(mPlayPauseAction =
142                new PlaybackControlsRow.PlayPauseAction(getContext()));
143    }
144
145    @Override
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
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
184    protected void onDetachedFromHost() {
185        super.onDetachedFromHost();
186
187        if (getHost() instanceof PlaybackSeekUi) {
188            ((PlaybackSeekUi) getHost()).setPlaybackSeekUiClient(null);
189        }
190    }
191
192    @Override
193    void onUpdateProgress() {
194        if (mControlsRow != null && !mPlaybackSeekUiClient.mIsSeek) {
195            mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared()
196                    ? mPlayerAdapter.getCurrentPosition() : -1);
197        }
198    }
199
200    @Override
201    public void onActionClicked(Action action) {
202        dispatchAction(action, null);
203    }
204
205    @Override
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
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     */
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
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
300    void onUpdatePlaybackState() {
301        mIsPlaying = mPlayerAdapter.isPlaying();
302        updatePlaybackState(mIsPlaying);
303    }
304
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
342        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
343            return mSeekProvider;
344        }
345
346        @Override
347        public boolean isSeekEnabled() {
348            return mSeekProvider != null || mSeekEnabled;
349        }
350
351        @Override
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
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
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     */
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     */
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     */
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     */
427    public final boolean isSeekEnabled() {
428        return mSeekEnabled;
429    }
430}
431