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 com.android.tv.dvr.ui.playback; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.graphics.drawable.Drawable; 22 import android.media.MediaMetadata; 23 import android.media.session.MediaController; 24 import android.media.session.MediaController.TransportControls; 25 import android.media.session.PlaybackState; 26 import android.media.tv.TvTrackInfo; 27 import android.os.Bundle; 28 import android.support.annotation.Nullable; 29 import androidx.leanback.media.PlaybackControlGlue; 30 import androidx.leanback.widget.AbstractDetailsDescriptionPresenter; 31 import androidx.leanback.widget.Action; 32 import androidx.leanback.widget.ArrayObjectAdapter; 33 import androidx.leanback.widget.PlaybackControlsRow; 34 import androidx.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction; 35 import androidx.leanback.widget.PlaybackControlsRow.MultiAction; 36 import androidx.leanback.widget.PlaybackControlsRowPresenter; 37 import androidx.leanback.widget.RowPresenter; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.view.KeyEvent; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import com.android.tv.R; 44 import com.android.tv.util.TimeShiftUtils; 45 import java.util.ArrayList; 46 47 /** 48 * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and send 49 * command to the media controller. It also helps to update playback states displayed in the 50 * fragment according to information the media session provides. 51 */ 52 class DvrPlaybackControlHelper extends PlaybackControlGlue { 53 private static final String TAG = "DvrPlaybackControlHelpr"; 54 private static final boolean DEBUG = false; 55 56 private static final int AUDIO_ACTION_ID = 1001; 57 private static final long INVALID_TIME = -1; 58 59 private int mPlaybackState = PlaybackState.STATE_NONE; 60 private int mPlaybackSpeedLevel; 61 private int mPlaybackSpeedId; 62 private long mProgramStartTimeMs = INVALID_TIME; 63 private boolean mEnableBuffering = false; 64 private boolean mReadyToControl; 65 66 private final DvrPlaybackOverlayFragment mFragment; 67 private final MediaController mMediaController; 68 private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); 69 private final TransportControls mTransportControls; 70 private final int mExtraPaddingTopForNoDescription; 71 private final MultiAction mClosedCaptioningAction; 72 private final MultiAction mMultiAudioAction; 73 private ArrayObjectAdapter mSecondaryActionsAdapter; 74 private PlaybackControlsRow mPlaybackControlsRow; 75 @Nullable private View mPlayPauseButton; 76 DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment)77 DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { 78 super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); 79 mFragment = overlayFragment; 80 mMediaController = activity.getMediaController(); 81 mMediaController.registerCallback(mMediaControllerCallback); 82 mTransportControls = mMediaController.getTransportControls(); 83 mExtraPaddingTopForNoDescription = 84 activity.getResources() 85 .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); 86 mClosedCaptioningAction = new ClosedCaptioningAction(activity); 87 mMultiAudioAction = new MultiAudioAction(activity); 88 mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs(); 89 if (mProgramStartTimeMs != INVALID_TIME) { 90 mEnableBuffering = true; 91 } 92 createControlsRowPresenter(); 93 } 94 createControlsRow()95 void createControlsRow() { 96 mPlaybackControlsRow = new PlaybackControlsRow(this); 97 setControlsRow(mPlaybackControlsRow); 98 mSecondaryActionsAdapter = 99 (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter(); 100 } 101 createControlsRowPresenter()102 private void createControlsRowPresenter() { 103 AbstractDetailsDescriptionPresenter detailsPresenter = 104 new AbstractDetailsDescriptionPresenter() { 105 @Override 106 protected void onBindDescription( 107 AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, 108 Object object) { 109 PlaybackControlGlue glue = (PlaybackControlGlue) object; 110 if (glue.hasValidMedia()) { 111 viewHolder.getTitle().setText(glue.getMediaTitle()); 112 viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); 113 } else { 114 viewHolder.getTitle().setText(""); 115 viewHolder.getSubtitle().setText(""); 116 } 117 if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { 118 viewHolder.view.setPadding( 119 viewHolder.view.getPaddingLeft(), 120 mExtraPaddingTopForNoDescription, 121 viewHolder.view.getPaddingRight(), 122 viewHolder.view.getPaddingBottom()); 123 } 124 } 125 }; 126 PlaybackControlsRowPresenter presenter = 127 new PlaybackControlsRowPresenter(detailsPresenter) { 128 @Override 129 protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { 130 super.onBindRowViewHolder(vh, item); 131 vh.setOnKeyListener(DvrPlaybackControlHelper.this); 132 ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar); 133 mPlayPauseButton = controlBar.getChildAt(1); 134 } 135 136 @Override 137 protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { 138 super.onUnbindRowViewHolder(vh); 139 vh.setOnKeyListener(null); 140 } 141 }; 142 presenter.setProgressColor( 143 getContext().getResources().getColor(R.color.play_controls_progress_bar_watched)); 144 presenter.setBackgroundColor( 145 getContext() 146 .getResources() 147 .getColor(R.color.play_controls_body_background_enabled)); 148 setControlsRowPresenter(presenter); 149 } 150 151 @Override onActionClicked(Action action)152 public void onActionClicked(Action action) { 153 if (mReadyToControl) { 154 int trackType; 155 if (action.getId() == mClosedCaptioningAction.getId()) { 156 trackType = TvTrackInfo.TYPE_SUBTITLE; 157 } else if (action.getId() == AUDIO_ACTION_ID) { 158 trackType = TvTrackInfo.TYPE_AUDIO; 159 } else { 160 super.onActionClicked(action); 161 return; 162 } 163 ArrayList<TvTrackInfo> trackInfos = mFragment.getTracks(trackType); 164 if (!trackInfos.isEmpty()) { 165 showSideFragment(trackInfos, mFragment.getSelectedTrackId(trackType)); 166 } 167 } 168 } 169 170 @Override onKey(View v, int keyCode, KeyEvent event)171 public boolean onKey(View v, int keyCode, KeyEvent event) { 172 return mReadyToControl && super.onKey(v, keyCode, event); 173 } 174 175 @Override hasValidMedia()176 public boolean hasValidMedia() { 177 PlaybackState playbackState = mMediaController.getPlaybackState(); 178 return playbackState != null; 179 } 180 181 @Override isMediaPlaying()182 public boolean isMediaPlaying() { 183 PlaybackState playbackState = mMediaController.getPlaybackState(); 184 if (playbackState == null) { 185 return false; 186 } 187 int state = playbackState.getState(); 188 return state != PlaybackState.STATE_NONE 189 && state != PlaybackState.STATE_CONNECTING 190 && state != PlaybackState.STATE_PAUSED; 191 } 192 193 /** Returns the ID of the media under playback. */ getMediaId()194 public String getMediaId() { 195 MediaMetadata mediaMetadata = mMediaController.getMetadata(); 196 return mediaMetadata == null 197 ? null 198 : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 199 } 200 201 @Override getMediaTitle()202 public CharSequence getMediaTitle() { 203 MediaMetadata mediaMetadata = mMediaController.getMetadata(); 204 return mediaMetadata == null 205 ? "" 206 : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); 207 } 208 209 @Override getMediaSubtitle()210 public CharSequence getMediaSubtitle() { 211 MediaMetadata mediaMetadata = mMediaController.getMetadata(); 212 return mediaMetadata == null 213 ? "" 214 : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); 215 } 216 217 @Override getMediaDuration()218 public int getMediaDuration() { 219 MediaMetadata mediaMetadata = mMediaController.getMetadata(); 220 return mediaMetadata == null 221 ? 0 222 : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); 223 } 224 225 @Override getMediaArt()226 public Drawable getMediaArt() { 227 // Do not show the poster art on control row. 228 return null; 229 } 230 231 @Override getSupportedActions()232 public long getSupportedActions() { 233 return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; 234 } 235 236 @Override getCurrentSpeedId()237 public int getCurrentSpeedId() { 238 return mPlaybackSpeedId; 239 } 240 241 @Override getCurrentPosition()242 public int getCurrentPosition() { 243 PlaybackState playbackState = mMediaController.getPlaybackState(); 244 if (playbackState == null) { 245 return 0; 246 } 247 return (int) playbackState.getPosition(); 248 } 249 250 /** Unregister media controller's callback. */ unregisterCallback()251 void unregisterCallback() { 252 mMediaController.unregisterCallback(mMediaControllerCallback); 253 } 254 255 /** 256 * Update the secondary controls row. 257 * 258 * @param hasClosedCaption {@code true} to show the closed caption selection button, {@code 259 * false} to hide it. 260 * @param hasMultiAudio {@code true} to show the audio track selection button, {@code false} to 261 * hide it. 262 */ updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio)263 void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { 264 if (hasClosedCaption) { 265 if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) { 266 mSecondaryActionsAdapter.add(0, mClosedCaptioningAction); 267 } 268 } else { 269 mSecondaryActionsAdapter.remove(mClosedCaptioningAction); 270 } 271 if (hasMultiAudio) { 272 if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) { 273 mSecondaryActionsAdapter.add(mMultiAudioAction); 274 } 275 } else { 276 mSecondaryActionsAdapter.remove(mMultiAudioAction); 277 } 278 getHost().notifyPlaybackRowChanged(); 279 } 280 281 /** Update the focus to play pause button. */ onPlaybackResume()282 public void onPlaybackResume() { 283 if (mPlayPauseButton != null) { 284 mPlayPauseButton.requestFocus(); 285 } 286 } 287 288 @Nullable hasSecondaryRow()289 Boolean hasSecondaryRow() { 290 if (mSecondaryActionsAdapter == null) { 291 return null; 292 } 293 return mSecondaryActionsAdapter.size() != 0; 294 } 295 296 @Override play(int speedId)297 public void play(int speedId) { 298 if (getCurrentSpeedId() == speedId) { 299 return; 300 } 301 if (speedId == PLAYBACK_SPEED_NORMAL) { 302 mTransportControls.play(); 303 } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { 304 mTransportControls.rewind(); 305 } else if (speedId >= PLAYBACK_SPEED_FAST_L0) { 306 mTransportControls.fastForward(); 307 } 308 } 309 310 @Override pause()311 public void pause() { 312 mTransportControls.pause(); 313 } 314 315 @Override updateProgress()316 public void updateProgress() { 317 if (mEnableBuffering) { 318 super.updateProgress(); 319 long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs; 320 mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs); 321 } 322 } 323 324 /** Notifies closed caption being enabled/disabled to update related UI. */ onSubtitleTrackStateChanged(boolean enabled)325 void onSubtitleTrackStateChanged(boolean enabled) { 326 mClosedCaptioningAction.setIndex( 327 enabled ? ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); 328 } 329 onStateChanged(int state, long positionMs, int speedLevel)330 private void onStateChanged(int state, long positionMs, int speedLevel) { 331 if (DEBUG) Log.d(TAG, "onStateChanged"); 332 getControlsRow().setCurrentTime((int) positionMs); 333 if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) { 334 // Only position is changed, no need to update controls row 335 return; 336 } 337 // NOTICE: The below two variables should only be used in this method. 338 // The only usage of them is to confirm if the state is changed or not. 339 mPlaybackState = state; 340 mPlaybackSpeedLevel = speedLevel; 341 switch (state) { 342 case PlaybackState.STATE_PLAYING: 343 mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL; 344 setFadingEnabled(true); 345 mReadyToControl = true; 346 break; 347 case PlaybackState.STATE_PAUSED: 348 mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED; 349 setFadingEnabled(true); 350 mReadyToControl = true; 351 break; 352 case PlaybackState.STATE_FAST_FORWARDING: 353 mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel; 354 setFadingEnabled(false); 355 mReadyToControl = true; 356 break; 357 case PlaybackState.STATE_REWINDING: 358 mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel; 359 setFadingEnabled(false); 360 mReadyToControl = true; 361 break; 362 case PlaybackState.STATE_CONNECTING: 363 setFadingEnabled(false); 364 mReadyToControl = false; 365 break; 366 case PlaybackState.STATE_NONE: 367 mReadyToControl = false; 368 break; 369 default: 370 setFadingEnabled(true); 371 break; 372 } 373 onStateChanged(); 374 } 375 showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId)376 private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) { 377 Bundle args = new Bundle(); 378 args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos); 379 args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); 380 DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); 381 sideFragment.setArguments(args); 382 mFragment 383 .getFragmentManager() 384 .beginTransaction() 385 .hide(mFragment) 386 .replace(R.id.dvr_playback_side_fragment, sideFragment) 387 .addToBackStack(null) 388 .commit(); 389 } 390 391 private class MediaControllerCallback extends MediaController.Callback { 392 @Override onPlaybackStateChanged(PlaybackState state)393 public void onPlaybackStateChanged(PlaybackState state) { 394 if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); 395 onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed()); 396 } 397 398 @Override onMetadataChanged(MediaMetadata metadata)399 public void onMetadataChanged(MediaMetadata metadata) { 400 DvrPlaybackControlHelper.this.onMetadataChanged(); 401 } 402 } 403 404 private static class MultiAudioAction extends MultiAction { MultiAudioAction(Context context)405 MultiAudioAction(Context context) { 406 super(AUDIO_ACTION_ID); 407 setDrawables(new Drawable[] {context.getDrawable(R.drawable.ic_tvoption_multi_track)}); 408 } 409 } 410 } 411