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