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