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