| /* |
| * 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.content.ContentResolver; |
| 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.common.recording.RecordedProgram; |
| import com.android.tv.data.Channel; |
| import com.android.tv.data.OnCurrentProgramUpdatedListener; |
| import com.android.tv.data.Program; |
| import com.android.tv.data.ProgramDataManager; |
| import com.android.tv.ui.TunableTvView; |
| import com.android.tv.ui.TunableTvView.TimeShiftListener; |
| import com.android.tv.util.AsyncDbTask; |
| 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 Live TV. 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; |
| |
| private static final int SHORT_PROGRAM_THRESHOLD_MILLIS = 46 * 60 * 1000; // 46 mins. |
| private static final int[] SHORT_PROGRAM_SPEED_FACTORS = new int[] {2, 4, 12, 48}; |
| private static final int[] LONG_PROGRAM_SPEED_FACTORS = new int[] {2, 8, 32, 128}; |
| |
| @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_DUMMY_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); |
| |
| @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 Live TV. |
| * So the boundary threshold is necessary. |
| * The same goes for the recording start time. |
| * It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at least. |
| */ |
| private static final long RECORDING_BOUNDARY_THRESHOLD = 3 * 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; |
| |
| // TODO: Remove these variables once API level 23 is available. |
| 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; |
| tvView.setOnScreenBlockedListener(new TunableTvView.OnScreenBlockingChangedListener() { |
| @Override |
| public void onScreenBlockingChanged(boolean blocked) { |
| mPlayController.onAvailabilityChanged(); |
| } |
| }); |
| } |
| |
| /** |
| * 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() { |
| Program currentProgram = getProgramAt(mCurrentPositionMediator.mCurrentPositionMs); |
| if (!Program.isValid(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(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks whether the TV is playing the recorded content. |
| */ |
| public boolean isRecordingPlayback() { |
| return mPlayController.mRecordingPlayback; |
| } |
| |
| /** |
| * 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.addDummyProgramsAt(timeMs); |
| program = mProgramManager.getProgramAt(timeMs); |
| } |
| return program; |
| } |
| |
| void onAvailabilityChanged() { |
| mProgramManager.onAvailabilityChanged(mPlayController.mAvailable, |
| mPlayController.mRecordingPlayback ? null : 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.<p> |
| * |
| * If the program is a dummy program, which means there's no program information, |
| * returns {@code null}. |
| */ |
| @Nullable |
| public Program getCurrentProgram() { |
| if (isAvailable()) { |
| return mCurrentProgram; |
| } |
| return null; |
| } |
| |
| private int getPlaybackSpeed() { |
| int[] playbackSpeedList; |
| if (getCurrentProgram() == null || getCurrentProgram().getEndTimeUtcMillis() |
| - getCurrentProgram().getStartTimeUtcMillis() > SHORT_PROGRAM_THRESHOLD_MILLIS) { |
| playbackSpeedList = LONG_PROGRAM_SPEED_FACTORS; |
| } else { |
| playbackSpeedList = SHORT_PROGRAM_SPEED_FACTORS; |
| } |
| switch (mPlayController.mDisplayedPlaySpeed) { |
| case PLAY_SPEED_1X: |
| return 1; |
| case PLAY_SPEED_2X: |
| return playbackSpeedList[0]; |
| case PLAY_SPEED_3X: |
| return playbackSpeedList[1]; |
| case PLAY_SPEED_4X: |
| return playbackSpeedList[2]; |
| case PLAY_SPEED_5X: |
| return playbackSpeedList[3]; |
| default: |
| Log.w(TAG, "Unknown displayed play speed is chosen : " |
| + mPlayController.mDisplayedPlaySpeed); |
| return 1; |
| } |
| } |
| |
| /** |
| * A class which controls the trick play. |
| */ |
| private class PlayController { |
| private final TunableTvView mTvView; |
| |
| 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; |
| private boolean mRecordingPlayback; |
| |
| /** |
| * 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() { |
| PlayController.this.onAvailabilityChanged(); |
| } |
| |
| @Override |
| public void onRecordStartTimeChanged(long recordStartTimeMs) { |
| 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() && !mTvView.isScreenBlocked(); |
| 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; |
| mRecordingPlayback = mTvView.isRecordingPlayback(); |
| if (mRecordingPlayback) { |
| RecordedProgram recordedProgram = mTvView.getPlayingRecordedProgram(); |
| SoftPreconditions.checkNotNull(recordedProgram); |
| mIsPlayOffsetChanged = true; |
| mRecordStartTimeMs = 0; |
| mRecordEndTimeMs = recordedProgram.getDurationMillis(); |
| } else { |
| mIsPlayOffsetChanged = false; |
| mRecordStartTimeMs = System.currentTimeMillis(); |
| mRecordEndTimeMs = CURRENT_TIME; |
| } |
| mCurrentPositionMediator.initialize(mRecordStartTimeMs); |
| mHandler.removeMessages(MSG_GET_CURRENT_POSITION); |
| |
| if (mAvailable) { |
| // When the media availability message has come. |
| mPlayController.setPlayStatus(PLAY_STATUS_PLAYING); |
| mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION, |
| REQUEST_CURRENT_POSITION_INTERVAL); |
| } else { |
| // 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); |
| } |
| } |
| } |
| |
| 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<Program> mPrograms = new ArrayList<>(); |
| private final Queue<Range<Long>> mProgramLoadQueue = new LinkedList<>(); |
| private LoadProgramsForCurrentChannelTask mProgramLoadTask = null; |
| private int mEmptyFetchCount = 0; |
| |
| ProgramManager(ProgramDataManager programDataManager) { |
| mProgramDataManager = programDataManager; |
| } |
| |
| void onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs) { |
| if (DEBUG) { |
| Log.d(TAG, "onAvailabilityChanged(" + available + "+," + channel + ", " |
| + currentPositionMs + ")"); |
| } |
| |
| mProgramLoadQueue.clear(); |
| if (mProgramLoadTask != null) { |
| mProgramLoadTask.cancel(true); |
| } |
| mHandler.removeMessages(MSG_PREFETCH_PROGRAM); |
| mPrograms.clear(); |
| mEmptyFetchCount = 0; |
| mChannel = channel; |
| if (channel == null || channel.isPassthrough()) { |
| return; |
| } |
| if (available) { |
| Program program = mProgramDataManager.getCurrentProgram(channel.getId()); |
| long prefetchStartTimeMs; |
| if (program != null) { |
| mPrograms.add(program); |
| prefetchStartTimeMs = program.getEndTimeUtcMillis(); |
| } else { |
| prefetchStartTimeMs = Utils.floorTime(currentPositionMs, |
| MAX_DUMMY_PROGRAM_DURATION); |
| } |
| // Create dummy program |
| mPrograms.addAll(createDummyPrograms(prefetchStartTimeMs, |
| currentPositionMs + PREFETCH_DURATION_FOR_NEXT)); |
| schedulePrefetchPrograms(); |
| TimeShiftManager.this.onProgramInfoChanged(); |
| } |
| } |
| |
| void onRecordTimeRangeChanged(long startTimeMs, long endTimeMs) { |
| if (mChannel == null || mChannel.isPassthrough()) { |
| return; |
| } |
| if (endTimeMs == CURRENT_TIME) { |
| endTimeMs = System.currentTimeMillis(); |
| } |
| |
| long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION); |
| boolean needToLoad = addDummyPrograms(fetchStartTimeMs, |
| endTimeMs + PREFETCH_DURATION_FOR_NEXT); |
| if (needToLoad) { |
| Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs); |
| mProgramLoadQueue.add(period); |
| startTaskIfNeeded(); |
| } |
| } |
| |
| private void startTaskIfNeeded() { |
| if (mProgramLoadQueue.isEmpty()) { |
| return; |
| } |
| if (mProgramLoadTask == null || mProgramLoadTask.isCancelled()) { |
| startNext(); |
| } else { |
| switch (mProgramLoadTask.getStatus()) { |
| case PENDING: |
| if (mProgramLoadTask.overlaps(mProgramLoadQueue)) { |
| if (mProgramLoadTask.cancel(true)) { |
| mProgramLoadQueue.add(mProgramLoadTask.getPeriod()); |
| mProgramLoadTask = null; |
| startNext(); |
| } |
| } |
| break; |
| case RUNNING: |
| // Remove pending task fully satisfied by the current |
| Range<Long> current = mProgramLoadTask.getPeriod(); |
| Iterator<Range<Long>> i = mProgramLoadQueue.iterator(); |
| while (i.hasNext()) { |
| Range<Long> r = i.next(); |
| if (current.contains(r)) { |
| i.remove(); |
| } |
| } |
| break; |
| case FINISHED: |
| // The task should have already cleared it self, clear and restart anyways. |
| Log.w(TAG, mProgramLoadTask + " is finished, but was not cleared"); |
| startNext(); |
| break; |
| } |
| } |
| } |
| |
| private void startNext() { |
| mProgramLoadTask = null; |
| if (mProgramLoadQueue.isEmpty()) { |
| return; |
| } |
| |
| Range<Long> next = mProgramLoadQueue.poll(); |
| // Extend next to include any overlapping Ranges. |
| Iterator<Range<Long>> i = mProgramLoadQueue.iterator(); |
| while(i.hasNext()) { |
| Range<Long> r = i.next(); |
| if(next.contains(r.getLower()) || next.contains(r.getUpper())){ |
| i.remove(); |
| next = next.extend(r); |
| } |
| } |
| if (mChannel != null) { |
| mProgramLoadTask = new LoadProgramsForCurrentChannelTask( |
| mContext.getContentResolver(), next); |
| mProgramLoadTask.executeOnDbThread(); |
| } |
| } |
| |
| void addDummyProgramsAt(long timeMs) { |
| addDummyPrograms(timeMs, timeMs + PREFETCH_DURATION_FOR_NEXT); |
| } |
| |
| private boolean addDummyPrograms(Range<Long> period) { |
| return addDummyPrograms(period.getLower(), period.getUpper()); |
| } |
| |
| private boolean addDummyPrograms(long startTimeMs, long endTimeMs) { |
| boolean added = false; |
| if (mPrograms.isEmpty()) { |
| // Insert dummy program. |
| mPrograms.addAll(createDummyPrograms(startTimeMs, endTimeMs)); |
| return true; |
| } |
| // Insert dummy program to the head of the list if needed. |
| Program firstProgram = mPrograms.get(0); |
| if (startTimeMs < firstProgram.getStartTimeUtcMillis()) { |
| if (!firstProgram.isValid()) { |
| // Already the firstProgram is dummy. |
| mPrograms.remove(0); |
| mPrograms.addAll(0, |
| createDummyPrograms(startTimeMs, firstProgram.getEndTimeUtcMillis())); |
| } else { |
| mPrograms.addAll(0, |
| createDummyPrograms(startTimeMs, firstProgram.getStartTimeUtcMillis())); |
| } |
| added = true; |
| } |
| // Insert dummy program to the tail of the list if needed. |
| Program lastProgram = mPrograms.get(mPrograms.size() - 1); |
| if (endTimeMs > lastProgram.getEndTimeUtcMillis()) { |
| if (!lastProgram.isValid()) { |
| // Already the lastProgram is dummy. |
| mPrograms.remove(mPrograms.size() - 1); |
| mPrograms.addAll( |
| createDummyPrograms(lastProgram.getStartTimeUtcMillis(), endTimeMs)); |
| } else { |
| mPrograms.addAll( |
| createDummyPrograms(lastProgram.getEndTimeUtcMillis(), endTimeMs)); |
| } |
| added = true; |
| } |
| // Insert dummy programs if the holes exist in the list. |
| for (int i = 1; i < mPrograms.size(); ++i) { |
| long endOfPrevious = mPrograms.get(i - 1).getEndTimeUtcMillis(); |
| long startOfCurrent = mPrograms.get(i).getStartTimeUtcMillis(); |
| if (startOfCurrent > endOfPrevious) { |
| List<Program> dummyPrograms = |
| createDummyPrograms(endOfPrevious, startOfCurrent); |
| mPrograms.addAll(i, dummyPrograms); |
| i += dummyPrograms.size(); |
| added = true; |
| } |
| } |
| return added; |
| } |
| |
| private void removeDummyPrograms() { |
| for (int i = 0; i < mPrograms.size(); ++i) { |
| Program program = mPrograms.get(i); |
| if (!program.isValid()) { |
| mPrograms.remove(i--); |
| } |
| } |
| } |
| |
| private void removeOverlappedPrograms(List<Program> loadedPrograms) { |
| if (mPrograms.size() == 0) { |
| return; |
| } |
| Program program = mPrograms.get(0); |
| for (int i = 0, j = 0; i < mPrograms.size() && j < loadedPrograms.size(); ++j) { |
| Program loadedProgram = loadedPrograms.get(j); |
| // Skip previous programs. |
| while (program.getEndTimeUtcMillis() < loadedProgram.getStartTimeUtcMillis()) { |
| // Reached end of mPrograms. |
| if (++i == mPrograms.size()) { |
| return; |
| } |
| program = mPrograms.get(i); |
| } |
| // Remove overlapped programs. |
| while (program.getStartTimeUtcMillis() < loadedProgram.getEndTimeUtcMillis() |
| && program.getEndTimeUtcMillis() > loadedProgram.getStartTimeUtcMillis()) { |
| mPrograms.remove(i); |
| if (i >= mPrograms.size()) { |
| break; |
| } |
| program = mPrograms.get(i); |
| } |
| } |
| } |
| |
| // Returns a list of dummy programs. |
| // The maximum duration of a dummy program is {@link MAX_DUMMY_PROGRAM_DURATION}. |
| // So if the duration ({@code endTimeMs}-{@code startTimeMs}) is greater than the duration, |
| // we need to create multiple dummy programs. |
| // The reason of the limitation of the duration is because we want the trick play viewer |
| // to show the time-line duration of {@link MAX_DUMMY_PROGRAM_DURATION} at most |
| // for a dummy program. |
| private List<Program> createDummyPrograms(long startTimeMs, long endTimeMs) { |
| if (startTimeMs >= endTimeMs) { |
| return Collections.emptyList(); |
| } |
| List<Program> programs = new ArrayList<>(); |
| long start = startTimeMs; |
| long end = Utils.ceilTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION); |
| while (end < endTimeMs) { |
| programs.add(new Program.Builder() |
| .setStartTimeUtcMillis(start) |
| .setEndTimeUtcMillis(end) |
| .build()); |
| start = end; |
| end += MAX_DUMMY_PROGRAM_DURATION; |
| } |
| programs.add(new Program.Builder() |
| .setStartTimeUtcMillis(start) |
| .setEndTimeUtcMillis(endTimeMs) |
| .build()); |
| return programs; |
| } |
| |
| Program getProgramAt(long timeMs) { |
| return getProgramAt(timeMs, 0, mPrograms.size() - 1); |
| } |
| |
| private Program getProgramAt(long timeMs, int start, int end) { |
| if (start > end) { |
| return null; |
| } |
| int mid = (start + end) / 2; |
| Program program = mPrograms.get(mid); |
| if (program.getStartTimeUtcMillis() > timeMs) { |
| return getProgramAt(timeMs, start, mid - 1); |
| } else if (program.getEndTimeUtcMillis() <= timeMs) { |
| return getProgramAt(timeMs, mid+1, end); |
| } else { |
| return program; |
| } |
| } |
| |
| private long getOldestProgramStartTime() { |
| if (mPrograms.isEmpty()) { |
| return INVALID_TIME; |
| } |
| return mPrograms.get(0).getStartTimeUtcMillis(); |
| } |
| |
| private Program getLastValidProgram() { |
| for (int i = mPrograms.size() - 1; i >= 0; --i) { |
| Program program = mPrograms.get(i); |
| if (program.isValid()) { |
| return program; |
| } |
| } |
| return null; |
| } |
| |
| private void schedulePrefetchPrograms() { |
| if (DEBUG) Log.d(TAG, "Scheduling prefetching programs."); |
| if (mHandler.hasMessages(MSG_PREFETCH_PROGRAM)) { |
| return; |
| } |
| Program lastValidProgram = getLastValidProgram(); |
| if (DEBUG) Log.d(TAG, "Last valid program = " + lastValidProgram); |
| final long delay; |
| if (lastValidProgram != null) { |
| delay = lastValidProgram.getEndTimeUtcMillis() |
| - PREFETCH_TIME_OFFSET_FROM_PROGRAM_END - System.currentTimeMillis(); |
| } else { |
| // Since there might not be any program data delay the retry 5 seconds, |
| // then 30 seconds then 5 minutes |
| switch (mEmptyFetchCount) { |
| case 0: |
| delay = 0; |
| break; |
| case 1: |
| delay = TimeUnit.SECONDS.toMillis(5); |
| break; |
| case 2: |
| delay = TimeUnit.SECONDS.toMillis(30); |
| break; |
| default: |
| delay = TimeUnit.MINUTES.toMillis(5); |
| break; |
| } |
| if (DEBUG) { |
| Log.d(TAG, |
| "No last valid program. Already tried " + mEmptyFetchCount + " times"); |
| } |
| } |
| mHandler.sendEmptyMessageDelayed(MSG_PREFETCH_PROGRAM, delay); |
| if (DEBUG) Log.d(TAG, "Scheduling with " + delay + "(ms) delays."); |
| } |
| |
| // Prefecth programs within PREFETCH_DURATION_FOR_NEXT from now. |
| private void prefetchPrograms() { |
| long startTimeMs; |
| Program lastValidProgram = getLastValidProgram(); |
| if (lastValidProgram == null) { |
| startTimeMs = System.currentTimeMillis(); |
| } else { |
| startTimeMs = lastValidProgram.getEndTimeUtcMillis(); |
| } |
| long endTimeMs = System.currentTimeMillis() + PREFETCH_DURATION_FOR_NEXT; |
| if (startTimeMs <= endTimeMs) { |
| if (DEBUG) { |
| Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs) |
| + ", endTime=" + Utils.toTimeString(endTimeMs) + "}"); |
| } |
| mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs)); |
| } |
| startTaskIfNeeded(); |
| } |
| |
| private class LoadProgramsForCurrentChannelTask |
| extends AsyncDbTask.LoadProgramsForChannelTask { |
| |
| public LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, |
| Range<Long> period) { |
| super(contentResolver, mChannel.getId(), period); |
| } |
| |
| @Override |
| protected void onPostExecute(List<Program> programs) { |
| if (DEBUG) { |
| Log.d(TAG, "Programs are loaded {channelId=" + mChannelId + |
| ", from=" + Utils.toTimeString(mPeriod.getLower()) + |
| ", to=" + Utils.toTimeString(mPeriod.getUpper()) + |
| "}"); |
| } |
| //remove pending tasks that are fully satisfied by this query. |
| Iterator<Range<Long>> it = mProgramLoadQueue.iterator(); |
| while (it.hasNext()) { |
| Range<Long> r = it.next(); |
| if (mPeriod.contains(r)) { |
| it.remove(); |
| } |
| } |
| if (programs == null || programs.isEmpty()) { |
| mEmptyFetchCount++; |
| if (addDummyPrograms(mPeriod)) { |
| TimeShiftManager.this.onProgramInfoChanged(); |
| } |
| schedulePrefetchPrograms(); |
| startNextLoadingIfNeeded(); |
| return; |
| } |
| mEmptyFetchCount = 0; |
| if(!mPrograms.isEmpty()) { |
| removeDummyPrograms(); |
| removeOverlappedPrograms(programs); |
| Program loadedProgram = programs.get(0); |
| for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) { |
| Program program = mPrograms.get(i); |
| while (program.getStartTimeUtcMillis() > loadedProgram |
| .getStartTimeUtcMillis()) { |
| mPrograms.add(i++, loadedProgram); |
| programs.remove(0); |
| if (programs.isEmpty()) { |
| break; |
| } |
| loadedProgram = programs.get(0); |
| } |
| } |
| } |
| mPrograms.addAll(programs); |
| addDummyPrograms(mPeriod); |
| TimeShiftManager.this.onProgramInfoChanged(); |
| schedulePrefetchPrograms(); |
| startNextLoadingIfNeeded(); |
| } |
| |
| @Override |
| protected void onCancelled(List<Program> programs) { |
| if (DEBUG) { |
| Log.d(TAG, "Program loading has been canceled {channelId=" + (mChannel == null |
| ? "null" : mChannelId) + ", from=" + Utils |
| .toTimeString(mPeriod.getLower()) + ", to=" + Utils |
| .toTimeString(mPeriod.getUpper()) + "}"); |
| } |
| startNextLoadingIfNeeded(); |
| } |
| |
| private void startNextLoadingIfNeeded() { |
| mProgramLoadTask = null; |
| // Need to post to handler, because the task is still running. |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| startTaskIfNeeded(); |
| } |
| }); |
| } |
| |
| public boolean overlaps(Queue<Range<Long>> programLoadQueue) { |
| for (Range<Long> r : programLoadQueue) { |
| if (mPeriod.contains(r.getLower()) || mPeriod.contains(r.getUpper())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| final class CurrentPositionMediator { |
| long mCurrentPositionMs; |
| long mSeekRequestTimeMs; |
| |
| void initialize(long timeMs) { |
| mSeekRequestTimeMs = INVALID_TIME; |
| mCurrentPositionMs = timeMs; |
| TimeShiftManager.this.onCurrentPositionChanged(); |
| } |
| |
| void onSeekRequested(long seekTimeMs) { |
| mSeekRequestTimeMs = System.currentTimeMillis(); |
| mCurrentPositionMs = seekTimeMs; |
| TimeShiftManager.this.onCurrentPositionChanged(); |
| } |
| |
| void onCurrentPositionChanged(long currentPositionMs) { |
| if (mSeekRequestTimeMs == INVALID_TIME) { |
| mCurrentPositionMs = currentPositionMs; |
| TimeShiftManager.this.onCurrentPositionChanged(); |
| return; |
| } |
| long currentTimeMs = System.currentTimeMillis(); |
| boolean isValid = Math.abs(currentPositionMs - mCurrentPositionMs) < REQUEST_TIMEOUT_MS; |
| boolean isTimeout = currentTimeMs > mSeekRequestTimeMs + REQUEST_TIMEOUT_MS; |
| if (isValid || isTimeout) { |
| initialize(currentPositionMs); |
| } else { |
| if (getPlayStatus() == PLAY_STATUS_PLAYING) { |
| if (getPlayDirection() == PLAY_DIRECTION_FORWARD) { |
| mCurrentPositionMs += (currentTimeMs - mSeekRequestTimeMs) |
| * getPlaybackSpeed(); |
| } else { |
| mCurrentPositionMs -= (currentTimeMs - mSeekRequestTimeMs) |
| * getPlaybackSpeed(); |
| } |
| } |
| TimeShiftManager.this.onCurrentPositionChanged(); |
| } |
| } |
| } |
| |
| /** |
| * The listener used to receive the events by the time-shift manager |
| */ |
| public interface Listener { |
| /** |
| * Called when the availability of the time-shift for the current channel has been changed. |
| * If the time shift is available, {@link TimeShiftManager#getRecordStartTimeMs} should |
| * return the valid time. |
| */ |
| void onAvailabilityChanged(); |
| |
| /** |
| * Called when the play status is changed between {@link #PLAY_STATUS_PLAYING} and |
| * {@link #PLAY_STATUS_PAUSED} |
| * |
| * @param status The new play state. |
| */ |
| void onPlayStatusChanged(int status); |
| |
| /** |
| * Called when the recordStartTime has been changed. |
| */ |
| void onRecordTimeRangeChanged(); |
| |
| /** |
| * Called when the current position is changed. |
| */ |
| void onCurrentPositionChanged(); |
| |
| /** |
| * Called when the program information is updated. |
| */ |
| void onProgramInfoChanged(); |
| |
| /** |
| * Called when an action becomes enabled or disabled. |
| */ |
| void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled); |
| } |
| |
| private static class TimeShiftHandler extends WeakHandler<TimeShiftManager> { |
| public TimeShiftHandler(TimeShiftManager ref) { |
| super(ref); |
| } |
| |
| @Override |
| public void handleMessage(Message msg, @NonNull TimeShiftManager timeShiftManager) { |
| switch (msg.what) { |
| case MSG_GET_CURRENT_POSITION: |
| timeShiftManager.mPlayController.handleGetCurrentPosition(); |
| break; |
| case MSG_PREFETCH_PROGRAM: |
| timeShiftManager.mProgramManager.prefetchPrograms(); |
| break; |
| } |
| } |
| } |
| } |