diff options
Diffstat (limited to 'src/com/android/tv/dvr/ui/playback')
7 files changed, 2046 insertions, 0 deletions
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java new file mode 100644 index 00000000..2437d1f5 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.data.RecordedProgram; + +/** + * Activity to play a {@link RecordedProgram}. + */ +public class DvrPlaybackActivity extends Activity { + private static final String TAG = "DvrPlaybackActivity"; + private static final boolean DEBUG = false; + + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_dvr_playback); + mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment); + } + + @Override + public void onVisibleBehindCanceled() { + if (DEBUG) Log.d(TAG, "onVisibleBehindCanceled"); + super.onVisibleBehindCanceled(); + finish(); + } + + @Override + protected void onNewIntent(Intent intent) { + mOverlayFragment.onNewIntent(intent); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + float density = getResources().getDisplayMetrics().density; + mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density), + (int) (newConfig.screenHeightDp * density)); + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java new file mode 100644 index 00000000..4bd121b1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.playback; + +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.browse.RecordedProgramPresenter; +import com.android.tv.dvr.ui.browse.RecordingCardView; +import com.android.tv.util.Utils; + +/** + * This class is used to generate Views and bind Objects for related recordings in DVR playback. + */ +class DvrPlaybackCardPresenter extends RecordedProgramPresenter { + private static final String TAG = "DvrPlaybackCardPresenter"; + private static final boolean DEBUG = false; + + private final int mRelatedRecordingCardWidth; + private final int mRelatedRecordingCardHeight; + private final DvrPlaybackOverlayFragment mFragment; + + DvrPlaybackCardPresenter(Context context, DvrPlaybackOverlayFragment fragment) { + super(context); + mFragment = fragment; + mRelatedRecordingCardWidth = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); + mRelatedRecordingCardHeight = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView( + getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true); + return new ViewHolder(view); + } + + @Override + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + // Disable fading of overlay fragment to prevent the layout blinking while updating + // new playback states and info. The fading enabled status will be reset during + // playback state changing, in DvrPlaybackControlHelper.onStateChanged(). + mFragment.setFadingEnabled(false); + long programId = ((RecordedProgram) v.getTag()).getId(); + if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); + Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + }; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java new file mode 100644 index 00000000..4658a328 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.app.Activity; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaController.TransportControls; +import android.media.session.PlaybackState; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.v17.leanback.app.PlaybackControlGlue; +import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ControlButtonPresenterSelector; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction; +import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction; +import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; +import android.support.v17.leanback.widget.RowPresenter; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; + +import com.android.tv.R; +import com.android.tv.util.TimeShiftUtils; + +import java.util.ArrayList; + +/** + * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and + * send command to the media controller. It also helps to update playback states displayed in the + * fragment according to information the media session provides. + */ +class DvrPlaybackControlHelper extends PlaybackControlGlue { + private static final String TAG = "DvrPlaybackControlHelper"; + private static final boolean DEBUG = false; + + private static final int AUDIO_ACTION_ID = 1001; + + private int mPlaybackState = PlaybackState.STATE_NONE; + private int mPlaybackSpeedLevel; + private int mPlaybackSpeedId; + private boolean mReadyToControl; + + private final MediaController mMediaController; + private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); + private final TransportControls mTransportControls; + private final int mExtraPaddingTopForNoDescription; + private final ArrayObjectAdapter mSecondaryActionsAdapter; + private final MultiAction mClosedCaptioningAction; + private final MultiAction mMultiAudioAction; + + public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { + super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); + mMediaController = activity.getMediaController(); + mMediaController.registerCallback(mMediaControllerCallback); + mTransportControls = mMediaController.getTransportControls(); + mExtraPaddingTopForNoDescription = activity.getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); + mSecondaryActionsAdapter = new ArrayObjectAdapter(new ControlButtonPresenterSelector()); + mClosedCaptioningAction = new ClosedCaptioningAction(activity); + mMultiAudioAction = new MultiAudioAction(activity); + } + + @Override + public PlaybackControlsRowPresenter createControlsRowAndPresenter() { + PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); + controlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); + setControlsRow(controlsRow); + AbstractDetailsDescriptionPresenter detailsPresenter = + new AbstractDetailsDescriptionPresenter() { + @Override + protected void onBindDescription( + AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) { + PlaybackControlGlue glue = (PlaybackControlGlue) object; + if (glue.hasValidMedia()) { + viewHolder.getTitle().setText(glue.getMediaTitle()); + viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); + } else { + viewHolder.getTitle().setText(""); + viewHolder.getSubtitle().setText(""); + } + if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { + viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(), + mExtraPaddingTopForNoDescription, + viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom()); + } + } + }; + PlaybackControlsRowPresenter presenter = + new PlaybackControlsRowPresenter(detailsPresenter) { + @Override + protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { + super.onBindRowViewHolder(vh, item); + vh.setOnKeyListener(DvrPlaybackControlHelper.this); + } + + @Override + protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { + super.onUnbindRowViewHolder(vh); + vh.setOnKeyListener(null); + } + }; + presenter.setProgressColor(getContext().getResources() + .getColor(R.color.play_controls_progress_bar_watched)); + presenter.setBackgroundColor(getContext().getResources() + .getColor(R.color.play_controls_body_background_enabled)); + presenter.setOnActionClickedListener(new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (mReadyToControl) { + int trackType; + if (action.getId() == mClosedCaptioningAction.getId()) { + trackType = TvTrackInfo.TYPE_SUBTITLE; + } else if (action.getId() == AUDIO_ACTION_ID) { + trackType = TvTrackInfo.TYPE_AUDIO; + } else { + DvrPlaybackControlHelper.super.onActionClicked(action); + return; + } + ArrayList<TvTrackInfo> trackInfos = + ((DvrPlaybackOverlayFragment) getFragment()).getTracks(trackType); + if (!trackInfos.isEmpty()) { + showSideFragment(trackInfos, ((DvrPlaybackOverlayFragment) + getFragment()).getSelectedTrackId(trackType)); + } + } + } + }); + return presenter; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (mReadyToControl) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN + && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + || mPlaybackState == PlaybackState.STATE_REWINDING)) { + // Workaround of b/31489271. Clicks play/pause button first to reset play controls + // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. + onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); + } + return super.onKey(v, keyCode, event); + } + return false; + } + + @Override + public boolean hasValidMedia() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + return playbackState != null; + } + + @Override + public boolean isMediaPlaying() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + if (playbackState == null) { + return false; + } + int state = playbackState.getState(); + return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING + && state != PlaybackState.STATE_PAUSED; + } + + /** + * Returns the ID of the media under playback. + */ + public String getMediaId() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? null + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + } + + @Override + public CharSequence getMediaTitle() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? "" + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + } + + @Override + public CharSequence getMediaSubtitle() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? "" + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); + } + + @Override + public int getMediaDuration() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? 0 + : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); + } + + @Override + public Drawable getMediaArt() { + // Do not show the poster art on control row. + return null; + } + + @Override + public long getSupportedActions() { + return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; + } + + @Override + public int getCurrentSpeedId() { + return mPlaybackSpeedId; + } + + @Override + public int getCurrentPosition() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + if (playbackState == null) { + return 0; + } + return (int) playbackState.getPosition(); + } + + /** + * Unregister media controller's callback. + */ + public void unregisterCallback() { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + + /** + * Update the secondary controls row. + * @param hasClosedCaption {@code true} to show the closed caption selection button, + * {@code false} to hide it. + * @param hasMultiAudio {@code true} to show the audio track selection button, + * {@code false} to hide it. + */ + public void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { + if (hasClosedCaption) { + if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) { + mSecondaryActionsAdapter.add(0, mClosedCaptioningAction); + } + } else { + mSecondaryActionsAdapter.remove(mClosedCaptioningAction); + } + if (hasMultiAudio) { + if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) { + mSecondaryActionsAdapter.add(mMultiAudioAction); + } + } else { + mSecondaryActionsAdapter.remove(mMultiAudioAction); + } + } + + /** + * Returns if the secondary controls row has any buttons and thus should be shown. + */ + public boolean hasSecondaryRow() { + return mSecondaryActionsAdapter.size() != 0; + } + + @Override + protected void startPlayback(int speedId) { + if (getCurrentSpeedId() == speedId) { + return; + } + if (speedId == PLAYBACK_SPEED_NORMAL) { + mTransportControls.play(); + } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { + mTransportControls.rewind(); + } else if (speedId >= PLAYBACK_SPEED_FAST_L0){ + mTransportControls.fastForward(); + } + } + + @Override + protected void pausePlayback() { + mTransportControls.pause(); + } + + @Override + protected void skipToNext() { + // Do nothing. + } + + @Override + protected void skipToPrevious() { + // Do nothing. + } + + @Override + protected void onRowChanged(PlaybackControlsRow row) { + // Do nothing. + } + + /** + * Notifies closed caption being enabled/disabled to update related UI. + */ + void onSubtitleTrackStateChanged(boolean enabled) { + mClosedCaptioningAction.setIndex(enabled ? + ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); + } + + private void onStateChanged(int state, long positionMs, int speedLevel) { + if (DEBUG) Log.d(TAG, "onStateChanged"); + getControlsRow().setCurrentTime((int) positionMs); + if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) { + // Only position is changed, no need to update controls row + return; + } + // NOTICE: The below two variables should only be used in this method. + // The only usage of them is to confirm if the state is changed or not. + mPlaybackState = state; + mPlaybackSpeedLevel = speedLevel; + switch (state) { + case PlaybackState.STATE_PLAYING: + mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL; + setFadingEnabled(true); + mReadyToControl = true; + break; + case PlaybackState.STATE_PAUSED: + mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED; + setFadingEnabled(true); + mReadyToControl = true; + break; + case PlaybackState.STATE_FAST_FORWARDING: + mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel; + setFadingEnabled(false); + mReadyToControl = true; + break; + case PlaybackState.STATE_REWINDING: + mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel; + setFadingEnabled(false); + mReadyToControl = true; + break; + case PlaybackState.STATE_CONNECTING: + setFadingEnabled(false); + mReadyToControl = false; + break; + case PlaybackState.STATE_NONE: + mReadyToControl = false; + break; + default: + setFadingEnabled(true); + break; + } + onStateChanged(); + } + + private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) { + Bundle args = new Bundle(); + args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos); + args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); + DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); + sideFragment.setArguments(args); + getFragment().getFragmentManager().beginTransaction() + .hide(getFragment()) + .replace(R.id.dvr_playback_side_fragment, sideFragment) + .addToBackStack(null) + .commit(); + } + + private class MediaControllerCallback extends MediaController.Callback { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); + onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed()); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + DvrPlaybackControlHelper.this.onMetadataChanged(); + ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); + } + } + + private static class MultiAudioAction extends MultiAction { + MultiAudioAction(Context context) { + super(AUDIO_ACTION_ID); + setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)}); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java new file mode 100644 index 00000000..843d2dbe --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.media.tv.TvContract; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.util.ImageLoader; +import com.android.tv.util.TimeShiftUtils; +import com.android.tv.util.Utils; + +class DvrPlaybackMediaSessionHelper { + private static final String TAG = "DvrPlaybackMediaSessionHelper"; + private static final boolean DEBUG = false; + + private int mNowPlayingCardWidth; + private int mNowPlayingCardHeight; + private int mSpeedLevel; + private long mProgramDurationMs; + + private Activity mActivity; + private DvrPlayer mDvrPlayer; + private MediaSession mMediaSession; + private final DvrWatchedPositionManager mDvrWatchedPositionManager; + private final ChannelDataManager mChannelDataManager; + + public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, + DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { + mActivity = activity; + mDvrPlayer = dvrPlayer; + mDvrWatchedPositionManager = + TvApplication.getSingletons(activity).getDvrWatchedPositionManager(); + mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager(); + mDvrPlayer.setCallback(new DvrPlayer.DvrPlayerCallback() { + @Override + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { + updateMediaSessionPlaybackState(); + } + + @Override + public void onPlaybackPositionChanged(long positionMs) { + updateMediaSessionPlaybackState(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrWatchedPositionManager + .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); + } + } + + @Override + public void onPlaybackEnded() { + // TODO: Deal with watched over recordings in DVR library + RecordedProgram nextEpisode = + overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); + if (nextEpisode == null) { + mDvrPlayer.reset(); + mActivity.finish(); + } else { + Intent intent = new Intent(activity, DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); + mActivity.startActivity(intent); + } + } + }); + initializeMediaSession(mediaSessionTag); + } + + /** + * Stops DVR player and release media session. + */ + public void release() { + if (mDvrPlayer != null) { + mDvrPlayer.reset(); + } + if (mMediaSession != null) { + mMediaSession.release(); + mMediaSession = null; + } + } + + /** + * Updates media session's playback state and speed. + */ + public void updateMediaSessionPlaybackState() { + mMediaSession.setPlaybackState(new PlaybackState.Builder() + .setState(mDvrPlayer.getPlaybackState(), mDvrPlayer.getPlaybackPosition(), + mSpeedLevel).build()); + } + + /** + * Sets the recorded program for playback. + * + * @param program The recorded program to play. {@code null} to reset the DVR player. + */ + public void setupPlayback(RecordedProgram program, long seekPositionMs) { + if (program != null) { + mDvrPlayer.setProgram(program, seekPositionMs); + setupMediaSession(program); + } else { + mDvrPlayer.reset(); + mMediaSession.setActive(false); + } + } + + /** + * Returns the recorded program now playing. + */ + public RecordedProgram getProgram() { + return mDvrPlayer.getProgram(); + } + + /** + * Checks if the recorded program is the same as now playing one. + */ + public boolean isCurrentProgram(RecordedProgram program) { + return program != null && program.equals(getProgram()); + } + + /** + * Returns playback state. + */ + public int getPlaybackState() { + return mDvrPlayer.getPlaybackState(); + } + + /** + * Returns the underlying DVR player. + */ + public DvrPlayer getDvrPlayer() { + return mDvrPlayer; + } + + private void initializeMediaSession(String mediaSessionTag) { + mMediaSession = new MediaSession(mActivity, mediaSessionTag); + mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mNowPlayingCardWidth = mActivity.getResources() + .getDimensionPixelSize(R.dimen.notif_card_img_max_width); + mNowPlayingCardHeight = mActivity.getResources() + .getDimensionPixelSize(R.dimen.notif_card_img_height); + mMediaSession.setCallback(new MediaSessionCallback()); + mActivity.setMediaController( + new MediaController(mActivity, mMediaSession.getSessionToken())); + updateMediaSessionPlaybackState(); + } + + private void setupMediaSession(RecordedProgram program) { + mProgramDurationMs = program.getDurationMillis(); + String cardTitleText = program.getTitle(); + if (TextUtils.isEmpty(cardTitleText)) { + Channel channel = mChannelDataManager.getChannel(program.getChannelId()); + cardTitleText = (channel != null) ? channel.getDisplayName() + : mActivity.getString(R.string.no_program_information); + } + final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText, + program.getDescription(), mProgramDurationMs); + String posterArtUri = program.getPosterArtUri(); + if (posterArtUri == null) { + posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); + } + updatePosterArt(program, currentMetadata, null, posterArtUri); + mMediaSession.setActive(true); + } + + private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, + @Nullable Bitmap posterArt, @Nullable String posterArtUri) { + if (posterArt != null) { + updateMetadataImageInfo(program, currentMetadata, posterArt, 0); + } else if (posterArtUri != null) { + ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, + mNowPlayingCardHeight, + new ProgramPosterArtCallback(mActivity, program, currentMetadata)); + } else { + updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); + } + } + + private class ProgramPosterArtCallback extends + ImageLoader.ImageLoaderCallback<Activity> { + private final RecordedProgram mRecordedProgram; + private final MediaMetadata mCurrentMetadata; + + public ProgramPosterArtCallback(Activity activity, RecordedProgram program, + MediaMetadata metadata) { + super(activity); + mRecordedProgram = program; + mCurrentMetadata = metadata; + } + + @Override + public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { + if (isCurrentProgram(mRecordedProgram)) { + updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); + } + } + } + + private MediaMetadata updateMetadataTextInfo(final long programId, final String title, + final String subtitle, final long duration) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); + if (subtitle != null) { + builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + } + MediaMetadata metadata = builder.build(); + mMediaSession.setMetadata(metadata); + return metadata; + } + + private void updateMetadataImageInfo(final RecordedProgram program, + final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) { + if (mMediaSession != null && (posterArt != null || imageResId != 0)) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); + if (posterArt != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); + mMediaSession.setMetadata(builder.build()); + } else { + new AsyncTask<Void, Void, Bitmap>() { + @Override + protected Bitmap doInBackground(Void... arg0) { + return BitmapFactory.decodeResource(mActivity.getResources(), imageResId); + } + + @Override + protected void onPostExecute(Bitmap programPosterArt) { + if (mMediaSession != null && programPosterArt != null + && isCurrentProgram(program)) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); + mMediaSession.setMetadata(builder.build()); + } + } + }.execute(); + } + } + } + + // An event was triggered by MediaController.TransportControls and must be handled here. + // Here we update the media itself to act on the event that was triggered. + private class MediaSessionCallback extends MediaSession.Callback { + @Override + public void onPrepare() { + if (!mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.prepare(true); + } + } + + @Override + public void onPlay() { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.play(); + } + } + + @Override + public void onPause() { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.pause(); + } + } + + @Override + public void onFastForward() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } + if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { + if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { + mSpeedLevel++; + } else { + return; + } + } else { + mSpeedLevel = 0; + } + mDvrPlayer.fastForward( + TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); + } + + @Override + public void onRewind() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } + if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { + if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { + mSpeedLevel++; + } else { + return; + } + } else { + mSpeedLevel = 0; + } + mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); + } + + @Override + public void onSeekTo(long positionMs) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.seekTo(positionMs); + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java new file mode 100644 index 00000000..ff907182 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.hardware.display.DisplayManager; +import android.media.tv.TvContentRating; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.media.session.PlaybackState; +import android.media.tv.TvInputManager; +import android.media.tv.TvView; +import android.support.v17.leanback.app.PlaybackOverlayFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; +import android.support.v17.leanback.widget.SinglePresenterSelector; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.dvr.ui.browse.DvrListRowPresenter; +import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.util.TvSettings; +import com.android.tv.util.TvTrackInfoUtils; +import com.android.tv.util.Utils; + +import java.util.List; +import java.util.ArrayList; + +public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { + // TODO: Handles audio focus. Deals with block and ratings. + private static final String TAG = "DvrPlaybackOverlayFragment"; + private static final boolean DEBUG = false; + + private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; + private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; + + // mProgram is only used to store program from intent. Don't use it elsewhere. + private RecordedProgram mProgram; + private DvrPlayer mDvrPlayer; + private DvrPlaybackMediaSessionHelper mMediaSessionHelper; + private DvrPlaybackControlHelper mPlaybackControlHelper; + private ArrayObjectAdapter mRowsAdapter; + private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; + private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; + private DvrDataManager mDvrDataManager; + private ContentRatingsManager mContentRatingsManager; + private TvView mTvView; + private View mBlockScreenView; + private ListRow mRelatedRecordingsRow; + private int mPaddingWithoutRelatedRow; + private int mPaddingWithoutSecondaryRow; + private int mWindowWidth; + private int mWindowHeight; + private float mAppliedAspectRatio; + private float mWindowAspectRatio; + private boolean mPinChecked; + private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener = + new DvrPlayer.OnTrackSelectedListener() { + @Override + public void onTrackSelected(String selectedTrackId) { + mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null); + mRowsAdapter.notifyArrayItemRangeChanged(0, 1); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + mPaddingWithoutRelatedRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row); + mPaddingWithoutSecondaryRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); + mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mContentRatingsManager = TvApplication.getSingletons(getContext()) + .getTvInputManagerHelper().getContentRatingsManager(); + mProgram = getProgramFromIntent(getActivity().getIntent()); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + getActivity().finish(); + return; + } + Point size = new Point(); + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); + mWindowWidth = size.x; + mWindowHeight = size.y; + mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; + setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); + setFadingEnabled(true); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); + mBlockScreenView = getActivity().findViewById(R.id.block_screen); + mDvrPlayer = new DvrPlayer(mTvView); + mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( + getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); + mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); + setUpRows(); + mDvrPlayer.setOnTracksAvailabilityChangedListener( + new DvrPlayer.OnTracksAvailabilityChangedListener() { + @Override + public void onTracksAvailabilityChanged(boolean hasClosedCaption, + boolean hasMultiAudio) { + mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); + if (hasClosedCaption) { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, + mOnSubtitleTrackSelectedListener); + selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); + } else { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); + } + if (hasMultiAudio) { + selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO); + } + onMediaControllerUpdated(); + } + }); + mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() { + @Override + public void onAspectRatioChanged(float videoAspectRatio) { + updateAspectRatio(videoAspectRatio); + } + }); + mPinChecked = getActivity().getIntent() + .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); + mDvrPlayer.setOnContentBlockedListener(new DvrPlayer.OnContentBlockedListener() { + @Override + public void onContentBlocked(TvContentRating rating) { + if (mPinChecked) { + mTvView.unblockContent(rating); + return; + } + mBlockScreenView.setVisibility(View.VISIBLE); + getActivity().getMediaController().getTransportControls().pause(); + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + mPinChecked = true; + mTvView.unblockContent(rating); + mBlockScreenView.setVisibility(View.GONE); + getActivity().getMediaController() + .getTransportControls().play(); + } + } + }, mContentRatingsManager.getDisplayNameForRating(rating)) + .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + }); + preparePlayback(getActivity().getIntent()); + } + + @Override + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause"); + super.onPause(); + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING + || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { + getActivity().getMediaController().getTransportControls().pause(); + } + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { + getActivity().requestVisibleBehind(false); + } else { + getActivity().requestVisibleBehind(true); + } + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mPlaybackControlHelper.unregisterCallback(); + mMediaSessionHelper.release(); + mRelatedRecordingCardPresenter.unbindAllViewHolders(); + super.onDestroy(); + } + + /** + * Passes the intent to the fragment. + */ + public void onNewIntent(Intent intent) { + mProgram = getProgramFromIntent(intent); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + // Continue playing the original program + return; + } + preparePlayback(intent); + } + + /** + * Should be called when windows' size is changed in order to notify DVR player + * to update it's view width/height and position. + */ + public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; + updateAspectRatio(mAppliedAspectRatio); + } + + /** + * Returns next recorded episode in the same series as now playing program. + */ + public RecordedProgram getNextEpisode(RecordedProgram program) { + int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); + if (position == mRelatedRecordingsRowAdapter.size()) { + return null; + } else { + return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); + } + } + + /** + * Returns the tracks of the give type of the current playback. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. + */ + public ArrayList<TvTrackInfo> getTracks(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mDvrPlayer.getAudioTracks(); + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mDvrPlayer.getSubtitleTracks(); + } + return null; + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + return mDvrPlayer.getSelectedTrackId(trackType); + } + + /** + * Returns the language setting of the given track type. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @return {@code null} if no language has been set for the given track type. + */ + TvTrackInfo getTrackSetting(int trackType) { + return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType); + } + + /** + * Selects the given audio or subtitle track for DVR playback. + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @param selectedTrack {@code null} to disable the audio or subtitle track according to + * trackType. + */ + void selectTrack(int trackType, TvTrackInfo selectedTrack) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.selectTrack(trackType, selectedTrack); + } + } + + /** + * Notifies the content of controls row or related recordings row is changed and the UI should + * be updated according to the change. + */ + void onMediaControllerUpdated() { + updateVerticalPosition(); + mRowsAdapter.notifyArrayItemRangeChanged(0, 2); + } + + private void selectBestMatchedTrack(int trackType) { + TvTrackInfo selectedTrack = getTrackSetting(trackType); + if (selectedTrack != null) { + TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType), + selectedTrack.getId(), selectedTrack.getLanguage(), + trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0); + if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils + .isEqualLanguage(bestMatchedTrack.getLanguage(), + selectedTrack.getLanguage()))) { + selectTrack(trackType, bestMatchedTrack); + return; + } + } + if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + // Disables closed captioning if there's no matched language. + selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); + } + } + + private void updateAspectRatio(float videoAspectRatio) { + if (videoAspectRatio <= 0) { + // We don't have video's width or height information, use window's aspect ratio. + videoAspectRatio = mWindowAspectRatio; + } + if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + // No need to change + return; + } + if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0); + } else if (videoAspectRatio < mWindowAspectRatio) { + int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); + } else { + int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); + } + mAppliedAspectRatio = videoAspectRatio; + } + + private void preparePlayback(Intent intent) { + mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); + mPlaybackControlHelper.updateSecondaryRow(false, false); + getActivity().getMediaController().getTransportControls().prepare(); + updateRelatedRecordingsRow(); + } + + private void updateRelatedRecordingsRow() { + boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); + mRelatedRecordingsRowAdapter.clear(); + long programId = mProgram.getId(); + String seriesId = mProgram.getSeriesId(); + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + if (seriesRecording != null) { + if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); + List<RecordedProgram> relatedPrograms = + mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); + for (RecordedProgram program : relatedPrograms) { + if (programId != program.getId()) { + mRelatedRecordingsRowAdapter.add(program); + } + } + } + if (mRelatedRecordingsRowAdapter.size() == 0) { + mRowsAdapter.remove(mRelatedRecordingsRow); + } else if (wasEmpty){ + mRowsAdapter.add(mRelatedRecordingsRow); + } + onMediaControllerUpdated(); + } + + private void updateVerticalPosition() { + int verticalPadding = 0; + verticalPadding += + mRelatedRecordingsRowAdapter.size() == 0 ? mPaddingWithoutRelatedRow : 0; + verticalPadding += + mPlaybackControlHelper.hasSecondaryRow() ? 0 : mPaddingWithoutSecondaryRow; + if (DEBUG) Log.d(TAG, "New controls padding: " + verticalPadding); + View view = getView(); + view.setPadding(view.getPaddingLeft(), verticalPadding, + view.getPaddingRight(), view.getPaddingBottom()); + } + + private void setUpRows() { + PlaybackControlsRowPresenter controlsRowPresenter = + mPlaybackControlHelper.createControlsRowAndPresenter(); + + ClassPresenterSelector selector = new ClassPresenterSelector(); + selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); + selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + + mRowsAdapter = new ArrayObjectAdapter(selector); + mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); + mRelatedRecordingsRow = getRelatedRecordingsRow(); + setAdapter(mRowsAdapter); + } + + private ListRow getRelatedRecordingsRow() { + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity(), this); + mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); + HeaderItem header = new HeaderItem(0, + getActivity().getString(R.string.dvr_playback_related_recordings)); + return new ListRow(header, mRelatedRecordingsRowAdapter); + } + + private RecordedProgram getProgramFromIntent(Intent intent) { + long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); + return mDvrDataManager.getRecordedProgram(programId); + } + + private long getSeekTimeFromIntent(Intent intent) { + return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, + TvInputManager.TIME_SHIFT_INVALID_TIME); + } + + private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { + RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { + super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); + } + + @Override + public long getId(BaseProgram item) { + return item.getId(); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java new file mode 100644 index 00000000..e49870f1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.playback; + +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; +import android.text.TextUtils; +import android.transition.Transition; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.util.TvSettings; + +import java.util.List; +import java.util.Locale; + +/** + * Fragment for DVR playback closed-caption/multi-audio settings. + */ +public class DvrPlaybackSideFragment extends GuidedStepFragment { + /** + * The tag for passing track infos to side fragments. + */ + public static final String TRACK_INFOS = "dvr_key_track_infos"; + /** + * The tag for passing selected track's ID to side fragments. + */ + public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id"; + + private static final int ACTION_ID_NO_SUBTITLE = -1; + private static final int CHECK_SET_ID = 1; + + private List<TvTrackInfo> mTrackInfos; + private String mSelectedTrackId; + private TvTrackInfo mSelectedTrack; + private int mTrackType; + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS); + mTrackType = mTrackInfos.get(0).getType(); + mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID); + mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment)); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState); + backgroundView.setBackgroundColor(getResources() + .getColor(R.color.lb_playback_controls_background_light)); + return backgroundView; + } + + @Override + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_NO_SUBTITLE) + .title(getString(R.string.closed_caption_option_item_off)) + .checkSetId(CHECK_SET_ID) + .checked(mSelectedTrackId == null) + .build()); + } + for (int i = 0; i < mTrackInfos.size(); i++) { + TvTrackInfo info = mTrackInfos.get(i); + boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId); + GuidedAction action = new GuidedAction.Builder(getActivity()) + .id(i) + .title(getTrackLabel(info, i)) + .checkSetId(CHECK_SET_ID) + .checked(checked) + .build(); + actions.add(action); + if (checked) { + mSelectedTrack = info; + } + } + } + + @Override + public void onGuidedActionFocused(GuidedAction action) { + int actionId = (int) action.getId(); + mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId)); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + int actionId = (int) action.getId(); + mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId); + TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack); + getFragmentManager().popBackStack(); + } + + @Override + public void onStart() { + super.onStart(); + // Workaround: when overlay fragment is faded out, any focus will lost due to overlay + // fragment's implementation. So we disable overlay fragment's fading here to prevent + // losing focus while users are interacting with the side fragment. + mOverlayFragment.setFadingEnabled(false); + } + + @Override + public void onStop() { + super.onStop(); + // We disable fading of overlay fragment to prevent side fragment from losing focus, + // therefore we should resume it here. + mOverlayFragment.setFadingEnabled(true); + mOverlayFragment.selectTrack(mTrackType, mSelectedTrack); + } + + private String getTrackLabel(TvTrackInfo track, int trackIndex) { + if (track.getLanguage() != null) { + return new Locale(track.getLanguage()).getDisplayName(); + } + return track.getType() == TvTrackInfo.TYPE_SUBTITLE ? + getString(R.string.closed_caption_unknown_language, trackIndex + 1) + : getString(R.string.multi_audio_unknown_language); + } + + @Override + protected void onProvideFragmentTransitions() { + super.onProvideFragmentTransitions(); + // Excludes the background scrim from transition to prevent the blinking caused by + // hiding the overlay fragment and sliding in the side fragment at the same time. + Transition t = getEnterTransition(); + if (t != null) { + t.excludeTarget(R.id.guidedstep_background, true); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java new file mode 100644 index 00000000..780bfb2f --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.playback; + +import android.media.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; +import android.media.session.PlaybackState; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.dvr.data.RecordedProgram; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +class DvrPlayer { + private static final String TAG = "DvrPlayer"; + private static final boolean DEBUG = false; + + /** + * The max rewinding speed supported by DVR player. + */ + public static final int MAX_REWIND_SPEED = 256; + /** + * The max fast-forwarding speed supported by DVR player. + */ + public static final int MAX_FAST_FORWARD_SPEED = 256; + + private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); + private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 + + private RecordedProgram mProgram; + private long mInitialSeekPositionMs; + private final TvView mTvView; + private DvrPlayerCallback mCallback; + private OnAspectRatioChangedListener mOnAspectRatioChangedListener; + private OnContentBlockedListener mOnContentBlockedListener; + private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; + private OnTrackSelectedListener mOnAudioTrackSelectedListener; + private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; + private String mSelectedAudioTrackId; + private String mSelectedSubtitleTrackId; + private float mAspectRatio = Float.NaN; + private int mPlaybackState = PlaybackState.STATE_NONE; + private long mTimeShiftCurrentPositionMs; + private boolean mPauseOnPrepared; + private boolean mHasClosedCaption; + private boolean mHasMultiAudio; + private final PlaybackParams mPlaybackParams = new PlaybackParams(); + private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); + private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + private boolean mTimeShiftPlayAvailable; + + public static class DvrPlayerCallback { + /** + * Called when the playback position is changed. The normal updating frequency is + * around 1 sec., which is restricted to the implementation of + * {@link android.media.tv.TvInputService}. + */ + public void onPlaybackPositionChanged(long positionMs) { } + /** + * Called when the playback state or the playback speed is changed. + */ + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } + /** + * Called when the playback toward the end. + */ + public void onPlaybackEnded() { } + } + + public interface OnAspectRatioChangedListener { + /** + * Called when the Video's aspect ratio is changed. + * + * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. + * Listeners should handle it carefully. + */ + void onAspectRatioChanged(float videoAspectRatio); + } + + public interface OnContentBlockedListener { + /** + * Called when the Video's aspect ratio is changed. + */ + void onContentBlocked(TvContentRating rating); + } + + public interface OnTracksAvailabilityChangedListener { + /** + * Called when the Video's subtitle or audio tracks are changed. + */ + void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); + } + + public interface OnTrackSelectedListener { + /** + * Called when certain subtitle or audio track is selected. + */ + void onTrackSelected(String selectedTrackId); + } + + public DvrPlayer(TvView tvView) { + mTvView = tvView; + mTvView.setCaptionEnabled(true); + mPlaybackParams.setSpeed(1.0f); + setTvViewCallbacks(); + setCallback(null); + } + + /** + * Prepares playback. + * + * @param doPlay indicates DVR player do or do not start playback after media is prepared. + */ + public void prepare(boolean doPlay) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "prepare()"); + if (mProgram == null) { + throw new IllegalStateException("Recorded program not set"); + } else if (mPlaybackState != PlaybackState.STATE_NONE) { + throw new IllegalStateException("Playback is already prepared"); + } + mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); + mPlaybackState = PlaybackState.STATE_CONNECTING; + mPauseOnPrepared = !doPlay; + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Resumes playback. + */ + public void play() throws IllegalStateException { + if (DEBUG) Log.d(TAG, "play()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or video not ready yet"); + } + switch (mPlaybackState) { + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + setPlaybackSpeed(1); + break; + default: + mTvView.timeShiftResume(); + } + mPlaybackState = PlaybackState.STATE_PLAYING; + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Pauses playback. + */ + public void pause() throws IllegalStateException { + if (DEBUG) Log.d(TAG, "pause()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + switch (mPlaybackState) { + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + setPlaybackSpeed(1); + // falls through + case PlaybackState.STATE_PLAYING: + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + break; + default: + break; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Fast-forwards playback with the given speed. If the given speed is larger than + * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. + */ + public void fastForward(int speed) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "fastForward()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (speed <= 0) { + throw new IllegalArgumentException("Speed cannot be negative or 0"); + } + if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { + return; + } + speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); + if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); + setPlaybackSpeed(speed); + mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; + mCallback.onPlaybackStateChanged(mPlaybackState, speed); + } + + /** + * Rewinds playback with the given speed. If the given speed is larger than + * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. + */ + public void rewind(int speed) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "rewind()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (speed <= 0) { + throw new IllegalArgumentException("Speed cannot be negative or 0"); + } + if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { + return; + } + speed = Math.min(speed, MAX_REWIND_SPEED); + if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); + setPlaybackSpeed(-speed); + mPlaybackState = PlaybackState.STATE_REWINDING; + mCallback.onPlaybackStateChanged(mPlaybackState, speed); + } + + /** + * Seeks playback to the specified position. + */ + public void seekTo(long positionMs) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "seekTo()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { + return; + } + positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); + if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); + mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); + if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || + mPlaybackState == PlaybackState.STATE_REWINDING) { + mPlaybackState = PlaybackState.STATE_PLAYING; + mTvView.timeShiftResume(); + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + } + + /** + * Resets playback. + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); + mPlaybackState = PlaybackState.STATE_NONE; + mTvView.reset(); + mTimeShiftPlayAvailable = false; + mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + mTimeShiftCurrentPositionMs = 0; + mPlaybackParams.setSpeed(1.0f); + mProgram = null; + mSelectedAudioTrackId = null; + mSelectedSubtitleTrackId = null; + } + + /** + * Sets callbacks for playback. + */ + public void setCallback(DvrPlayerCallback callback) { + if (callback != null) { + mCallback = callback; + } else { + mCallback = mEmptyCallback; + } + } + + /** + * Sets the listener to aspect ratio changing. + */ + public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { + mOnAspectRatioChangedListener = listener; + } + + /** + * Sets the listener to content blocking. + */ + public void setOnContentBlockedListener(OnContentBlockedListener listener) { + mOnContentBlockedListener = listener; + } + + /** + * Sets the listener to tracks changing. + */ + public void setOnTracksAvailabilityChangedListener( + OnTracksAvailabilityChangedListener listener) { + mOnTracksAvailabilityChangedListener = listener; + } + + /** + * Sets the listener to tracks of the given type being selected. + * + * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} + * or {@link TvTrackInfo#TYPE_SUBTITLE}. + */ + public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mOnAudioTrackSelectedListener = listener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mOnSubtitleTrackSelectedListener = listener; + } + } + + /** + * Gets the listener to tracks of the given type being selected. + */ + public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mOnAudioTrackSelectedListener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mOnSubtitleTrackSelectedListener; + } + return null; + } + + /** + * Sets recorded programs for playback. If the player is playing another program, stops it. + */ + public void setProgram(RecordedProgram program, long initialSeekPositionMs) { + if (mProgram != null && mProgram.equals(program)) { + return; + } + if (mPlaybackState != PlaybackState.STATE_NONE) { + reset(); + } + mInitialSeekPositionMs = initialSeekPositionMs; + mProgram = program; + } + + /** + * Returns the recorded program now playing. + */ + public RecordedProgram getProgram() { + return mProgram; + } + + /** + * Returns the currrent playback posistion in msecs. + */ + public long getPlaybackPosition() { + return mTimeShiftCurrentPositionMs; + } + + /** + * Returns the playback speed currently used. + */ + public int getPlaybackSpeed() { + return (int) mPlaybackParams.getSpeed(); + } + + /** + * Returns the playback state defined in {@link android.media.session.PlaybackState}. + */ + public int getPlaybackState() { + return mPlaybackState; + } + + /** + * Returns the subtitle tracks of the current playback. + */ + public ArrayList<TvTrackInfo> getSubtitleTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); + } + + /** + * Returns the audio tracks of the current playback. + */ + public ArrayList<TvTrackInfo> getAudioTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mSelectedAudioTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mSelectedSubtitleTrackId; + } + return null; + } + + /** + * Returns if playback of the recorded program is started. + */ + public boolean isPlaybackPrepared() { + return mPlaybackState != PlaybackState.STATE_NONE + && mPlaybackState != PlaybackState.STATE_CONNECTING; + } + + /** + * Selects the given track. + * + * @return ID of the selected track. + */ + String selectTrack(int trackType, TvTrackInfo selectedTrack) { + String oldSelectedTrackId = getSelectedTrackId(trackType); + String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); + if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { + if (selectedTrack == null) { + mTvView.selectTrack(trackType, null); + return null; + } else { + List<TvTrackInfo> tracks = mTvView.getTracks(trackType); + if (tracks != null && tracks.contains(selectedTrack)) { + mTvView.selectTrack(trackType, newSelectedTrackId); + return newSelectedTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { + // Track not found, disabled closed caption. + mTvView.selectTrack(trackType, null); + return null; + } + } + } + return oldSelectedTrackId; + } + + private void setSelectedTrackId(int trackType, String trackId) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mSelectedAudioTrackId = trackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mSelectedSubtitleTrackId = trackId; + } + } + + private void setPlaybackSpeed(int speed) { + mPlaybackParams.setSpeed(speed); + mTvView.timeShiftSetPlaybackParams(mPlaybackParams); + } + + private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { + return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); + } + + private void setTvViewCallbacks() { + mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { + @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); + mStartPositionMs = timeMs; + if (mTimeShiftPlayAvailable) { + resumeToWatchedPositionIfNeeded(); + } + } + + @Override + public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); + if (!mTimeShiftPlayAvailable) { + // Workaround of b/31436263 + return; + } + // Workaround of b/32211561, TIF won't report start position when TIS report + // its start position as 0. In that case, we have to do the prework of playback + // on the first time we get current position, and the start position should be 0 + // at that time. + if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mStartPositionMs = 0; + resumeToWatchedPositionIfNeeded(); + } + timeMs -= mStartPositionMs; + if (mPlaybackState == PlaybackState.STATE_REWINDING + && timeMs <= REWIND_POSITION_MARGIN_MS) { + play(); + } else { + mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); + mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); + if (timeMs >= mProgram.getDurationMillis()) { + pause(); + mCallback.onPlaybackEnded(); + } + } + } + }); + mTvView.setCallback(new TvView.TvInputCallback() { + @Override + public void onTimeShiftStatusChanged(String inputId, int status) { + if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); + if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE + && mPlaybackState == PlaybackState.STATE_CONNECTING) { + mTimeShiftPlayAvailable = true; + if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + // onTimeShiftStatusChanged is sometimes called after + // onTimeShiftStartPositionChanged is called. In this case, + // resumeToWatchedPositionIfNeeded needs to be called here. + resumeToWatchedPositionIfNeeded(); + } + } + } + + @Override + public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { + boolean hasClosedCaption = + !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); + boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; + if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) + && mOnTracksAvailabilityChangedListener != null) { + mOnTracksAvailabilityChangedListener + .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); + } + mHasClosedCaption = hasClosedCaption; + mHasMultiAudio = hasMultiAudio; + } + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { + setSelectedTrackId(type, trackId); + OnTrackSelectedListener listener = getOnTrackSelectedListener(type); + if (listener != null) { + listener.onTrackSelected(trackId); + } + } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null + && mOnAspectRatioChangedListener != null) { + List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); + if (trackInfos != null) { + for (TvTrackInfo trackInfo : trackInfos) { + if (trackInfo.getId().equals(trackId)) { + float videoAspectRatio; + int videoWidth = trackInfo.getVideoWidth(); + int videoHeight = trackInfo.getVideoHeight(); + if (videoWidth > 0 && videoHeight > 0) { + videoAspectRatio = trackInfo.getVideoPixelAspectRatio() + * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); + } else { + // Aspect ratio is unknown. Pass the message to listeners. + videoAspectRatio = 0; + } + if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); + if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { + mOnAspectRatioChangedListener + .onAspectRatioChanged(videoAspectRatio); + mAspectRatio = videoAspectRatio; + return; + } + } + } + } + } + } + + @Override + public void onContentBlocked(String inputId, TvContentRating rating) { + if (mOnContentBlockedListener != null) { + mOnContentBlockedListener.onContentBlocked(rating); + } + } + }); + } + + private void resumeToWatchedPositionIfNeeded() { + if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, + SEEK_POSITION_MARGIN_MS) + mStartPositionMs); + mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + if (mPauseOnPrepared) { + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + mPauseOnPrepared = false; + } else { + mTvView.timeShiftResume(); + mPlaybackState = PlaybackState.STATE_PLAYING; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } +}
\ No newline at end of file |