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 
17 package androidx.leanback.media;
18 
19 import android.content.Context;
20 import android.graphics.drawable.Drawable;
21 import android.text.TextUtils;
22 import android.util.Log;
23 import android.view.KeyEvent;
24 import android.view.View;
25 
26 import androidx.annotation.CallSuper;
27 import androidx.leanback.widget.Action;
28 import androidx.leanback.widget.ArrayObjectAdapter;
29 import androidx.leanback.widget.ControlButtonPresenterSelector;
30 import androidx.leanback.widget.OnActionClickedListener;
31 import androidx.leanback.widget.PlaybackControlsRow;
32 import androidx.leanback.widget.PlaybackRowPresenter;
33 import androidx.leanback.widget.PlaybackTransportRowPresenter;
34 import androidx.leanback.widget.Presenter;
35 
36 import 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  */
62 public 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      */
PlaybackBaseControlGlue(Context context, T impl)212     public PlaybackBaseControlGlue(Context context, T impl) {
213         super(context);
214         mPlayerAdapter = impl;
215         mPlayerAdapter.setCallback(mAdapterCallback);
216     }
217 
getPlayerAdapter()218     public final T getPlayerAdapter() {
219         return mPlayerAdapter;
220     }
221 
222     @Override
onAttachedToHost(PlaybackGlueHost host)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 
onAttachHostCallback()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 
onDetachHostCallback()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
onHostStart()259     protected void onHostStart() {
260         mPlayerAdapter.setProgressUpdatingEnabled(true);
261     }
262 
263     @Override
onHostStop()264     protected void onHostStop() {
265         mPlayerAdapter.setProgressUpdatingEnabled(false);
266     }
267 
268     @Override
onDetachedFromHost()269     protected void onDetachedFromHost() {
270         onDetachHostCallback();
271         mPlayerCallback = null;
272         mPlayerAdapter.onDetachedFromHost();
273         mPlayerAdapter.setProgressUpdatingEnabled(false);
274         super.onDetachedFromHost();
275     }
276 
onCreateDefaultControlsRow()277     void onCreateDefaultControlsRow() {
278         if (mControlsRow == null) {
279             PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
280             setControlsRow(controlsRow);
281         }
282     }
283 
onCreateDefaultRowPresenter()284     void onCreateDefaultRowPresenter() {
285         if (mControlsRowPresenter == null) {
286             setPlaybackRowPresenter(onCreateRowPresenter());
287         }
288     }
289 
onCreateRowPresenter()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      */
setControlsOverlayAutoHideEnabled(boolean enable)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      */
isControlsOverlayAutoHideEnabled()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      */
setControlsRow(PlaybackControlsRow controlsRow)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      */
setPlaybackRowPresenter(PlaybackRowPresenter presenter)347     public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
348         mControlsRowPresenter = presenter;
349     }
350 
351     /**
352      * Returns the playback controls row managed by the glue layer.
353      */
getControlsRow()354     public PlaybackControlsRow getControlsRow() {
355         return mControlsRow;
356     }
357 
358     /**
359      * Returns the playback controls row Presenter managed by the glue layer.
360      */
getPlaybackRowPresenter()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
onActionClicked(Action action)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
onKey(View v, int keyCode, KeyEvent event)376     public abstract boolean onKey(View v, int keyCode, KeyEvent event);
377 
updateControlsRow()378     private void updateControlsRow() {
379         onMetadataChanged();
380     }
381 
382     @Override
isPlaying()383     public final boolean isPlaying() {
384         return mPlayerAdapter.isPlaying();
385     }
386 
387     @Override
play()388     public void play() {
389         mPlayerAdapter.play();
390     }
391 
392     @Override
pause()393     public void pause() {
394         mPlayerAdapter.pause();
395     }
396 
397     @Override
next()398     public void next() {
399         mPlayerAdapter.next();
400     }
401 
402     @Override
previous()403     public void previous() {
404         mPlayerAdapter.previous();
405     }
406 
notifyItemChanged(ArrayObjectAdapter adapter, Object object)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      */
onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter)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      */
onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)428     protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
429     }
430 
431     @CallSuper
onUpdateProgress()432     protected void onUpdateProgress() {
433         if (mControlsRow != null) {
434             mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared()
435                     ? getCurrentPosition() : -1);
436         }
437     }
438 
439     @CallSuper
onUpdateBufferedProgress()440     protected void onUpdateBufferedProgress() {
441         if (mControlsRow != null) {
442             mControlsRow.setBufferedPosition(mPlayerAdapter.getBufferedPosition());
443         }
444     }
445 
446     @CallSuper
onUpdateDuration()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      */
getDuration()457     public final long getDuration() {
458         return mPlayerAdapter.getDuration();
459     }
460 
461     /**
462      * @return The current position of the media item in milliseconds.
463      */
getCurrentPosition()464     public long getCurrentPosition() {
465         return mPlayerAdapter.getCurrentPosition();
466     }
467 
468     /**
469      * @return The current buffered position of the media item in milliseconds.
470      */
getBufferedPosition()471     public final long getBufferedPosition() {
472         return mPlayerAdapter.getBufferedPosition();
473     }
474 
475     @Override
isPrepared()476     public final boolean isPrepared() {
477         return mPlayerAdapter.isPrepared();
478     }
479 
480     /**
481      * Event when ready state for play changes.
482      */
483     @CallSuper
onPreparedStateChanged()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      */
setArt(Drawable cover)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      */
getArt()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      */
setSubtitle(CharSequence subtitle)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      */
getSubtitle()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      */
setTitle(CharSequence title)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      */
getTitle()557     public CharSequence getTitle() {
558         return mTitle;
559     }
560 
561     /**
562      * Event when metadata changed
563      */
onMetadataChanged()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
onPlayStateChanged()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
onPlayCompleted()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      */
seekTo(long position)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      */
getSupportedActions()617     public long getSupportedActions() {
618         return mPlayerAdapter.getSupportedActions();
619     }
620 }
621