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