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