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