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