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