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