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.Fragment; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.graphics.Point; 23 import android.hardware.display.DisplayManager; 24 import android.media.session.PlaybackState; 25 import android.media.tv.TvContentRating; 26 import android.media.tv.TvInputManager; 27 import android.media.tv.TvTrackInfo; 28 import android.os.Bundle; 29 import android.support.v17.leanback.app.PlaybackFragment; 30 import android.support.v17.leanback.app.PlaybackFragmentGlueHost; 31 import android.support.v17.leanback.widget.ArrayObjectAdapter; 32 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; 33 import android.support.v17.leanback.widget.ClassPresenterSelector; 34 import android.support.v17.leanback.widget.HeaderItem; 35 import android.support.v17.leanback.widget.ListRow; 36 import android.support.v17.leanback.widget.Presenter; 37 import android.support.v17.leanback.widget.RowPresenter; 38 import android.support.v17.leanback.widget.SinglePresenterSelector; 39 import android.util.Log; 40 import android.view.Display; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.widget.Toast; 44 import com.android.tv.R; 45 import com.android.tv.TvSingletons; 46 import com.android.tv.data.BaseProgram; 47 import com.android.tv.dialog.PinDialogFragment; 48 import com.android.tv.dvr.DvrDataManager; 49 import com.android.tv.dvr.data.RecordedProgram; 50 import com.android.tv.dvr.data.SeriesRecording; 51 import com.android.tv.dvr.ui.SortedArrayAdapter; 52 import com.android.tv.dvr.ui.browse.DvrListRowPresenter; 53 import com.android.tv.dvr.ui.browse.RecordingCardView; 54 import com.android.tv.ui.AppLayerTvView; 55 import com.android.tv.util.TvSettings; 56 import com.android.tv.util.TvTrackInfoUtils; 57 import com.android.tv.util.Utils; 58 import java.util.ArrayList; 59 import java.util.List; 60 61 public class DvrPlaybackOverlayFragment extends PlaybackFragment { 62 // TODO: Handles audio focus. Deals with block and ratings. 63 private static final String TAG = "DvrPlaybackOverlayFrag"; 64 private static final boolean DEBUG = false; 65 66 private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; 67 private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; 68 private static final long INVALID_TIME = -1; 69 70 // mProgram is only used to store program from intent. Don't use it elsewhere. 71 private RecordedProgram mProgram; 72 private DvrPlayer mDvrPlayer; 73 private DvrPlaybackMediaSessionHelper mMediaSessionHelper; 74 private DvrPlaybackControlHelper mPlaybackControlHelper; 75 private ArrayObjectAdapter mRowsAdapter; 76 private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; 77 private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; 78 private DvrDataManager mDvrDataManager; 79 private AppLayerTvView mTvView; 80 private View mBlockScreenView; 81 private ListRow mRelatedRecordingsRow; 82 private int mVerticalPaddingBase; 83 private int mPaddingWithoutRelatedRow; 84 private int mPaddingWithoutSecondaryRow; 85 private int mWindowWidth; 86 private int mWindowHeight; 87 private float mAppliedAspectRatio; 88 private float mWindowAspectRatio; 89 private boolean mPinChecked; 90 private boolean mStarted; 91 private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener = 92 new DvrPlayer.OnTrackSelectedListener() { 93 @Override 94 public void onTrackSelected(String selectedTrackId) { 95 mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null); 96 mRowsAdapter.notifyArrayItemRangeChanged(0, 1); 97 } 98 }; 99 100 @Override onCreate(Bundle savedInstanceState)101 public void onCreate(Bundle savedInstanceState) { 102 if (DEBUG) Log.d(TAG, "onCreate"); 103 super.onCreate(savedInstanceState); 104 mVerticalPaddingBase = 105 getActivity() 106 .getResources() 107 .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_base); 108 mPaddingWithoutRelatedRow = 109 getActivity() 110 .getResources() 111 .getDimensionPixelOffset( 112 R.dimen.dvr_playback_overlay_padding_top_no_related_row); 113 mPaddingWithoutSecondaryRow = 114 getActivity() 115 .getResources() 116 .getDimensionPixelOffset( 117 R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); 118 mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager(); 119 if (!mDvrDataManager.isRecordedProgramLoadFinished()) { 120 mDvrDataManager.addRecordedProgramLoadFinishedListener( 121 new DvrDataManager.OnRecordedProgramLoadFinishedListener() { 122 @Override 123 public void onRecordedProgramLoadFinished() { 124 mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); 125 if (handleIntent(getActivity().getIntent(), true)) { 126 setUpRows(); 127 preparePlayback(getActivity().getIntent()); 128 } 129 } 130 }); 131 } else if (!handleIntent(getActivity().getIntent(), true)) { 132 return; 133 } 134 Point size = new Point(); 135 ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) 136 .getDisplay(Display.DEFAULT_DISPLAY) 137 .getSize(size); 138 mWindowWidth = size.x; 139 mWindowHeight = size.y; 140 mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; 141 setBackgroundType(PlaybackFragment.BG_LIGHT); 142 setFadingEnabled(true); 143 } 144 145 @Override onStart()146 public void onStart() { 147 super.onStart(); 148 mStarted = true; 149 updateVerticalPosition(); 150 } 151 152 @Override onActivityCreated(Bundle savedInstanceState)153 public void onActivityCreated(Bundle savedInstanceState) { 154 super.onActivityCreated(savedInstanceState); 155 mTvView = getActivity().findViewById(R.id.dvr_tv_view); 156 mBlockScreenView = getActivity().findViewById(R.id.block_screen); 157 mDvrPlayer = new DvrPlayer(mTvView, getActivity()); 158 mMediaSessionHelper = 159 new DvrPlaybackMediaSessionHelper( 160 getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); 161 mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); 162 mRelatedRecordingsRow = getRelatedRecordingsRow(); 163 mDvrPlayer.setOnTracksAvailabilityChangedListener( 164 new DvrPlayer.OnTracksAvailabilityChangedListener() { 165 @Override 166 public void onTracksAvailabilityChanged( 167 boolean hasClosedCaption, boolean hasMultiAudio) { 168 mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); 169 if (hasClosedCaption) { 170 mDvrPlayer.setOnTrackSelectedListener( 171 TvTrackInfo.TYPE_SUBTITLE, mOnSubtitleTrackSelectedListener); 172 selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); 173 } else { 174 mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); 175 } 176 if (hasMultiAudio) { 177 selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO); 178 } 179 updateVerticalPosition(); 180 mPlaybackControlHelper.getHost().notifyPlaybackRowChanged(); 181 } 182 }); 183 mDvrPlayer.setOnAspectRatioChangedListener( 184 new DvrPlayer.OnAspectRatioChangedListener() { 185 @Override 186 public void onAspectRatioChanged(float videoAspectRatio) { 187 updateAspectRatio(videoAspectRatio); 188 } 189 }); 190 mPinChecked = 191 getActivity() 192 .getIntent() 193 .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); 194 mDvrPlayer.setOnContentBlockedListener( 195 new DvrPlayer.OnContentBlockedListener() { 196 @Override 197 public void onContentBlocked(TvContentRating contentRating) { 198 if (mPinChecked) { 199 mTvView.unblockContent(contentRating); 200 return; 201 } 202 mBlockScreenView.setVisibility(View.VISIBLE); 203 getActivity().getMediaController().getTransportControls().pause(); 204 ((DvrPlaybackActivity) getActivity()) 205 .setOnPinCheckListener( 206 new PinDialogFragment.OnPinCheckedListener() { 207 @Override 208 public void onPinChecked( 209 boolean checked, int type, String rating) { 210 ((DvrPlaybackActivity) getActivity()) 211 .setOnPinCheckListener(null); 212 if (checked) { 213 mPinChecked = true; 214 mTvView.unblockContent(contentRating); 215 mBlockScreenView.setVisibility(View.GONE); 216 getActivity() 217 .getMediaController() 218 .getTransportControls() 219 .play(); 220 } 221 } 222 }); 223 PinDialogFragment.create( 224 PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, 225 contentRating.flattenToString()) 226 .show( 227 getActivity().getFragmentManager(), 228 PinDialogFragment.DIALOG_TAG); 229 } 230 }); 231 setOnItemViewClickedListener( 232 new BaseOnItemViewClickedListener() { 233 @Override 234 public void onItemClicked( 235 Presenter.ViewHolder itemViewHolder, 236 Object item, 237 RowPresenter.ViewHolder rowViewHolder, 238 Object row) { 239 if (itemViewHolder.view instanceof RecordingCardView) { 240 setFadingEnabled(false); 241 long programId = 242 ((RecordedProgram) itemViewHolder.view.getTag()).getId(); 243 if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); 244 Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); 245 intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); 246 getContext().startActivity(intent); 247 } 248 } 249 }); 250 if (mProgram != null) { 251 setUpRows(); 252 preparePlayback(getActivity().getIntent()); 253 } 254 } 255 256 @Override onPause()257 public void onPause() { 258 if (DEBUG) Log.d(TAG, "onPause"); 259 super.onPause(); 260 if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING 261 || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { 262 getActivity().getMediaController().getTransportControls().pause(); 263 } 264 if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { 265 getActivity().requestVisibleBehind(false); 266 } else { 267 getActivity().requestVisibleBehind(true); 268 } 269 } 270 271 @Override onDestroy()272 public void onDestroy() { 273 if (DEBUG) Log.d(TAG, "onDestroy"); 274 mPlaybackControlHelper.unregisterCallback(); 275 mMediaSessionHelper.release(); 276 mRelatedRecordingCardPresenter.unbindAllViewHolders(); 277 mDvrPlayer.release(); 278 super.onDestroy(); 279 } 280 281 /** Passes the intent to the fragment. */ onNewIntent(Intent intent)282 public void onNewIntent(Intent intent) { 283 if (mDvrDataManager.isRecordedProgramLoadFinished() && handleIntent(intent, false)) { 284 preparePlayback(intent); 285 } 286 } 287 288 /** 289 * Should be called when windows' size is changed in order to notify DVR player to update it's 290 * view width/height and position. 291 */ onWindowSizeChanged(final int windowWidth, final int windowHeight)292 public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { 293 mWindowWidth = windowWidth; 294 mWindowHeight = windowHeight; 295 mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; 296 updateAspectRatio(mAppliedAspectRatio); 297 } 298 299 /** Returns next recorded episode in the same series as now playing program. */ getNextEpisode(RecordedProgram program)300 public RecordedProgram getNextEpisode(RecordedProgram program) { 301 int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); 302 if (position == mRelatedRecordingsRowAdapter.size()) { 303 return null; 304 } else { 305 return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); 306 } 307 } 308 309 /** 310 * Returns the tracks of the give type of the current playback. 311 * 312 * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link 313 * TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. 314 */ getTracks(int trackType)315 public ArrayList<TvTrackInfo> getTracks(int trackType) { 316 if (trackType == TvTrackInfo.TYPE_AUDIO) { 317 return mDvrPlayer.getAudioTracks(); 318 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 319 return mDvrPlayer.getSubtitleTracks(); 320 } 321 return null; 322 } 323 324 /** Returns the ID of the selected track of the given type. */ getSelectedTrackId(int trackType)325 public String getSelectedTrackId(int trackType) { 326 return mDvrPlayer.getSelectedTrackId(trackType); 327 } 328 329 /** 330 * Returns the language setting of the given track type. 331 * 332 * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link 333 * TvTrackInfo#TYPE_AUDIO}. 334 * @return {@code null} if no language has been set for the given track type. 335 */ getTrackSetting(int trackType)336 TvTrackInfo getTrackSetting(int trackType) { 337 return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType); 338 } 339 340 /** 341 * Selects the given audio or subtitle track for DVR playback. 342 * 343 * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link 344 * TvTrackInfo#TYPE_AUDIO}. 345 * @param selectedTrack {@code null} to disable the audio or subtitle track according to 346 * trackType. 347 */ selectTrack(int trackType, TvTrackInfo selectedTrack)348 void selectTrack(int trackType, TvTrackInfo selectedTrack) { 349 if (mDvrPlayer.isPlaybackPrepared()) { 350 mDvrPlayer.selectTrack(trackType, selectedTrack); 351 } 352 } 353 handleIntent(Intent intent, boolean finishActivity)354 private boolean handleIntent(Intent intent, boolean finishActivity) { 355 mProgram = getProgramFromIntent(intent); 356 if (mProgram == null) { 357 Toast.makeText( 358 getActivity(), 359 getString(R.string.dvr_program_not_found), 360 Toast.LENGTH_SHORT) 361 .show(); 362 if (finishActivity) { 363 getActivity().finish(); 364 } 365 return false; 366 } 367 return true; 368 } 369 selectBestMatchedTrack(int trackType)370 private void selectBestMatchedTrack(int trackType) { 371 TvTrackInfo selectedTrack = getTrackSetting(trackType); 372 if (selectedTrack != null) { 373 TvTrackInfo bestMatchedTrack = 374 TvTrackInfoUtils.getBestTrackInfo( 375 getTracks(trackType), 376 selectedTrack.getId(), 377 selectedTrack.getLanguage(), 378 trackType == TvTrackInfo.TYPE_AUDIO 379 ? selectedTrack.getAudioChannelCount() 380 : 0); 381 if (bestMatchedTrack != null 382 && (trackType == TvTrackInfo.TYPE_AUDIO 383 || Utils.isEqualLanguage( 384 bestMatchedTrack.getLanguage(), selectedTrack.getLanguage()))) { 385 selectTrack(trackType, bestMatchedTrack); 386 return; 387 } 388 } 389 if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 390 // Disables closed captioning if there's no matched language. 391 selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); 392 } 393 } 394 updateAspectRatio(float videoAspectRatio)395 private void updateAspectRatio(float videoAspectRatio) { 396 if (videoAspectRatio <= 0) { 397 // We don't have video's width or height information, use window's aspect ratio. 398 videoAspectRatio = mWindowAspectRatio; 399 } 400 if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { 401 // No need to change 402 return; 403 } 404 if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { 405 ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0); 406 } else if (videoAspectRatio < mWindowAspectRatio) { 407 int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; 408 ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); 409 } else { 410 int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; 411 ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); 412 } 413 mAppliedAspectRatio = videoAspectRatio; 414 } 415 preparePlayback(Intent intent)416 private void preparePlayback(Intent intent) { 417 mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); 418 mPlaybackControlHelper.updateSecondaryRow(false, false); 419 getActivity().getMediaController().getTransportControls().prepare(); 420 updateRelatedRecordingsRow(); 421 } 422 updateRelatedRecordingsRow()423 private void updateRelatedRecordingsRow() { 424 boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); 425 mRelatedRecordingsRowAdapter.clear(); 426 long programId = mProgram.getId(); 427 String seriesId = mProgram.getSeriesId(); 428 SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); 429 if (seriesRecording != null) { 430 if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); 431 List<RecordedProgram> relatedPrograms = 432 mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); 433 for (RecordedProgram program : relatedPrograms) { 434 if (programId != program.getId()) { 435 mRelatedRecordingsRowAdapter.add(program); 436 } 437 } 438 } 439 if (mRelatedRecordingsRowAdapter.size() == 0) { 440 mRowsAdapter.remove(mRelatedRecordingsRow); 441 } else if (wasEmpty) { 442 mRowsAdapter.add(mRelatedRecordingsRow); 443 } 444 updateVerticalPosition(); 445 mRowsAdapter.notifyArrayItemRangeChanged(1, 1); 446 } 447 setUpRows()448 private void setUpRows() { 449 mPlaybackControlHelper.createControlsRow(); 450 mPlaybackControlHelper.setHost(new PlaybackFragmentGlueHost(this)); 451 mRowsAdapter = (ArrayObjectAdapter) getAdapter(); 452 ClassPresenterSelector selector = 453 (ClassPresenterSelector) mRowsAdapter.getPresenterSelector(); 454 selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); 455 mRowsAdapter.setPresenterSelector(selector); 456 if (mStarted) { 457 // If it's started before setting up rows, vertical position has not been updated and 458 // should be updated here. 459 updateVerticalPosition(); 460 } 461 } 462 getRelatedRecordingsRow()463 private ListRow getRelatedRecordingsRow() { 464 mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); 465 mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); 466 HeaderItem header = 467 new HeaderItem( 468 0, getActivity().getString(R.string.dvr_playback_related_recordings)); 469 return new ListRow(header, mRelatedRecordingsRowAdapter); 470 } 471 getProgramFromIntent(Intent intent)472 private RecordedProgram getProgramFromIntent(Intent intent) { 473 long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); 474 return mDvrDataManager.getRecordedProgram(programId); 475 } 476 getSeekTimeFromIntent(Intent intent)477 private long getSeekTimeFromIntent(Intent intent) { 478 return intent.getLongExtra( 479 Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, TvInputManager.TIME_SHIFT_INVALID_TIME); 480 } 481 updateVerticalPosition()482 private void updateVerticalPosition() { 483 Boolean hasSecondaryRow = mPlaybackControlHelper.hasSecondaryRow(); 484 if (hasSecondaryRow == null) { 485 return; 486 } 487 488 int verticalPadding = mVerticalPaddingBase; 489 if (mRelatedRecordingsRowAdapter.size() == 0) { 490 verticalPadding += mPaddingWithoutRelatedRow; 491 } 492 if (!hasSecondaryRow) { 493 verticalPadding += mPaddingWithoutSecondaryRow; 494 } 495 Fragment fragment = getChildFragmentManager().findFragmentById(R.id.playback_controls_dock); 496 View view = fragment == null ? null : fragment.getView(); 497 if (view != null) { 498 view.setTranslationY(verticalPadding); 499 } 500 } 501 onPlaybackResume()502 public void onPlaybackResume() { 503 mPlaybackControlHelper.onPlaybackResume(); 504 } 505 getProgramStartTimeMs()506 public long getProgramStartTimeMs() { 507 return (mProgram != null && mProgram.isPartial()) 508 ? mProgram.getStartTimeUtcMillis() 509 : INVALID_TIME; 510 } 511 updateProgress()512 public void updateProgress() { 513 mPlaybackControlHelper.updateProgress(); 514 } 515 516 private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter)517 RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { 518 super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); 519 } 520 521 @Override getId(BaseProgram item)522 public long getId(BaseProgram item) { 523 return item.getId(); 524 } 525 } 526 } 527