/* * Copyright (C) 2015 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; import android.annotation.SuppressLint; import android.content.Context; import android.os.Handler; import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.Range; import com.android.tv.analytics.Tracker; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.ProgramDataManager; import com.android.tv.data.ProgramImpl; import com.android.tv.data.api.Channel; import com.android.tv.data.api.Program; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.api.TunableTvViewPlayingApi.TimeShiftListener; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Queue; import java.util.concurrent.TimeUnit; /** * A class which manages the time shift feature in TV app. It consists of two parts. {@link * PlayController} controls the playback such as play/pause, rewind and fast-forward using {@link * TunableTvView} which communicates with TvInputService through {@link * android.media.tv.TvInputService.Session}. {@link ProgramManager} loads programs of the current * channel in the background. */ public class TimeShiftManager { private static final String TAG = "TimeShiftManager"; private static final boolean DEBUG = false; @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING}) public @interface PlayStatus {} public static final int PLAY_STATUS_PAUSED = 0; public static final int PLAY_STATUS_PLAYING = 1; @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_SPEED_1X, PLAY_SPEED_2X, PLAY_SPEED_3X, PLAY_SPEED_4X, PLAY_SPEED_5X}) public @interface PlaySpeed {} public static final int PLAY_SPEED_1X = 1; public static final int PLAY_SPEED_2X = 2; public static final int PLAY_SPEED_3X = 3; public static final int PLAY_SPEED_4X = 4; public static final int PLAY_SPEED_5X = 5; @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_DIRECTION_FORWARD, PLAY_DIRECTION_BACKWARD}) public @interface PlayDirection {} public static final int PLAY_DIRECTION_FORWARD = 0; public static final int PLAY_DIRECTION_BACKWARD = 1; @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, value = { TIME_SHIFT_ACTION_ID_PLAY, TIME_SHIFT_ACTION_ID_PAUSE, TIME_SHIFT_ACTION_ID_REWIND, TIME_SHIFT_ACTION_ID_FAST_FORWARD, TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT }) public @interface TimeShiftActionId {} public static final int TIME_SHIFT_ACTION_ID_PLAY = 1; public static final int TIME_SHIFT_ACTION_ID_PAUSE = 1 << 1; public static final int TIME_SHIFT_ACTION_ID_REWIND = 1 << 2; public static final int TIME_SHIFT_ACTION_ID_FAST_FORWARD = 1 << 3; public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS = 1 << 4; public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT = 1 << 5; private static final int MSG_GET_CURRENT_POSITION = 1000; private static final int MSG_PREFETCH_PROGRAM = 1001; private static final long REQUEST_CURRENT_POSITION_INTERVAL = TimeUnit.SECONDS.toMillis(1); private static final long MAX_PLACEHOLDER_PROGRAM_DURATION = TimeUnit.MINUTES.toMillis(30); @VisibleForTesting static final long INVALID_TIME = -1; static final long CURRENT_TIME = -2; private static final long PREFETCH_TIME_OFFSET_FROM_PROGRAM_END = TimeUnit.MINUTES.toMillis(1); private static final long PREFETCH_DURATION_FOR_NEXT = TimeUnit.HOURS.toMillis(2); private static final long ALLOWED_START_TIME_OFFSET = TimeUnit.DAYS.toMillis(14); private static final long TWO_WEEKS_MS = TimeUnit.DAYS.toMillis(14); @VisibleForTesting static final long REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3); /** * If the user presses the {@link android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS} button within * this threshold from the program start time, the play position moves to the start of the * previous program. Otherwise, the play position moves to the start of the current program. * This value is specified in the UX document. */ private static final long PROGRAM_START_TIME_THRESHOLD = TimeUnit.SECONDS.toMillis(3); /** * If the current position enters within this range from the recording start time, rewind action * and jump to previous action is disabled. Similarly, if the current position enters within * this range from the current system time, fast forward action and jump to next action is * disabled. It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at * least. */ private static final long DISABLE_ACTION_THRESHOLD = 3 * REQUEST_CURRENT_POSITION_INTERVAL; /** * If the current position goes out of this range from the recording start time, rewind action * and jump to previous action is enabled. Similarly, if the current position goes out of this * range from the current system time, fast forward action and jump to next action is enabled. * Enable threshold and disable threshold must be different because the current position does * not have the continuous value. It changes every one second. */ private static final long ENABLE_ACTION_THRESHOLD = DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL; /** * The current position sent from TIS can not be exactly the same as the current system time due * to the elapsed time to pass the message from TIS to TV app. So the boundary threshold is * necessary. The same goes for the recording start time. It's the same {@link * #REQUEST_CURRENT_POSITION_INTERVAL}. */ private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL; private final PlayController mPlayController; private final ProgramManager mProgramManager; private final Tracker mTracker; @VisibleForTesting final CurrentPositionMediator mCurrentPositionMediator = new CurrentPositionMediator(); private Listener mListener; private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener; private int mEnabledActionIds = TIME_SHIFT_ACTION_ID_PLAY | TIME_SHIFT_ACTION_ID_PAUSE | TIME_SHIFT_ACTION_ID_REWIND | TIME_SHIFT_ACTION_ID_FAST_FORWARD | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; @TimeShiftActionId private int mLastActionId = 0; private final Context mContext; private Program mCurrentProgram; // This variable is used to block notification while changing the availability status. private boolean mNotificationEnabled; private final Handler mHandler = new TimeShiftHandler(this); public TimeShiftManager( Context context, TunableTvView tvView, ProgramDataManager programDataManager, Tracker tracker, OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener) { mContext = context; mPlayController = new PlayController(tvView); mProgramManager = new ProgramManager(programDataManager); mTracker = tracker; mOnCurrentProgramUpdatedListener = onCurrentProgramUpdatedListener; } /** Sets a listener which will receive events from this class. */ public void setListener(Listener listener) { mListener = listener; } /** Checks if the trick play is available for the current channel. */ public boolean isAvailable() { return mPlayController.mAvailable; } /** Returns the current time position in milliseconds. */ public long getCurrentPositionMs() { return mCurrentPositionMediator.mCurrentPositionMs; } void setCurrentPositionMs(long currentTimeMs) { mCurrentPositionMediator.onCurrentPositionChanged(currentTimeMs); } /** Returns the start time of the recording in milliseconds. */ public long getRecordStartTimeMs() { long oldestProgramStartTime = mProgramManager.getOldestProgramStartTime(); return oldestProgramStartTime == INVALID_TIME ? INVALID_TIME : mPlayController.mRecordStartTimeMs; } /** Returns the end time of the recording in milliseconds. */ public long getRecordEndTimeMs() { if (mPlayController.mRecordEndTimeMs == CURRENT_TIME) { return System.currentTimeMillis(); } else { return mPlayController.mRecordEndTimeMs; } } /** * Plays the media. * * @throws IllegalStateException if the trick play is not available. */ public void play() { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PLAY)) { return; } mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY); mLastActionId = TIME_SHIFT_ACTION_ID_PLAY; mPlayController.play(); updateActions(); } /** * Pauses the playback. * * @throws IllegalStateException if the trick play is not available. */ public void pause() { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PAUSE)) { return; } mLastActionId = TIME_SHIFT_ACTION_ID_PAUSE; mTracker.sendTimeShiftAction(mLastActionId); mPlayController.pause(); updateActions(); } /** * Toggles the playing and paused state. * * @throws IllegalStateException if the trick play is not available. */ public void togglePlayPause() { mPlayController.togglePlayPause(); } /** * Plays the media in backward direction. The playback speed is increased by 1x each time this * is called. The range of the speed is from 2x to 5x. If the playing position is considered the * same as the record start time, it does nothing * * @throws IllegalStateException if the trick play is not available. */ public void rewind() { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND)) { return; } mLastActionId = TIME_SHIFT_ACTION_ID_REWIND; mTracker.sendTimeShiftAction(mLastActionId); mPlayController.rewind(); updateActions(); } /** * Plays the media in forward direction. The playback speed is increased by 1x each time this is * called. The range of the speed is from 2x to 5x. If the playing position is the same as the * current time, it does nothing. * * @throws IllegalStateException if the trick play is not available. */ public void fastForward() { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)) { return; } mLastActionId = TIME_SHIFT_ACTION_ID_FAST_FORWARD; mTracker.sendTimeShiftAction(mLastActionId); mPlayController.fastForward(); updateActions(); } /** * Jumps to the start of the current program. If the currently playing position is within 3 * seconds (={@link #PROGRAM_START_TIME_THRESHOLD})from the start time of the program, it goes * to the start of the previous program if exists. If the playing position is the same as the * record start time, it does nothing. * * @throws IllegalStateException if the trick play is not available. */ public void jumpToPrevious() { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) { return; } Program program = mProgramManager.getProgramAt( mCurrentPositionMediator.mCurrentPositionMs - PROGRAM_START_TIME_THRESHOLD); if (program == null) { return; } long seekPosition = Math.max(program.getStartTimeUtcMillis(), mPlayController.mRecordStartTimeMs); mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS; mTracker.sendTimeShiftAction(mLastActionId); mPlayController.seekTo(seekPosition); mCurrentPositionMediator.onSeekRequested(seekPosition); updateActions(); } /** * Jumps to the start of the next program if exists. If there's no next program, it jumps to the * current system time and shows the live TV. If the playing position is considered the same as * the current time, it does nothing. * * @throws IllegalStateException if the trick play is not available. */ public void jumpToNext() { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) { return; } Program currentProgram = mProgramManager.getProgramAt(mCurrentPositionMediator.mCurrentPositionMs); if (currentProgram == null) { return; } Program nextProgram = mProgramManager.getProgramAt(currentProgram.getEndTimeUtcMillis()); long currentTimeMs = System.currentTimeMillis(); mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; mTracker.sendTimeShiftAction(mLastActionId); if (nextProgram == null || nextProgram.getStartTimeUtcMillis() > currentTimeMs) { mPlayController.seekTo(currentTimeMs); if (mPlayController.isForwarding()) { // The current position will be the current system time from now. mPlayController.mIsPlayOffsetChanged = false; mCurrentPositionMediator.initialize(currentTimeMs); } else { // The current position would not be the current system time. // So need to wait for the correct time from TIS. mCurrentPositionMediator.onSeekRequested(currentTimeMs); } } else { mPlayController.seekTo(nextProgram.getStartTimeUtcMillis()); mCurrentPositionMediator.onSeekRequested(nextProgram.getStartTimeUtcMillis()); } updateActions(); } /** Returns the playback status. The value is PLAY_STATUS_PAUSED or PLAY_STATUS_PLAYING. */ @PlayStatus public int getPlayStatus() { return mPlayController.mPlayStatus; } /** * Returns the displayed playback speed. The value is one of PLAY_SPEED_1X, PLAY_SPEED_2X, * PLAY_SPEED_3X, PLAY_SPEED_4X and PLAY_SPEED_5X. */ @PlaySpeed public int getDisplayedPlaySpeed() { return mPlayController.mDisplayedPlaySpeed; } /** * Returns the playback speed. The value is PLAY_DIRECTION_FORWARD or PLAY_DIRECTION_BACKWARD. */ @PlayDirection public int getPlayDirection() { return mPlayController.mPlayDirection; } /** Returns the ID of the last action.. */ @TimeShiftActionId public int getLastActionId() { return mLastActionId; } /** Enables or disables the time-shift actions. */ @VisibleForTesting void enableAction(@TimeShiftActionId int actionId, boolean enable) { int oldEnabledActionIds = mEnabledActionIds; if (enable) { mEnabledActionIds |= actionId; } else { mEnabledActionIds &= ~actionId; } if (mNotificationEnabled && mListener != null && oldEnabledActionIds != mEnabledActionIds) { mListener.onActionEnabledChanged(actionId, enable); } } public boolean isActionEnabled(@TimeShiftActionId int actionId) { return (mEnabledActionIds & actionId) == actionId; } private void updateActions() { if (isAvailable()) { enableAction(TIME_SHIFT_ACTION_ID_PLAY, true); enableAction(TIME_SHIFT_ACTION_ID_PAUSE, true); // Rewind action and jump to previous action. long threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND) ? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD; boolean enabled = mCurrentPositionMediator.mCurrentPositionMs - mPlayController.mRecordStartTimeMs > threshold; enableAction(TIME_SHIFT_ACTION_ID_REWIND, enabled); enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, enabled); // Fast forward action and jump to next action threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD) ? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD; enabled = getRecordEndTimeMs() - mCurrentPositionMediator.mCurrentPositionMs > threshold; enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled); enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled); } else { enableAction(TIME_SHIFT_ACTION_ID_PLAY, false); enableAction(TIME_SHIFT_ACTION_ID_PAUSE, false); enableAction(TIME_SHIFT_ACTION_ID_REWIND, false); enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, false); enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, false); enableAction(TIME_SHIFT_ACTION_ID_PLAY, false); } } private void updateCurrentProgram() { SoftPreconditions.checkState(isAvailable(), TAG, "Time shift is not available"); SoftPreconditions.checkState(mCurrentPositionMediator.mCurrentPositionMs != INVALID_TIME); Program currentProgram = getProgramAt(mCurrentPositionMediator.mCurrentPositionMs); if (!Program.isProgramValid(currentProgram)) { currentProgram = null; } if (!Objects.equals(mCurrentProgram, currentProgram)) { if (DEBUG) Log.d(TAG, "Current program has been updated. " + currentProgram); mCurrentProgram = currentProgram; if (mNotificationEnabled && mOnCurrentProgramUpdatedListener != null) { Channel channel = mPlayController.getCurrentChannel(); if (channel != null) { mOnCurrentProgramUpdatedListener.onCurrentProgramUpdated( channel.getId(), mCurrentProgram); mPlayController.onCurrentProgramChanged(); } } } } /** * Returns {@code true} if the trick play is available and it's playing to the forward direction * with normal speed, otherwise {@code false}. */ public boolean isNormalPlaying() { return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PLAYING && mPlayController.mPlayDirection == PLAY_DIRECTION_FORWARD && mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X; } /** Checks if the trick play is available and it's playback status is paused. */ public boolean isPaused() { return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED; } /** Returns the program which airs at the given time. */ @NonNull public Program getProgramAt(long timeMs) { Program program = mProgramManager.getProgramAt(timeMs); if (program == null) { // Guard just in case when the program prefetch handler doesn't work on time. mProgramManager.addPlaceholderProgramsAt(timeMs); program = mProgramManager.getProgramAt(timeMs); } return program; } void onAvailabilityChanged() { mCurrentPositionMediator.initialize(mPlayController.mRecordStartTimeMs); mProgramManager.onAvailabilityChanged( mPlayController.mAvailable, mPlayController.getCurrentChannel(), mPlayController.mRecordStartTimeMs); updateActions(); // Availability change notification should be always sent // even if mNotificationEnabled is false. if (mListener != null) { mListener.onAvailabilityChanged(); } } void onRecordTimeRangeChanged() { if (mPlayController.mAvailable) { mProgramManager.onRecordTimeRangeChanged( mPlayController.mRecordStartTimeMs, mPlayController.mRecordEndTimeMs); } updateActions(); if (mNotificationEnabled && mListener != null) { mListener.onRecordTimeRangeChanged(); } } void onCurrentPositionChanged() { updateActions(); updateCurrentProgram(); if (mNotificationEnabled && mListener != null) { mListener.onCurrentPositionChanged(); } } void onPlayStatusChanged(@PlayStatus int status) { if (mNotificationEnabled && mListener != null) { mListener.onPlayStatusChanged(status); } } void onProgramInfoChanged() { updateCurrentProgram(); if (mNotificationEnabled && mListener != null) { mListener.onProgramInfoChanged(); } } /** * Returns the current program which airs right now. * *
If the program is a placeholder program, which means there's no program information,
* returns {@code null}.
*/
@Nullable
public Program getCurrentProgram() {
if (isAvailable()) {
return mCurrentProgram;
}
return null;
}
private int getPlaybackSpeed() {
if (mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X) {
return 1;
} else {
long durationMs =
(getCurrentProgram() == null ? 0 : getCurrentProgram().getDurationMillis());
if (mPlayController.mDisplayedPlaySpeed > PLAY_SPEED_5X) {
Log.w(
TAG,
"Unknown displayed play speed is chosen : "
+ mPlayController.mDisplayedPlaySpeed);
return TimeShiftUtils.getMaxPlaybackSpeed(durationMs);
} else {
return TimeShiftUtils.getPlaybackSpeed(
mPlayController.mDisplayedPlaySpeed - PLAY_SPEED_2X, durationMs);
}
}
}
/** A class which controls the trick play. */
private class PlayController {
private final TunableTvView mTvView;
private long mAvailablityChangedTimeMs;
private long mRecordStartTimeMs;
private long mRecordEndTimeMs;
@PlayStatus private int mPlayStatus = PLAY_STATUS_PAUSED;
@PlaySpeed private int mDisplayedPlaySpeed = PLAY_SPEED_1X;
@PlayDirection private int mPlayDirection = PLAY_DIRECTION_FORWARD;
private int mPlaybackSpeed;
private boolean mAvailable;
/**
* Indicates that the trick play is not playing the current time position. It is set true
* when {@link PlayController#pause}, {@link PlayController#rewind}, {@link
* PlayController#fastForward} and {@link PlayController#seekTo} is called. If it is true,
* the current time is equal to System.currentTimeMillis().
*/
private boolean mIsPlayOffsetChanged;
PlayController(TunableTvView tvView) {
mTvView = tvView;
mTvView.setTimeShiftListener(
new TimeShiftListener() {
@Override
public void onAvailabilityChanged() {
if (DEBUG) {
Log.d(
TAG,
"onAvailabilityChanged(available="
+ mTvView.isTimeShiftAvailable()
+ ")");
}
PlayController.this.onAvailabilityChanged();
}
@Override
public void onRecordStartTimeChanged(long recordStartTimeMs) {
if (!SoftPreconditions.checkState(
mAvailable, TAG, "Trick play is not available.")) {
return;
}
if (recordStartTimeMs
< mAvailablityChangedTimeMs - ALLOWED_START_TIME_OFFSET) {
Log.e(
TAG,
"The start time is too earlier than the time of"
+ " availability: {startTime: "
+ recordStartTimeMs
+ ", availability: "
+ mAvailablityChangedTimeMs);
return;
}
if (recordStartTimeMs > System.currentTimeMillis()) {
// The time reported by TvInputService might not consistent with
// system
// clock,, use system's current time instead.
Log.e(
TAG,
"The start time should not be earlier than the current"
+ " time, reset the start time to the system's current"
+ " time: {startTime: "
+ recordStartTimeMs
+ ", current time: "
+ System.currentTimeMillis());
recordStartTimeMs = System.currentTimeMillis();
}
if (mRecordStartTimeMs == recordStartTimeMs) {
return;
}
mRecordStartTimeMs = recordStartTimeMs;
TimeShiftManager.this.onRecordTimeRangeChanged();
// According to the UX guidelines, the stream should be resumed if the
// recording buffer fills up while paused, which means that the current
// time
// position is the same as or before the recording start time.
// But, for this application and the TIS, it's an erroneous and
// confusing
// situation if the current time position is before the recording start
// time.
// So, we recommend the TIS to keep the current time position greater
// than or
// equal to the recording start time.
// And here, we assume that the buffer is full if the current time
// position
// is nearly equal to the recording start time.
if (mPlayStatus == PLAY_STATUS_PAUSED
&& getCurrentPositionMs() - mRecordStartTimeMs
< RECORDING_BOUNDARY_THRESHOLD) {
TimeShiftManager.this.play();
}
}
});
}
void onAvailabilityChanged() {
boolean newAvailable = mTvView.isTimeShiftAvailable();
if (mAvailable == newAvailable) {
return;
}
mAvailable = newAvailable;
// Do not send the notifications while the availability is changing,
// because the variables are in the intermediate state.
// For example, the current program can be null.
mNotificationEnabled = false;
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
mPlayDirection = PLAY_DIRECTION_FORWARD;
mHandler.removeMessages(MSG_GET_CURRENT_POSITION);
if (mAvailable) {
mAvailablityChangedTimeMs = System.currentTimeMillis();
mIsPlayOffsetChanged = false;
mRecordStartTimeMs = mAvailablityChangedTimeMs;
mRecordEndTimeMs = CURRENT_TIME;
// When the media availability message has come.
mPlayController.setPlayStatus(PLAY_STATUS_PLAYING);
mHandler.sendEmptyMessageDelayed(
MSG_GET_CURRENT_POSITION, REQUEST_CURRENT_POSITION_INTERVAL);
} else {
mAvailablityChangedTimeMs = INVALID_TIME;
mIsPlayOffsetChanged = false;
mRecordStartTimeMs = INVALID_TIME;
mRecordEndTimeMs = INVALID_TIME;
// When the tune command is sent.
mPlayController.setPlayStatus(PLAY_STATUS_PAUSED);
}
TimeShiftManager.this.onAvailabilityChanged();
mNotificationEnabled = true;
}
void handleGetCurrentPosition() {
if (mIsPlayOffsetChanged) {
long currentTimeMs =
mRecordEndTimeMs == CURRENT_TIME
? System.currentTimeMillis()
: mRecordEndTimeMs;
long currentPositionMs =
Math.max(
Math.min(mTvView.timeShiftGetCurrentPositionMs(), currentTimeMs),
mRecordStartTimeMs);
boolean isCurrentTime =
currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD;
long newCurrentPositionMs;
if (isCurrentTime && isForwarding()) {
// It's playing forward and the current playing position reached
// the current system time. i.e. The live stream is played.
// Therefore no need to call TvView.timeShiftGetCurrentPositionMs
// any more.
newCurrentPositionMs = currentTimeMs;
mIsPlayOffsetChanged = false;
if (mDisplayedPlaySpeed > PLAY_SPEED_1X) {
TimeShiftManager.this.play();
}
} else {
newCurrentPositionMs = currentPositionMs;
boolean isRecordStartTime =
currentPositionMs - mRecordStartTimeMs < RECORDING_BOUNDARY_THRESHOLD;
if (isRecordStartTime && isRewinding()) {
TimeShiftManager.this.play();
}
}
setCurrentPositionMs(newCurrentPositionMs);
} else {
setCurrentPositionMs(System.currentTimeMillis());
TimeShiftManager.this.onCurrentPositionChanged();
}
// Need to send message here just in case there is no or invalid response
// for the current time position request from TIS.
mHandler.sendEmptyMessageDelayed(
MSG_GET_CURRENT_POSITION, REQUEST_CURRENT_POSITION_INTERVAL);
}
void play() {
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
mPlayDirection = PLAY_DIRECTION_FORWARD;
mTvView.timeShiftPlay();
setPlayStatus(PLAY_STATUS_PLAYING);
}
void pause() {
mDisplayedPlaySpeed = PLAY_SPEED_1X;
mPlaybackSpeed = 1;
mTvView.timeShiftPause();
setPlayStatus(PLAY_STATUS_PAUSED);
mIsPlayOffsetChanged = true;
}
void togglePlayPause() {
if (mPlayStatus == PLAY_STATUS_PAUSED) {
play();
mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY);
} else {
pause();
mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PAUSE);
}
}
void rewind() {
if (mPlayDirection == PLAY_DIRECTION_BACKWARD) {
increaseDisplayedPlaySpeed();
} else {
mDisplayedPlaySpeed = PLAY_SPEED_2X;
}
mPlayDirection = PLAY_DIRECTION_BACKWARD;
mPlaybackSpeed = getPlaybackSpeed();
mTvView.timeShiftRewind(mPlaybackSpeed);
setPlayStatus(PLAY_STATUS_PLAYING);
mIsPlayOffsetChanged = true;
}
void fastForward() {
if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
increaseDisplayedPlaySpeed();
} else {
mDisplayedPlaySpeed = PLAY_SPEED_2X;
}
mPlayDirection = PLAY_DIRECTION_FORWARD;
mPlaybackSpeed = getPlaybackSpeed();
mTvView.timeShiftFastForward(mPlaybackSpeed);
setPlayStatus(PLAY_STATUS_PLAYING);
mIsPlayOffsetChanged = true;
}
/** Moves to the specified time. */
void seekTo(long timeMs) {
mTvView.timeShiftSeekTo(
Math.min(
mRecordEndTimeMs == CURRENT_TIME
? System.currentTimeMillis()
: mRecordEndTimeMs,
Math.max(mRecordStartTimeMs, timeMs)));
mIsPlayOffsetChanged = true;
}
void onCurrentProgramChanged() {
// Update playback speed
if (mDisplayedPlaySpeed == PLAY_SPEED_1X) {
return;
}
int playbackSpeed = getPlaybackSpeed();
if (playbackSpeed != mPlaybackSpeed) {
mPlaybackSpeed = playbackSpeed;
if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
mTvView.timeShiftFastForward(mPlaybackSpeed);
} else {
mTvView.timeShiftRewind(mPlaybackSpeed);
}
}
}
@SuppressLint("SwitchIntDef")
private void increaseDisplayedPlaySpeed() {
switch (mDisplayedPlaySpeed) {
case PLAY_SPEED_1X:
mDisplayedPlaySpeed = PLAY_SPEED_2X;
break;
case PLAY_SPEED_2X:
mDisplayedPlaySpeed = PLAY_SPEED_3X;
break;
case PLAY_SPEED_3X:
mDisplayedPlaySpeed = PLAY_SPEED_4X;
break;
case PLAY_SPEED_4X:
mDisplayedPlaySpeed = PLAY_SPEED_5X;
break;
}
}
private void setPlayStatus(@PlayStatus int status) {
mPlayStatus = status;
TimeShiftManager.this.onPlayStatusChanged(status);
}
boolean isForwarding() {
return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_FORWARD;
}
private boolean isRewinding() {
return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_BACKWARD;
}
Channel getCurrentChannel() {
return mTvView.getCurrentChannel();
}
}
private class ProgramManager {
private final ProgramDataManager mProgramDataManager;
private Channel mChannel;
private final List