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 android.support.v17.leanback.media; 18 19 import android.content.Context; 20 import android.graphics.drawable.Drawable; 21 import android.support.annotation.CallSuper; 22 import android.support.v17.leanback.widget.Action; 23 import android.support.v17.leanback.widget.ArrayObjectAdapter; 24 import android.support.v17.leanback.widget.ControlButtonPresenterSelector; 25 import android.support.v17.leanback.widget.OnActionClickedListener; 26 import android.support.v17.leanback.widget.PlaybackControlsRow; 27 import android.support.v17.leanback.widget.PlaybackRowPresenter; 28 import android.support.v17.leanback.widget.PlaybackTransportRowPresenter; 29 import android.support.v17.leanback.widget.Presenter; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.view.KeyEvent; 33 import android.view.View; 34 35 import 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 */ 61 public 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 */ PlaybackBaseControlGlue(Context context, T impl)159 public PlaybackBaseControlGlue(Context context, T impl) { 160 super(context); 161 mPlayerAdapter = impl; 162 mPlayerAdapter.setCallback(mAdapterCallback); 163 } 164 getPlayerAdapter()165 public final T getPlayerAdapter() { 166 return mPlayerAdapter; 167 } 168 169 @Override onAttachedToHost(PlaybackGlueHost host)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 onAttachHostCallback()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 onDetachHostCallback()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 onHostStart()206 protected void onHostStart() { 207 mPlayerAdapter.setProgressUpdatingEnabled(true); 208 } 209 210 @Override onHostStop()211 protected void onHostStop() { 212 mPlayerAdapter.setProgressUpdatingEnabled(false); 213 } 214 215 @Override onDetachedFromHost()216 protected void onDetachedFromHost() { 217 onDetachHostCallback(); 218 mPlayerCallback = null; 219 mPlayerAdapter.onDetachedFromHost(); 220 mPlayerAdapter.setProgressUpdatingEnabled(false); 221 super.onDetachedFromHost(); 222 } 223 onCreateDefaultControlsRow()224 void onCreateDefaultControlsRow() { 225 if (mControlsRow == null) { 226 PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); 227 setControlsRow(controlsRow); 228 } 229 } 230 onCreateDefaultRowPresenter()231 void onCreateDefaultRowPresenter() { 232 if (mControlsRowPresenter == null) { 233 setPlaybackRowPresenter(onCreateRowPresenter()); 234 } 235 } 236 onCreateRowPresenter()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 */ setControlsOverlayAutoHideEnabled(boolean enable)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 */ isControlsOverlayAutoHideEnabled()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 */ setControlsRow(PlaybackControlsRow controlsRow)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 */ setPlaybackRowPresenter(PlaybackRowPresenter presenter)294 public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) { 295 mControlsRowPresenter = presenter; 296 } 297 298 /** 299 * Returns the playback controls row managed by the glue layer. 300 */ getControlsRow()301 public PlaybackControlsRow getControlsRow() { 302 return mControlsRow; 303 } 304 305 /** 306 * Returns the playback controls row Presenter managed by the glue layer. 307 */ getPlaybackRowPresenter()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 onActionClicked(Action action)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 onKey(View v, int keyCode, KeyEvent event)323 public abstract boolean onKey(View v, int keyCode, KeyEvent event); 324 updateControlsRow()325 private void updateControlsRow() { 326 onMetadataChanged(); 327 } 328 329 @Override isPlaying()330 public final boolean isPlaying() { 331 return mPlayerAdapter.isPlaying(); 332 } 333 334 @Override play()335 public void play() { 336 mPlayerAdapter.play(); 337 } 338 339 @Override pause()340 public void pause() { 341 mPlayerAdapter.pause(); 342 } 343 notifyItemChanged(ArrayObjectAdapter adapter, Object object)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 */ onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter)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 */ onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)365 protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) { 366 } 367 onUpdateProgress()368 void onUpdateProgress() { 369 if (mControlsRow != null) { 370 mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared() 371 ? getCurrentPosition() : -1); 372 } 373 } 374 onUpdateBufferedProgress()375 void onUpdateBufferedProgress() { 376 if (mControlsRow != null) { 377 mControlsRow.setBufferedPosition(mPlayerAdapter.getBufferedPosition()); 378 } 379 } 380 onUpdateDuration()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 */ getDuration()391 public final long getDuration() { 392 return mPlayerAdapter.getDuration(); 393 } 394 395 /** 396 * @return The current position of the media item in milliseconds. 397 */ getCurrentPosition()398 public long getCurrentPosition() { 399 return mPlayerAdapter.getCurrentPosition(); 400 } 401 402 /** 403 * @return The current buffered position of the media item in milliseconds. 404 */ getBufferedPosition()405 public final long getBufferedPosition() { 406 return mPlayerAdapter.getBufferedPosition(); 407 } 408 409 @Override isPrepared()410 public final boolean isPrepared() { 411 return mPlayerAdapter.isPrepared(); 412 } 413 414 /** 415 * Event when ready state for play changes. 416 */ 417 @CallSuper onPreparedStateChanged()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 */ setArt(Drawable cover)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 */ getArt()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 */ setSubtitle(CharSequence subtitle)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 */ getSubtitle()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 */ setTitle(CharSequence title)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 */ getTitle()491 public CharSequence getTitle() { 492 return mTitle; 493 } 494 495 /** 496 * Event when metadata changed 497 */ onMetadataChanged()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 onPlayStateChanged()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 onPlayCompleted()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 */ seekTo(long position)544 public final void seekTo(long position) { 545 mPlayerAdapter.seekTo(position); 546 } 547 548 } 549