1/* 2 * Copyright (C) 2016 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.os.Handler; 21import android.os.Message; 22import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; 23import android.support.v17.leanback.widget.Action; 24import android.support.v17.leanback.widget.ArrayObjectAdapter; 25import android.support.v17.leanback.widget.ObjectAdapter; 26import android.support.v17.leanback.widget.PlaybackControlsRow; 27import android.support.v17.leanback.widget.PlaybackRowPresenter; 28import android.support.v17.leanback.widget.PlaybackSeekDataProvider; 29import android.support.v17.leanback.widget.PlaybackSeekUi; 30import android.support.v17.leanback.widget.PlaybackTransportRowPresenter; 31import android.support.v17.leanback.widget.RowPresenter; 32import android.util.Log; 33import android.view.KeyEvent; 34import android.view.View; 35 36import java.lang.ref.WeakReference; 37 38/** 39 * A helper 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 in that manages interaction between the 42 * leanback UI components {@link PlaybackControlsRow} {@link PlaybackTransportRowPresenter} 43 * and a functional {@link PlayerAdapter} which represents the underlying 44 * media player. 45 * 46 * <p>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 actions bar: primary actions bar and secondary actions bar. App 51 * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or 52 * {@link #onCreateSecondaryActions} and respond to actions by override 53 * {@link #onActionClicked(Action)}. 54 * </p> 55 * 56 * <p> It's also subclass's responsibility to implement the "repeat mode" in 57 * {@link #onPlayCompleted()}. 58 * </p> 59 * 60 * <p> 61 * Apps calls {@link #setSeekProvider(PlaybackSeekDataProvider)} to provide seek data. If the 62 * {@link PlaybackGlueHost} is instance of {@link PlaybackSeekUi}, the provider will be passed to 63 * PlaybackGlueHost to render thumb bitmaps. 64 * </p> 65 * Sample Code: 66 * <pre><code> 67 * public class MyVideoFragment extends VideoFragment { 68 * @Override 69 * public void onCreate(Bundle savedInstanceState) { 70 * super.onCreate(savedInstanceState); 71 * final PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue = 72 * new PlaybackTransportControlGlue(getActivity(), 73 * new MediaPlayerAdapter(getActivity())); 74 * playerGlue.setHost(new VideoFragmentGlueHost(this)); 75 * playerGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() { 76 * @Override 77 * public void onPreparedStateChanged(PlaybackGlue glue) { 78 * if (glue.isPrepared()) { 79 * playerGlue.setSeekProvider(new MySeekProvider()); 80 * playerGlue.play(); 81 * } 82 * } 83 * }); 84 * playerGlue.setSubtitle("Leanback artist"); 85 * playerGlue.setTitle("Leanback team at work"); 86 * String uriPath = "android.resource://com.example.android.leanback/raw/video"; 87 * playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath)); 88 * } 89 * } 90 * </code></pre> 91 * @param <T> Type of {@link PlayerAdapter} passed in constructor. 92 */ 93public class PlaybackTransportControlGlue<T extends PlayerAdapter> 94 extends PlaybackBaseControlGlue<T> { 95 96 static final String TAG = "PlaybackTransportGlue"; 97 static final boolean DEBUG = false; 98 99 static final int MSG_UPDATE_PLAYBACK_STATE = 100; 100 static final int UPDATE_PLAYBACK_STATE_DELAY_MS = 2000; 101 102 PlaybackSeekDataProvider mSeekProvider; 103 boolean mSeekEnabled; 104 105 static class UpdatePlaybackStateHandler extends Handler { 106 @Override 107 public void handleMessage(Message msg) { 108 if (msg.what == MSG_UPDATE_PLAYBACK_STATE) { 109 PlaybackTransportControlGlue glue = 110 ((WeakReference<PlaybackTransportControlGlue>) msg.obj).get(); 111 if (glue != null) { 112 glue.onUpdatePlaybackState(); 113 } 114 } 115 } 116 } 117 118 static final Handler sHandler = new UpdatePlaybackStateHandler(); 119 120 final WeakReference<PlaybackBaseControlGlue> mGlueWeakReference = new WeakReference(this); 121 122 /** 123 * Constructor for the glue. 124 * 125 * @param context 126 * @param impl Implementation to underlying media player. 127 */ 128 public PlaybackTransportControlGlue(Context context, T impl) { 129 super(context, impl); 130 } 131 132 @Override 133 public void setControlsRow(PlaybackControlsRow controlsRow) { 134 super.setControlsRow(controlsRow); 135 sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference); 136 onUpdatePlaybackState(); 137 } 138 139 @Override 140 protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) { 141 primaryActionsAdapter.add(mPlayPauseAction = 142 new PlaybackControlsRow.PlayPauseAction(getContext())); 143 } 144 145 @Override 146 protected PlaybackRowPresenter onCreateRowPresenter() { 147 final AbstractDetailsDescriptionPresenter detailsPresenter = 148 new AbstractDetailsDescriptionPresenter() { 149 @Override 150 protected void onBindDescription(ViewHolder 151 viewHolder, Object obj) { 152 PlaybackBaseControlGlue glue = (PlaybackBaseControlGlue) obj; 153 viewHolder.getTitle().setText(glue.getTitle()); 154 viewHolder.getSubtitle().setText(glue.getSubtitle()); 155 } 156 }; 157 158 PlaybackTransportRowPresenter rowPresenter = new PlaybackTransportRowPresenter() { 159 @Override 160 protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { 161 super.onBindRowViewHolder(vh, item); 162 vh.setOnKeyListener(PlaybackTransportControlGlue.this); 163 } 164 @Override 165 protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { 166 super.onUnbindRowViewHolder(vh); 167 vh.setOnKeyListener(null); 168 } 169 }; 170 rowPresenter.setDescriptionPresenter(detailsPresenter); 171 return rowPresenter; 172 } 173 174 @Override 175 protected void onAttachedToHost(PlaybackGlueHost host) { 176 super.onAttachedToHost(host); 177 178 if (host instanceof PlaybackSeekUi) { 179 ((PlaybackSeekUi) host).setPlaybackSeekUiClient(mPlaybackSeekUiClient); 180 } 181 } 182 183 @Override 184 protected void onDetachedFromHost() { 185 super.onDetachedFromHost(); 186 187 if (getHost() instanceof PlaybackSeekUi) { 188 ((PlaybackSeekUi) getHost()).setPlaybackSeekUiClient(null); 189 } 190 } 191 192 @Override 193 void onUpdateProgress() { 194 if (mControlsRow != null && !mPlaybackSeekUiClient.mIsSeek) { 195 mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared() 196 ? mPlayerAdapter.getCurrentPosition() : -1); 197 } 198 } 199 200 @Override 201 public void onActionClicked(Action action) { 202 dispatchAction(action, null); 203 } 204 205 @Override 206 public boolean onKey(View v, int keyCode, KeyEvent event) { 207 switch (keyCode) { 208 case KeyEvent.KEYCODE_DPAD_UP: 209 case KeyEvent.KEYCODE_DPAD_DOWN: 210 case KeyEvent.KEYCODE_DPAD_RIGHT: 211 case KeyEvent.KEYCODE_DPAD_LEFT: 212 case KeyEvent.KEYCODE_BACK: 213 case KeyEvent.KEYCODE_ESCAPE: 214 return false; 215 } 216 217 final ObjectAdapter primaryActionsAdapter = mControlsRow.getPrimaryActionsAdapter(); 218 Action action = mControlsRow.getActionForKeyCode(primaryActionsAdapter, keyCode); 219 if (action == null) { 220 action = mControlsRow.getActionForKeyCode(mControlsRow.getSecondaryActionsAdapter(), 221 keyCode); 222 } 223 224 if (action != null) { 225 if (event.getAction() == KeyEvent.ACTION_DOWN) { 226 dispatchAction(action, event); 227 } 228 return true; 229 } 230 return false; 231 } 232 233 void onUpdatePlaybackStatusAfterUserAction() { 234 updatePlaybackState(mIsPlaying); 235 236 // Sync playback state after a delay 237 sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference); 238 sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE, 239 mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS); 240 } 241 242 /** 243 * Called when the given action is invoked, either by click or keyevent. 244 */ 245 boolean dispatchAction(Action action, KeyEvent keyEvent) { 246 boolean handled = false; 247 if (action instanceof PlaybackControlsRow.PlayPauseAction) { 248 boolean canPlay = keyEvent == null 249 || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE 250 || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY; 251 boolean canPause = keyEvent == null 252 || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE 253 || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE; 254 // PLAY_PAUSE PLAY PAUSE 255 // playing paused paused 256 // paused playing playing 257 // ff/rw playing playing paused 258 if (canPause 259 && (canPlay ? mIsPlaying : 260 !mIsPlaying)) { 261 mIsPlaying = false; 262 pause(); 263 } else if (canPlay && !mIsPlaying) { 264 mIsPlaying = true; 265 play(); 266 } 267 onUpdatePlaybackStatusAfterUserAction(); 268 handled = true; 269 } else if (action instanceof PlaybackControlsRow.SkipNextAction) { 270 next(); 271 handled = true; 272 } else if (action instanceof PlaybackControlsRow.SkipPreviousAction) { 273 previous(); 274 handled = true; 275 } 276 return handled; 277 } 278 279 @Override 280 protected void onPlayStateChanged() { 281 if (DEBUG) Log.v(TAG, "onStateChanged"); 282 283 if (sHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference)) { 284 sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference); 285 if (mPlayerAdapter.isPlaying() != mIsPlaying) { 286 if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update"); 287 sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE, 288 mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS); 289 } else { 290 if (DEBUG) Log.v(TAG, "Update state matches expectation"); 291 onUpdatePlaybackState(); 292 } 293 } else { 294 onUpdatePlaybackState(); 295 } 296 297 super.onPlayStateChanged(); 298 } 299 300 void onUpdatePlaybackState() { 301 mIsPlaying = mPlayerAdapter.isPlaying(); 302 updatePlaybackState(mIsPlaying); 303 } 304 305 private void updatePlaybackState(boolean isPlaying) { 306 if (mControlsRow == null) { 307 return; 308 } 309 310 if (!isPlaying) { 311 onUpdateProgress(); 312 mPlayerAdapter.setProgressUpdatingEnabled(mPlaybackSeekUiClient.mIsSeek); 313 } else { 314 mPlayerAdapter.setProgressUpdatingEnabled(true); 315 } 316 317 if (mFadeWhenPlaying && getHost() != null) { 318 getHost().setControlsOverlayAutoHideEnabled(isPlaying); 319 } 320 321 if (mPlayPauseAction != null) { 322 int index = !isPlaying 323 ? PlaybackControlsRow.PlayPauseAction.INDEX_PLAY 324 : PlaybackControlsRow.PlayPauseAction.INDEX_PAUSE; 325 if (mPlayPauseAction.getIndex() != index) { 326 mPlayPauseAction.setIndex(index); 327 notifyItemChanged((ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(), 328 mPlayPauseAction); 329 } 330 } 331 } 332 333 final SeekUiClient mPlaybackSeekUiClient = new SeekUiClient(); 334 335 class SeekUiClient extends PlaybackSeekUi.Client { 336 boolean mPausedBeforeSeek; 337 long mPositionBeforeSeek; 338 long mLastUserPosition; 339 boolean mIsSeek; 340 341 @Override 342 public PlaybackSeekDataProvider getPlaybackSeekDataProvider() { 343 return mSeekProvider; 344 } 345 346 @Override 347 public boolean isSeekEnabled() { 348 return mSeekProvider != null || mSeekEnabled; 349 } 350 351 @Override 352 public void onSeekStarted() { 353 mIsSeek = true; 354 mPausedBeforeSeek = !isPlaying(); 355 mPlayerAdapter.setProgressUpdatingEnabled(true); 356 // if we seek thumbnails, we don't need save original position because current 357 // position is not changed during seeking. 358 // otherwise we will call seekTo() and may need to restore the original position. 359 mPositionBeforeSeek = mSeekProvider == null ? mPlayerAdapter.getCurrentPosition() : -1; 360 mLastUserPosition = -1; 361 pause(); 362 } 363 364 @Override 365 public void onSeekPositionChanged(long pos) { 366 if (mSeekProvider == null) { 367 mPlayerAdapter.seekTo(pos); 368 } else { 369 mLastUserPosition = pos; 370 } 371 if (mControlsRow != null) { 372 mControlsRow.setCurrentPosition(pos); 373 } 374 } 375 376 @Override 377 public void onSeekFinished(boolean cancelled) { 378 if (!cancelled) { 379 if (mLastUserPosition > 0) { 380 seekTo(mLastUserPosition); 381 } 382 } else { 383 if (mPositionBeforeSeek >= 0) { 384 seekTo(mPositionBeforeSeek); 385 } 386 } 387 mIsSeek = false; 388 if (!mPausedBeforeSeek) { 389 play(); 390 } else { 391 mPlayerAdapter.setProgressUpdatingEnabled(false); 392 // we neeed update UI since PlaybackControlRow still saves previous position. 393 onUpdateProgress(); 394 } 395 } 396 }; 397 398 /** 399 * Set seek data provider used during user seeking. 400 * @param seekProvider Seek data provider used during user seeking. 401 */ 402 public final void setSeekProvider(PlaybackSeekDataProvider seekProvider) { 403 mSeekProvider = seekProvider; 404 } 405 406 /** 407 * Get seek data provider used during user seeking. 408 * @return Seek data provider used during user seeking. 409 */ 410 public final PlaybackSeekDataProvider getSeekProvider() { 411 return mSeekProvider; 412 } 413 414 /** 415 * Enable or disable seek when {@link #getSeekProvider()} is null. When true, 416 * {@link PlayerAdapter#seekTo(long)} will be called during user seeking. 417 * 418 * @param seekEnabled True to enable seek, false otherwise 419 */ 420 public final void setSeekEnabled(boolean seekEnabled) { 421 mSeekEnabled = seekEnabled; 422 } 423 424 /** 425 * @return True if seek is enabled without {@link PlaybackSeekDataProvider}, false otherwise. 426 */ 427 public final boolean isSeekEnabled() { 428 return mSeekEnabled; 429 } 430} 431