Home | History | Annotate | Download | only in tv
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.os.Handler;
     23 import android.os.Message;
     24 import android.support.annotation.IntDef;
     25 import android.support.annotation.NonNull;
     26 import android.support.annotation.Nullable;
     27 import android.support.annotation.VisibleForTesting;
     28 import android.util.Log;
     29 import android.util.Range;
     30 
     31 import com.android.tv.analytics.Tracker;
     32 import com.android.tv.common.SoftPreconditions;
     33 import com.android.tv.common.WeakHandler;
     34 import com.android.tv.data.Channel;
     35 import com.android.tv.data.OnCurrentProgramUpdatedListener;
     36 import com.android.tv.data.Program;
     37 import com.android.tv.data.ProgramDataManager;
     38 import com.android.tv.ui.TunableTvView;
     39 import com.android.tv.ui.TunableTvView.TimeShiftListener;
     40 import com.android.tv.util.AsyncDbTask;
     41 import com.android.tv.util.TimeShiftUtils;
     42 import com.android.tv.util.Utils;
     43 
     44 import java.lang.annotation.Retention;
     45 import java.lang.annotation.RetentionPolicy;
     46 import java.util.ArrayList;
     47 import java.util.Collections;
     48 import java.util.Iterator;
     49 import java.util.LinkedList;
     50 import java.util.List;
     51 import java.util.Objects;
     52 import java.util.Queue;
     53 import java.util.concurrent.TimeUnit;
     54 
     55 /**
     56  * A class which manages the time shift feature in Live TV. It consists of two parts.
     57  * {@link PlayController} controls the playback such as play/pause, rewind and fast-forward using
     58  * {@link TunableTvView} which communicates with TvInputService through
     59  * {@link android.media.tv.TvInputService.Session}.
     60  * {@link ProgramManager} loads programs of the current channel in the background.
     61  */
     62 public class TimeShiftManager {
     63     private static final String TAG = "TimeShiftManager";
     64     private static final boolean DEBUG = false;
     65 
     66     @Retention(RetentionPolicy.SOURCE)
     67     @IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING})
     68     public @interface PlayStatus {}
     69     public static final int PLAY_STATUS_PAUSED  = 0;
     70     public static final int PLAY_STATUS_PLAYING = 1;
     71 
     72     @Retention(RetentionPolicy.SOURCE)
     73     @IntDef({PLAY_SPEED_1X, PLAY_SPEED_2X, PLAY_SPEED_3X, PLAY_SPEED_4X, PLAY_SPEED_5X})
     74     public @interface PlaySpeed{}
     75     public static final int PLAY_SPEED_1X = 1;
     76     public static final int PLAY_SPEED_2X = 2;
     77     public static final int PLAY_SPEED_3X = 3;
     78     public static final int PLAY_SPEED_4X = 4;
     79     public static final int PLAY_SPEED_5X = 5;
     80 
     81     @Retention(RetentionPolicy.SOURCE)
     82     @IntDef({PLAY_DIRECTION_FORWARD, PLAY_DIRECTION_BACKWARD})
     83     public @interface PlayDirection{}
     84     public static final int PLAY_DIRECTION_FORWARD  = 0;
     85     public static final int PLAY_DIRECTION_BACKWARD = 1;
     86 
     87     @Retention(RetentionPolicy.SOURCE)
     88     @IntDef(flag = true, value = {TIME_SHIFT_ACTION_ID_PLAY, TIME_SHIFT_ACTION_ID_PAUSE,
     89             TIME_SHIFT_ACTION_ID_REWIND, TIME_SHIFT_ACTION_ID_FAST_FORWARD,
     90             TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT})
     91     public @interface TimeShiftActionId{}
     92     public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
     93     public static final int TIME_SHIFT_ACTION_ID_PAUSE = 1 << 1;
     94     public static final int TIME_SHIFT_ACTION_ID_REWIND = 1 << 2;
     95     public static final int TIME_SHIFT_ACTION_ID_FAST_FORWARD = 1 << 3;
     96     public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS = 1 << 4;
     97     public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT = 1 << 5;
     98 
     99     private static final int MSG_GET_CURRENT_POSITION = 1000;
    100     private static final int MSG_PREFETCH_PROGRAM = 1001;
    101     private static final long REQUEST_CURRENT_POSITION_INTERVAL = TimeUnit.SECONDS.toMillis(1);
    102     private static final long MAX_DUMMY_PROGRAM_DURATION = TimeUnit.MINUTES.toMillis(30);
    103     @VisibleForTesting
    104     static final long INVALID_TIME = -1;
    105     static final long CURRENT_TIME = -2;
    106     private static final long PREFETCH_TIME_OFFSET_FROM_PROGRAM_END = TimeUnit.MINUTES.toMillis(1);
    107     private static final long PREFETCH_DURATION_FOR_NEXT = TimeUnit.HOURS.toMillis(2);
    108 
    109     private static final long ALLOWED_START_TIME_OFFSET = TimeUnit.DAYS.toMillis(14);
    110     private static final long TWO_WEEKS_MS = TimeUnit.DAYS.toMillis(14);
    111 
    112     @VisibleForTesting
    113     static final long REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3);
    114 
    115     /**
    116      * If the user presses the {@link android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS} button within
    117      * this threshold from the program start time, the play position moves to the start of the
    118      * previous program.
    119      * Otherwise, the play position moves to the start of the current program.
    120      * This value is specified in the UX document.
    121      */
    122     private static final long PROGRAM_START_TIME_THRESHOLD = TimeUnit.SECONDS.toMillis(3);
    123     /**
    124      * If the current position enters within this range from the recording start time, rewind action
    125      * and jump to previous action is disabled.
    126      * Similarly, if the current position enters within this range from the current system time,
    127      * fast forward action and jump to next action is disabled.
    128      * It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at least.
    129      */
    130     private static final long DISABLE_ACTION_THRESHOLD = 3 * REQUEST_CURRENT_POSITION_INTERVAL;
    131     /**
    132      * If the current position goes out of this range from the recording start time, rewind action
    133      * and jump to previous action is enabled.
    134      * Similarly, if the current position goes out of this range from the current system time,
    135      * fast forward action and jump to next action is enabled.
    136      * Enable threshold and disable threshold must be different because the current position
    137      * does not have the continuous value. It changes every one second.
    138      */
    139     private static final long ENABLE_ACTION_THRESHOLD =
    140             DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL;
    141     /**
    142      * The current position sent from TIS can not be exactly the same as the current system time
    143      * due to the elapsed time to pass the message from TIS to Live TV.
    144      * So the boundary threshold is necessary.
    145      * The same goes for the recording start time.
    146      * It's the same {@link #REQUEST_CURRENT_POSITION_INTERVAL}.
    147      */
    148     private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL;
    149 
    150     private final PlayController mPlayController;
    151     private final ProgramManager mProgramManager;
    152     private final Tracker mTracker;
    153     @VisibleForTesting
    154     final CurrentPositionMediator mCurrentPositionMediator = new CurrentPositionMediator();
    155 
    156     private Listener mListener;
    157     private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener;
    158     private int mEnabledActionIds = TIME_SHIFT_ACTION_ID_PLAY | TIME_SHIFT_ACTION_ID_PAUSE
    159             | TIME_SHIFT_ACTION_ID_REWIND | TIME_SHIFT_ACTION_ID_FAST_FORWARD
    160             | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT;
    161     @TimeShiftActionId
    162     private int mLastActionId = 0;
    163 
    164     // TODO: Remove these variables once API level 23 is available.
    165     private final Context mContext;
    166 
    167     private Program mCurrentProgram;
    168     // This variable is used to block notification while changing the availability status.
    169     private boolean mNotificationEnabled;
    170 
    171     private final Handler mHandler = new TimeShiftHandler(this);
    172 
    173     public TimeShiftManager(Context context, TunableTvView tvView,
    174             ProgramDataManager programDataManager, Tracker tracker,
    175             OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener) {
    176         mContext = context;
    177         mPlayController = new PlayController(tvView);
    178         mProgramManager = new ProgramManager(programDataManager);
    179         mTracker = tracker;
    180         mOnCurrentProgramUpdatedListener = onCurrentProgramUpdatedListener;
    181     }
    182 
    183     /**
    184      * Sets a listener which will receive events from this class.
    185      */
    186     public void setListener(Listener listener) {
    187         mListener = listener;
    188     }
    189 
    190     /**
    191      * Checks if the trick play is available for the current channel.
    192      */
    193     public boolean isAvailable() {
    194         return mPlayController.mAvailable;
    195     }
    196 
    197     /**
    198      * Returns the current time position in milliseconds.
    199      */
    200     public long getCurrentPositionMs() {
    201         return mCurrentPositionMediator.mCurrentPositionMs;
    202     }
    203 
    204     void setCurrentPositionMs(long currentTimeMs) {
    205         mCurrentPositionMediator.onCurrentPositionChanged(currentTimeMs);
    206     }
    207 
    208     /**
    209      * Returns the start time of the recording in milliseconds.
    210      */
    211     public long getRecordStartTimeMs() {
    212         long oldestProgramStartTime = mProgramManager.getOldestProgramStartTime();
    213         return oldestProgramStartTime == INVALID_TIME ? INVALID_TIME
    214                 : mPlayController.mRecordStartTimeMs;
    215     }
    216 
    217     /**
    218      * Returns the end time of the recording in milliseconds.
    219      */
    220     public long getRecordEndTimeMs() {
    221         if (mPlayController.mRecordEndTimeMs == CURRENT_TIME) {
    222             return System.currentTimeMillis();
    223         } else {
    224             return mPlayController.mRecordEndTimeMs;
    225         }
    226     }
    227 
    228     /**
    229      * Plays the media.
    230      *
    231      * @throws IllegalStateException if the trick play is not available.
    232      */
    233     public void play() {
    234         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PLAY)) {
    235             return;
    236         }
    237         mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY);
    238         mLastActionId = TIME_SHIFT_ACTION_ID_PLAY;
    239         mPlayController.play();
    240         updateActions();
    241     }
    242 
    243     /**
    244      * Pauses the playback.
    245      *
    246      * @throws IllegalStateException if the trick play is not available.
    247      */
    248     public void pause() {
    249         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PAUSE)) {
    250             return;
    251         }
    252         mLastActionId = TIME_SHIFT_ACTION_ID_PAUSE;
    253         mTracker.sendTimeShiftAction(mLastActionId);
    254         mPlayController.pause();
    255         updateActions();
    256     }
    257 
    258     /**
    259      * Toggles the playing and paused state.
    260      *
    261      * @throws IllegalStateException if the trick play is not available.
    262      */
    263     public void togglePlayPause() {
    264         mPlayController.togglePlayPause();
    265     }
    266 
    267     /**
    268      * Plays the media in backward direction. The playback speed is increased by 1x each time
    269      * this is called. The range of the speed is from 2x to 5x.
    270      * If the playing position is considered the same as the record start time, it does nothing
    271      *
    272      * @throws IllegalStateException if the trick play is not available.
    273      */
    274     public void rewind() {
    275         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND)) {
    276             return;
    277         }
    278         mLastActionId = TIME_SHIFT_ACTION_ID_REWIND;
    279         mTracker.sendTimeShiftAction(mLastActionId);
    280         mPlayController.rewind();
    281         updateActions();
    282     }
    283 
    284     /**
    285      * Plays the media in forward direction. The playback speed is increased by 1x each time
    286      * this is called. The range of the speed is from 2x to 5x.
    287      * If the playing position is the same as the current time, it does nothing.
    288      *
    289      * @throws IllegalStateException if the trick play is not available.
    290      */
    291     public void fastForward() {
    292         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)) {
    293             return;
    294         }
    295         mLastActionId = TIME_SHIFT_ACTION_ID_FAST_FORWARD;
    296         mTracker.sendTimeShiftAction(mLastActionId);
    297         mPlayController.fastForward();
    298         updateActions();
    299     }
    300 
    301     /**
    302      * Jumps to the start of the current program.
    303      * If the currently playing position is within 3 seconds
    304      * (={@link #PROGRAM_START_TIME_THRESHOLD})from the start time of the program, it goes to
    305      * the start of the previous program if exists.
    306      * If the playing position is the same as the record start time, it does nothing.
    307      *
    308      * @throws IllegalStateException if the trick play is not available.
    309      */
    310     public void jumpToPrevious() {
    311         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) {
    312             return;
    313         }
    314         Program program = mProgramManager.getProgramAt(
    315                 mCurrentPositionMediator.mCurrentPositionMs - PROGRAM_START_TIME_THRESHOLD);
    316         if (program == null) {
    317             return;
    318         }
    319         long seekPosition =
    320                 Math.max(program.getStartTimeUtcMillis(), mPlayController.mRecordStartTimeMs);
    321         mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS;
    322         mTracker.sendTimeShiftAction(mLastActionId);
    323         mPlayController.seekTo(seekPosition);
    324         mCurrentPositionMediator.onSeekRequested(seekPosition);
    325         updateActions();
    326     }
    327 
    328     /**
    329      * Jumps to the start of the next program if exists.
    330      * If there's no next program, it jumps to the current system time and shows the live TV.
    331      * If the playing position is considered the same as the current time, it does nothing.
    332      *
    333      * @throws IllegalStateException if the trick play is not available.
    334      */
    335     public void jumpToNext() {
    336         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) {
    337             return;
    338         }
    339         Program currentProgram = mProgramManager.getProgramAt(
    340                 mCurrentPositionMediator.mCurrentPositionMs);
    341         if (currentProgram == null) {
    342             return;
    343         }
    344         Program nextProgram = mProgramManager.getProgramAt(currentProgram.getEndTimeUtcMillis());
    345         long currentTimeMs = System.currentTimeMillis();
    346         mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT;
    347         mTracker.sendTimeShiftAction(mLastActionId);
    348         if (nextProgram == null || nextProgram.getStartTimeUtcMillis() > currentTimeMs) {
    349             mPlayController.seekTo(currentTimeMs);
    350             if (mPlayController.isForwarding()) {
    351                 // The current position will be the current system time from now.
    352                 mPlayController.mIsPlayOffsetChanged = false;
    353                 mCurrentPositionMediator.initialize(currentTimeMs);
    354             } else {
    355                 // The current position would not be the current system time.
    356                 // So need to wait for the correct time from TIS.
    357                 mCurrentPositionMediator.onSeekRequested(currentTimeMs);
    358             }
    359         } else {
    360             mPlayController.seekTo(nextProgram.getStartTimeUtcMillis());
    361             mCurrentPositionMediator.onSeekRequested(nextProgram.getStartTimeUtcMillis());
    362         }
    363         updateActions();
    364     }
    365 
    366     /**
    367      * Returns the playback status. The value is PLAY_STATUS_PAUSED or PLAY_STATUS_PLAYING.
    368      */
    369     @PlayStatus public int getPlayStatus() {
    370         return mPlayController.mPlayStatus;
    371     }
    372 
    373     /**
    374      * Returns the displayed playback speed. The value is one of PLAY_SPEED_1X, PLAY_SPEED_2X,
    375      * PLAY_SPEED_3X, PLAY_SPEED_4X and PLAY_SPEED_5X.
    376      */
    377     @PlaySpeed public int getDisplayedPlaySpeed() {
    378         return mPlayController.mDisplayedPlaySpeed;
    379     }
    380 
    381     /**
    382      * Returns the playback speed. The value is PLAY_DIRECTION_FORWARD or PLAY_DIRECTION_BACKWARD.
    383      */
    384     @PlayDirection public int getPlayDirection() {
    385         return mPlayController.mPlayDirection;
    386     }
    387 
    388     /**
    389      * Returns the ID of the last action..
    390      */
    391     @TimeShiftActionId public int getLastActionId() {
    392         return mLastActionId;
    393     }
    394 
    395     /**
    396      * Enables or disables the time-shift actions.
    397      */
    398     @VisibleForTesting
    399     void enableAction(@TimeShiftActionId int actionId, boolean enable) {
    400         int oldEnabledActionIds = mEnabledActionIds;
    401         if (enable) {
    402             mEnabledActionIds |= actionId;
    403         } else {
    404             mEnabledActionIds &= ~actionId;
    405         }
    406         if (mNotificationEnabled && mListener != null
    407                 && oldEnabledActionIds != mEnabledActionIds) {
    408             mListener.onActionEnabledChanged(actionId, enable);
    409         }
    410     }
    411 
    412     public boolean isActionEnabled(@TimeShiftActionId int actionId) {
    413         return (mEnabledActionIds & actionId) == actionId;
    414     }
    415 
    416     private void updateActions() {
    417         if (isAvailable()) {
    418             enableAction(TIME_SHIFT_ACTION_ID_PLAY, true);
    419             enableAction(TIME_SHIFT_ACTION_ID_PAUSE, true);
    420             // Rewind action and jump to previous action.
    421             long threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND)
    422                     ? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD;
    423             boolean enabled = mCurrentPositionMediator.mCurrentPositionMs
    424                     - mPlayController.mRecordStartTimeMs > threshold;
    425             enableAction(TIME_SHIFT_ACTION_ID_REWIND, enabled);
    426             enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, enabled);
    427             // Fast forward action and jump to next action
    428             threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)
    429                     ? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD;
    430             enabled = getRecordEndTimeMs() - mCurrentPositionMediator.mCurrentPositionMs
    431                     > threshold;
    432             enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled);
    433             enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled);
    434         } else {
    435             enableAction(TIME_SHIFT_ACTION_ID_PLAY, false);
    436             enableAction(TIME_SHIFT_ACTION_ID_PAUSE, false);
    437             enableAction(TIME_SHIFT_ACTION_ID_REWIND, false);
    438             enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, false);
    439             enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, false);
    440             enableAction(TIME_SHIFT_ACTION_ID_PLAY, false);
    441         }
    442     }
    443 
    444     private void updateCurrentProgram() {
    445         SoftPreconditions.checkState(isAvailable(), TAG, "Time shift is not available");
    446         SoftPreconditions.checkState(mCurrentPositionMediator.mCurrentPositionMs != INVALID_TIME);
    447         Program currentProgram = getProgramAt(mCurrentPositionMediator.mCurrentPositionMs);
    448         if (!Program.isValid(currentProgram)) {
    449             currentProgram = null;
    450         }
    451         if (!Objects.equals(mCurrentProgram, currentProgram)) {
    452             if (DEBUG) Log.d(TAG, "Current program has been updated. " + currentProgram);
    453             mCurrentProgram = currentProgram;
    454             if (mNotificationEnabled && mOnCurrentProgramUpdatedListener != null) {
    455                 Channel channel = mPlayController.getCurrentChannel();
    456                 if (channel != null) {
    457                     mOnCurrentProgramUpdatedListener.onCurrentProgramUpdated(channel.getId(),
    458                             mCurrentProgram);
    459                     mPlayController.onCurrentProgramChanged();
    460                 }
    461             }
    462         }
    463     }
    464 
    465     /**
    466      * Returns {@code true} if the trick play is available and it's playing to the forward direction
    467      * with normal speed, otherwise {@code false}.
    468      */
    469     public boolean isNormalPlaying() {
    470         return mPlayController.mAvailable
    471                 && mPlayController.mPlayStatus == PLAY_STATUS_PLAYING
    472                 && mPlayController.mPlayDirection == PLAY_DIRECTION_FORWARD
    473                 && mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X;
    474     }
    475 
    476     /**
    477      * Checks if the trick play is available and it's playback status is paused.
    478      */
    479     public boolean isPaused() {
    480         return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED;
    481     }
    482 
    483     /**
    484      * Returns the program which airs at the given time.
    485      */
    486     @NonNull
    487     public Program getProgramAt(long timeMs) {
    488         Program program = mProgramManager.getProgramAt(timeMs);
    489         if (program == null) {
    490             // Guard just in case when the program prefetch handler doesn't work on time.
    491             mProgramManager.addDummyProgramsAt(timeMs);
    492             program = mProgramManager.getProgramAt(timeMs);
    493         }
    494         return program;
    495     }
    496 
    497     void onAvailabilityChanged() {
    498         mCurrentPositionMediator.initialize(mPlayController.mRecordStartTimeMs);
    499         mProgramManager.onAvailabilityChanged(mPlayController.mAvailable,
    500                 mPlayController.getCurrentChannel(), mPlayController.mRecordStartTimeMs);
    501         updateActions();
    502         // Availability change notification should be always sent
    503         // even if mNotificationEnabled is false.
    504         if (mListener != null) {
    505             mListener.onAvailabilityChanged();
    506         }
    507     }
    508 
    509     void onRecordTimeRangeChanged() {
    510         if (mPlayController.mAvailable) {
    511             mProgramManager.onRecordTimeRangeChanged(mPlayController.mRecordStartTimeMs,
    512                     mPlayController.mRecordEndTimeMs);
    513         }
    514         updateActions();
    515         if (mNotificationEnabled && mListener != null) {
    516             mListener.onRecordTimeRangeChanged();
    517         }
    518     }
    519 
    520     void onCurrentPositionChanged() {
    521         updateActions();
    522         updateCurrentProgram();
    523         if (mNotificationEnabled && mListener != null) {
    524             mListener.onCurrentPositionChanged();
    525         }
    526     }
    527 
    528     void onPlayStatusChanged(@PlayStatus int status) {
    529         if (mNotificationEnabled && mListener != null) {
    530             mListener.onPlayStatusChanged(status);
    531         }
    532     }
    533 
    534     void onProgramInfoChanged() {
    535         updateCurrentProgram();
    536         if (mNotificationEnabled && mListener != null) {
    537             mListener.onProgramInfoChanged();
    538         }
    539     }
    540 
    541     /**
    542      * Returns the current program which airs right now.<p>
    543      *
    544      * If the program is a dummy program, which means there's no program information,
    545      * returns {@code null}.
    546      */
    547     @Nullable
    548     public Program getCurrentProgram() {
    549         if (isAvailable()) {
    550             return mCurrentProgram;
    551         }
    552         return null;
    553     }
    554 
    555     private int getPlaybackSpeed() {
    556         if (mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X) {
    557             return 1;
    558         } else {
    559             long durationMs =
    560                     (getCurrentProgram() == null ? 0 : getCurrentProgram().getDurationMillis());
    561             if (mPlayController.mDisplayedPlaySpeed > PLAY_SPEED_5X) {
    562                 Log.w(TAG, "Unknown displayed play speed is chosen : "
    563                         + mPlayController.mDisplayedPlaySpeed);
    564                 return TimeShiftUtils.getMaxPlaybackSpeed(durationMs);
    565             } else {
    566                 return TimeShiftUtils.getPlaybackSpeed(
    567                         mPlayController.mDisplayedPlaySpeed - PLAY_SPEED_2X, durationMs);
    568             }
    569         }
    570     }
    571 
    572     /**
    573      * A class which controls the trick play.
    574      */
    575     private class PlayController {
    576         private final TunableTvView mTvView;
    577 
    578         private long mAvailablityChangedTimeMs;
    579         private long mRecordStartTimeMs;
    580         private long mRecordEndTimeMs;
    581 
    582         @PlayStatus private int mPlayStatus = PLAY_STATUS_PAUSED;
    583         @PlaySpeed private int mDisplayedPlaySpeed = PLAY_SPEED_1X;
    584         @PlayDirection private int mPlayDirection = PLAY_DIRECTION_FORWARD;
    585         private int mPlaybackSpeed;
    586         private boolean mAvailable;
    587 
    588         /**
    589          * Indicates that the trick play is not playing the current time position.
    590          * It is set true when {@link PlayController#pause}, {@link PlayController#rewind},
    591          * {@link PlayController#fastForward} and {@link PlayController#seekTo}
    592          * is called.
    593          * If it is true, the current time is equal to System.currentTimeMillis().
    594          */
    595         private boolean mIsPlayOffsetChanged;
    596 
    597         PlayController(TunableTvView tvView) {
    598             mTvView = tvView;
    599             mTvView.setTimeShiftListener(new TimeShiftListener() {
    600                 @Override
    601                 public void onAvailabilityChanged() {
    602                     if (DEBUG) {
    603                         Log.d(TAG, "onAvailabilityChanged(available="
    604                                 + mTvView.isTimeShiftAvailable() + ")");
    605                     }
    606                     PlayController.this.onAvailabilityChanged();
    607                 }
    608 
    609                 @Override
    610                 public void onRecordStartTimeChanged(long recordStartTimeMs) {
    611                     if (!SoftPreconditions.checkState(mAvailable, TAG,
    612                             "Trick play is not available.")) {
    613                         return;
    614                     }
    615                     if (recordStartTimeMs < mAvailablityChangedTimeMs - ALLOWED_START_TIME_OFFSET) {
    616                         Log.e(TAG, "The start time is too earlier than the time of availability: {"
    617                                 + "startTime: " + recordStartTimeMs + ", availability: "
    618                                 + mAvailablityChangedTimeMs);
    619                         return;
    620                     }
    621                     if (mRecordStartTimeMs == recordStartTimeMs) {
    622                         return;
    623                     }
    624                     mRecordStartTimeMs = recordStartTimeMs;
    625                     TimeShiftManager.this.onRecordTimeRangeChanged();
    626 
    627                     // According to the UX guidelines, the stream should be resumed if the
    628                     // recording buffer fills up while paused, which means that the current time
    629                     // position is the same as or before the recording start time.
    630                     // But, for this application and the TIS, it's an erroneous and confusing
    631                     // situation if the current time position is before the recording start time.
    632                     // So, we recommend the TIS to keep the current time position greater than or
    633                     // equal to the recording start time.
    634                     // And here, we assume that the buffer is full if the current time position
    635                     // is nearly equal to the recording start time.
    636                     if (mPlayStatus == PLAY_STATUS_PAUSED &&
    637                             getCurrentPositionMs() - mRecordStartTimeMs
    638                             < RECORDING_BOUNDARY_THRESHOLD) {
    639                         TimeShiftManager.this.play();
    640                     }
    641                 }
    642             });
    643         }
    644 
    645         void onAvailabilityChanged() {
    646             boolean newAvailable = mTvView.isTimeShiftAvailable();
    647             if (mAvailable == newAvailable) {
    648                 return;
    649             }
    650             mAvailable = newAvailable;
    651             // Do not send the notifications while the availability is changing,
    652             // because the variables are in the intermediate state.
    653             // For example, the current program can be null.
    654             mNotificationEnabled = false;
    655             mDisplayedPlaySpeed = PLAY_SPEED_1X;
    656             mPlaybackSpeed = 1;
    657             mPlayDirection = PLAY_DIRECTION_FORWARD;
    658             mHandler.removeMessages(MSG_GET_CURRENT_POSITION);
    659 
    660             if (mAvailable) {
    661                 mAvailablityChangedTimeMs = System.currentTimeMillis();
    662                 mIsPlayOffsetChanged = false;
    663                 mRecordStartTimeMs = mAvailablityChangedTimeMs;
    664                 mRecordEndTimeMs = CURRENT_TIME;
    665                 // When the media availability message has come.
    666                 mPlayController.setPlayStatus(PLAY_STATUS_PLAYING);
    667                 mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION,
    668                         REQUEST_CURRENT_POSITION_INTERVAL);
    669             } else {
    670                 mAvailablityChangedTimeMs = INVALID_TIME;
    671                 mIsPlayOffsetChanged = false;
    672                 mRecordStartTimeMs = INVALID_TIME;
    673                 mRecordEndTimeMs = INVALID_TIME;
    674                 // When the tune command is sent.
    675                 mPlayController.setPlayStatus(PLAY_STATUS_PAUSED);
    676             }
    677             TimeShiftManager.this.onAvailabilityChanged();
    678             mNotificationEnabled = true;
    679         }
    680 
    681         void handleGetCurrentPosition() {
    682             if (mIsPlayOffsetChanged) {
    683                 long currentTimeMs = mRecordEndTimeMs == CURRENT_TIME ? System.currentTimeMillis()
    684                         : mRecordEndTimeMs;
    685                 long currentPositionMs = Math.max(
    686                         Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs),
    687                         mRecordStartTimeMs);
    688                 boolean isCurrentTime =
    689                         currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD;
    690                 long newCurrentPositionMs;
    691                 if (isCurrentTime && isForwarding()) {
    692                     // It's playing forward and the current playing position reached
    693                     // the current system time. i.e. The live stream is played.
    694                     // Therefore no need to call TvView.timeshiftGetCurrentPositionMs
    695                     // any more.
    696                     newCurrentPositionMs = currentTimeMs;
    697                     mIsPlayOffsetChanged = false;
    698                     if (mDisplayedPlaySpeed > PLAY_SPEED_1X) {
    699                         TimeShiftManager.this.play();
    700                     }
    701                 } else {
    702                     newCurrentPositionMs = currentPositionMs;
    703                     boolean isRecordStartTime = currentPositionMs - mRecordStartTimeMs
    704                             < RECORDING_BOUNDARY_THRESHOLD;
    705                     if (isRecordStartTime && isRewinding()) {
    706                         TimeShiftManager.this.play();
    707                     }
    708                 }
    709                 setCurrentPositionMs(newCurrentPositionMs);
    710             } else {
    711                 setCurrentPositionMs(System.currentTimeMillis());
    712                 TimeShiftManager.this.onCurrentPositionChanged();
    713             }
    714             // Need to send message here just in case there is no or invalid response
    715             // for the current time position request from TIS.
    716             mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION,
    717                     REQUEST_CURRENT_POSITION_INTERVAL);
    718         }
    719 
    720         void play() {
    721             mDisplayedPlaySpeed = PLAY_SPEED_1X;
    722             mPlaybackSpeed = 1;
    723             mPlayDirection = PLAY_DIRECTION_FORWARD;
    724             mTvView.timeshiftPlay();
    725             setPlayStatus(PLAY_STATUS_PLAYING);
    726         }
    727 
    728         void pause() {
    729             mDisplayedPlaySpeed = PLAY_SPEED_1X;
    730             mPlaybackSpeed = 1;
    731             mTvView.timeshiftPause();
    732             setPlayStatus(PLAY_STATUS_PAUSED);
    733             mIsPlayOffsetChanged = true;
    734         }
    735 
    736         void togglePlayPause() {
    737             if (mPlayStatus == PLAY_STATUS_PAUSED) {
    738                 play();
    739                 mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY);
    740             } else {
    741                 pause();
    742                 mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PAUSE);
    743             }
    744         }
    745 
    746         void rewind() {
    747             if (mPlayDirection == PLAY_DIRECTION_BACKWARD) {
    748                 increaseDisplayedPlaySpeed();
    749             } else {
    750                 mDisplayedPlaySpeed = PLAY_SPEED_2X;
    751             }
    752             mPlayDirection = PLAY_DIRECTION_BACKWARD;
    753             mPlaybackSpeed = getPlaybackSpeed();
    754             mTvView.timeshiftRewind(mPlaybackSpeed);
    755             setPlayStatus(PLAY_STATUS_PLAYING);
    756             mIsPlayOffsetChanged = true;
    757         }
    758 
    759         void fastForward() {
    760             if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
    761                 increaseDisplayedPlaySpeed();
    762             } else {
    763                 mDisplayedPlaySpeed = PLAY_SPEED_2X;
    764             }
    765             mPlayDirection = PLAY_DIRECTION_FORWARD;
    766             mPlaybackSpeed = getPlaybackSpeed();
    767             mTvView.timeshiftFastForward(mPlaybackSpeed);
    768             setPlayStatus(PLAY_STATUS_PLAYING);
    769             mIsPlayOffsetChanged = true;
    770         }
    771 
    772         /**
    773          * Moves to the specified time.
    774          */
    775         void seekTo(long timeMs) {
    776             mTvView.timeshiftSeekTo(Math.min(mRecordEndTimeMs == CURRENT_TIME
    777                     ? System.currentTimeMillis() : mRecordEndTimeMs,
    778                             Math.max(mRecordStartTimeMs, timeMs)));
    779             mIsPlayOffsetChanged = true;
    780         }
    781 
    782         void onCurrentProgramChanged() {
    783             // Update playback speed
    784             if (mDisplayedPlaySpeed == PLAY_SPEED_1X) {
    785                 return;
    786             }
    787             int playbackSpeed = getPlaybackSpeed();
    788             if (playbackSpeed != mPlaybackSpeed) {
    789                 mPlaybackSpeed = playbackSpeed;
    790                 if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
    791                     mTvView.timeshiftFastForward(mPlaybackSpeed);
    792                 } else {
    793                     mTvView.timeshiftRewind(mPlaybackSpeed);
    794                 }
    795             }
    796         }
    797 
    798         @SuppressLint("SwitchIntDef")
    799         private void increaseDisplayedPlaySpeed() {
    800             switch (mDisplayedPlaySpeed) {
    801                 case PLAY_SPEED_1X:
    802                     mDisplayedPlaySpeed = PLAY_SPEED_2X;
    803                     break;
    804                 case PLAY_SPEED_2X:
    805                     mDisplayedPlaySpeed = PLAY_SPEED_3X;
    806                     break;
    807                 case PLAY_SPEED_3X:
    808                     mDisplayedPlaySpeed = PLAY_SPEED_4X;
    809                     break;
    810                 case PLAY_SPEED_4X:
    811                     mDisplayedPlaySpeed = PLAY_SPEED_5X;
    812                     break;
    813             }
    814         }
    815 
    816         private void setPlayStatus(@PlayStatus int status) {
    817             mPlayStatus = status;
    818             TimeShiftManager.this.onPlayStatusChanged(status);
    819         }
    820 
    821         boolean isForwarding() {
    822             return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_FORWARD;
    823         }
    824 
    825         private boolean isRewinding() {
    826             return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_BACKWARD;
    827         }
    828 
    829         Channel getCurrentChannel() {
    830             return mTvView.getCurrentChannel();
    831         }
    832     }
    833 
    834     private class ProgramManager {
    835         private final ProgramDataManager mProgramDataManager;
    836         private Channel mChannel;
    837         private final List<Program> mPrograms = new ArrayList<>();
    838         private final Queue<Range<Long>> mProgramLoadQueue = new LinkedList<>();
    839         private LoadProgramsForCurrentChannelTask mProgramLoadTask = null;
    840         private int mEmptyFetchCount = 0;
    841 
    842         ProgramManager(ProgramDataManager programDataManager) {
    843             mProgramDataManager = programDataManager;
    844         }
    845 
    846         void onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs) {
    847             if (DEBUG) {
    848                 Log.d(TAG, "onAvailabilityChanged(" + available + "+," + channel + ", "
    849                         + currentPositionMs + ")");
    850             }
    851 
    852             mProgramLoadQueue.clear();
    853             if (mProgramLoadTask != null) {
    854                 mProgramLoadTask.cancel(true);
    855             }
    856             mHandler.removeMessages(MSG_PREFETCH_PROGRAM);
    857             mPrograms.clear();
    858             mEmptyFetchCount = 0;
    859             mChannel = channel;
    860             if (channel == null || channel.isPassthrough() || currentPositionMs == INVALID_TIME) {
    861                 return;
    862             }
    863             if (available) {
    864                 Program program = mProgramDataManager.getCurrentProgram(channel.getId());
    865                 long prefetchStartTimeMs;
    866                 if (program != null) {
    867                     mPrograms.add(program);
    868                     prefetchStartTimeMs = program.getEndTimeUtcMillis();
    869                 } else {
    870                     prefetchStartTimeMs = Utils.floorTime(currentPositionMs,
    871                             MAX_DUMMY_PROGRAM_DURATION);
    872                 }
    873                 // Create dummy program
    874                 mPrograms.addAll(createDummyPrograms(prefetchStartTimeMs,
    875                         currentPositionMs + PREFETCH_DURATION_FOR_NEXT));
    876                 schedulePrefetchPrograms();
    877                 TimeShiftManager.this.onProgramInfoChanged();
    878             }
    879         }
    880 
    881         void onRecordTimeRangeChanged(long startTimeMs, long endTimeMs) {
    882             if (mChannel == null || mChannel.isPassthrough()) {
    883                 return;
    884             }
    885             if (endTimeMs == CURRENT_TIME) {
    886                 endTimeMs = System.currentTimeMillis();
    887             }
    888 
    889             long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION);
    890             boolean needToLoad = addDummyPrograms(fetchStartTimeMs,
    891                     endTimeMs + PREFETCH_DURATION_FOR_NEXT);
    892             if (needToLoad) {
    893                 Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs);
    894                 mProgramLoadQueue.add(period);
    895                 startTaskIfNeeded();
    896             }
    897         }
    898 
    899         private void startTaskIfNeeded() {
    900             if (mProgramLoadQueue.isEmpty()) {
    901                 return;
    902             }
    903             if (mProgramLoadTask == null || mProgramLoadTask.isCancelled()) {
    904                 startNext();
    905             } else {
    906                 // Remove pending task fully satisfied by the current
    907                 Range<Long> current = mProgramLoadTask.getPeriod();
    908                 Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
    909                 while (i.hasNext()) {
    910                     Range<Long> r = i.next();
    911                     if (current.contains(r)) {
    912                         i.remove();
    913                     }
    914                 }
    915             }
    916         }
    917 
    918         private void startNext() {
    919             mProgramLoadTask = null;
    920             if (mProgramLoadQueue.isEmpty()) {
    921                 return;
    922             }
    923 
    924             Range<Long> next = mProgramLoadQueue.poll();
    925             // Extend next to include any overlapping Ranges.
    926             Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
    927             while(i.hasNext()) {
    928                 Range<Long> r = i.next();
    929                 if(next.contains(r.getLower()) || next.contains(r.getUpper())){
    930                     i.remove();
    931                     next = next.extend(r);
    932                 }
    933             }
    934             if (mChannel != null) {
    935                 mProgramLoadTask = new LoadProgramsForCurrentChannelTask(
    936                         mContext.getContentResolver(), next);
    937                 mProgramLoadTask.executeOnDbThread();
    938             }
    939         }
    940 
    941         void addDummyProgramsAt(long timeMs) {
    942             addDummyPrograms(timeMs, timeMs + PREFETCH_DURATION_FOR_NEXT);
    943         }
    944 
    945         private boolean addDummyPrograms(Range<Long> period) {
    946             return addDummyPrograms(period.getLower(), period.getUpper());
    947         }
    948 
    949         private boolean addDummyPrograms(long startTimeMs, long endTimeMs) {
    950             boolean added = false;
    951             if (mPrograms.isEmpty()) {
    952                 // Insert dummy program.
    953                 mPrograms.addAll(createDummyPrograms(startTimeMs, endTimeMs));
    954                 return true;
    955             }
    956             // Insert dummy program to the head of the list if needed.
    957             Program firstProgram = mPrograms.get(0);
    958             if (startTimeMs < firstProgram.getStartTimeUtcMillis()) {
    959                 if (!firstProgram.isValid()) {
    960                     // Already the firstProgram is dummy.
    961                     mPrograms.remove(0);
    962                     mPrograms.addAll(0,
    963                             createDummyPrograms(startTimeMs, firstProgram.getEndTimeUtcMillis()));
    964                 } else {
    965                     mPrograms.addAll(0,
    966                             createDummyPrograms(startTimeMs, firstProgram.getStartTimeUtcMillis()));
    967                 }
    968                 added = true;
    969             }
    970             // Insert dummy program to the tail of the list if needed.
    971             Program lastProgram = mPrograms.get(mPrograms.size() - 1);
    972             if (endTimeMs > lastProgram.getEndTimeUtcMillis()) {
    973                 if (!lastProgram.isValid()) {
    974                     // Already the lastProgram is dummy.
    975                     mPrograms.remove(mPrograms.size() - 1);
    976                     mPrograms.addAll(
    977                             createDummyPrograms(lastProgram.getStartTimeUtcMillis(), endTimeMs));
    978                 } else {
    979                     mPrograms.addAll(
    980                             createDummyPrograms(lastProgram.getEndTimeUtcMillis(), endTimeMs));
    981                 }
    982                 added = true;
    983             }
    984             // Insert dummy programs if the holes exist in the list.
    985             for (int i = 1; i < mPrograms.size(); ++i) {
    986                 long endOfPrevious = mPrograms.get(i - 1).getEndTimeUtcMillis();
    987                 long startOfCurrent = mPrograms.get(i).getStartTimeUtcMillis();
    988                 if (startOfCurrent > endOfPrevious) {
    989                     List<Program> dummyPrograms =
    990                             createDummyPrograms(endOfPrevious, startOfCurrent);
    991                     mPrograms.addAll(i, dummyPrograms);
    992                     i += dummyPrograms.size();
    993                     added = true;
    994                 }
    995             }
    996             return added;
    997         }
    998 
    999         private void removeDummyPrograms() {
   1000             for (Iterator<Program> it = mPrograms.listIterator(); it.hasNext(); ) {
   1001                 if (!it.next().isValid()) {
   1002                     it.remove();
   1003                 }
   1004             }
   1005         }
   1006 
   1007         private void removeOverlappedPrograms(List<Program> loadedPrograms) {
   1008             if (mPrograms.size() == 0) {
   1009                 return;
   1010             }
   1011             Program program = mPrograms.get(0);
   1012             for (int i = 0, j = 0; i < mPrograms.size() && j < loadedPrograms.size(); ++j) {
   1013                 Program loadedProgram = loadedPrograms.get(j);
   1014                 // Skip previous programs.
   1015                 while (program.getEndTimeUtcMillis() < loadedProgram.getStartTimeUtcMillis()) {
   1016                     // Reached end of mPrograms.
   1017                     if (++i == mPrograms.size()) {
   1018                         return;
   1019                     }
   1020                     program = mPrograms.get(i);
   1021                 }
   1022                 // Remove overlapped programs.
   1023                 while (program.getStartTimeUtcMillis() < loadedProgram.getEndTimeUtcMillis()
   1024                         && program.getEndTimeUtcMillis() > loadedProgram.getStartTimeUtcMillis()) {
   1025                     mPrograms.remove(i);
   1026                     if (i >= mPrograms.size()) {
   1027                         break;
   1028                     }
   1029                     program = mPrograms.get(i);
   1030                 }
   1031             }
   1032         }
   1033 
   1034         // Returns a list of dummy programs.
   1035         // The maximum duration of a dummy program is {@link MAX_DUMMY_PROGRAM_DURATION}.
   1036         // So if the duration ({@code endTimeMs}-{@code startTimeMs}) is greater than the duration,
   1037         // we need to create multiple dummy programs.
   1038         // The reason of the limitation of the duration is because we want the trick play viewer
   1039         // to show the time-line duration of {@link MAX_DUMMY_PROGRAM_DURATION} at most
   1040         // for a dummy program.
   1041         private List<Program> createDummyPrograms(long startTimeMs, long endTimeMs) {
   1042             SoftPreconditions.checkArgument(endTimeMs - startTimeMs <= TWO_WEEKS_MS, TAG,
   1043                     "createDummyProgram: long duration of dummy programs are requested ("
   1044                             + Utils.toTimeString(startTimeMs) + ", "
   1045                             + Utils.toTimeString(endTimeMs));
   1046             if (startTimeMs >= endTimeMs) {
   1047                 return Collections.emptyList();
   1048             }
   1049             List<Program> programs = new ArrayList<>();
   1050             long start = startTimeMs;
   1051             long end = Utils.ceilTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION);
   1052             while (end < endTimeMs) {
   1053                 programs.add(new Program.Builder()
   1054                         .setStartTimeUtcMillis(start)
   1055                         .setEndTimeUtcMillis(end)
   1056                         .build());
   1057                 start = end;
   1058                 end += MAX_DUMMY_PROGRAM_DURATION;
   1059             }
   1060             programs.add(new Program.Builder()
   1061                     .setStartTimeUtcMillis(start)
   1062                     .setEndTimeUtcMillis(endTimeMs)
   1063                     .build());
   1064             return programs;
   1065         }
   1066 
   1067         Program getProgramAt(long timeMs) {
   1068             return getProgramAt(timeMs, 0, mPrograms.size() - 1);
   1069         }
   1070 
   1071         private Program getProgramAt(long timeMs, int start, int end) {
   1072             if (start > end) {
   1073                 return null;
   1074             }
   1075             int mid = (start + end) / 2;
   1076             Program program = mPrograms.get(mid);
   1077             if (program.getStartTimeUtcMillis() > timeMs) {
   1078                 return getProgramAt(timeMs, start, mid - 1);
   1079             } else if (program.getEndTimeUtcMillis() <= timeMs) {
   1080                 return getProgramAt(timeMs, mid+1, end);
   1081             } else {
   1082                 return program;
   1083             }
   1084         }
   1085 
   1086         private long getOldestProgramStartTime() {
   1087             if (mPrograms.isEmpty()) {
   1088                 return INVALID_TIME;
   1089             }
   1090             return mPrograms.get(0).getStartTimeUtcMillis();
   1091         }
   1092 
   1093         private Program getLastValidProgram() {
   1094             for (int i = mPrograms.size() - 1; i >= 0; --i) {
   1095                 Program program = mPrograms.get(i);
   1096                 if (program.isValid()) {
   1097                     return program;
   1098                 }
   1099             }
   1100             return null;
   1101         }
   1102 
   1103         private void schedulePrefetchPrograms() {
   1104             if (DEBUG) Log.d(TAG, "Scheduling prefetching programs.");
   1105             if (mHandler.hasMessages(MSG_PREFETCH_PROGRAM)) {
   1106                 return;
   1107             }
   1108             Program lastValidProgram = getLastValidProgram();
   1109             if (DEBUG) Log.d(TAG, "Last valid program = " + lastValidProgram);
   1110             final long delay;
   1111             if (lastValidProgram != null) {
   1112                 delay = lastValidProgram.getEndTimeUtcMillis()
   1113                         - PREFETCH_TIME_OFFSET_FROM_PROGRAM_END - System.currentTimeMillis();
   1114             } else {
   1115                 // Since there might not be any program data delay the retry 5 seconds,
   1116                 // then 30 seconds then 5 minutes
   1117                 switch (mEmptyFetchCount) {
   1118                     case 0:
   1119                         delay = 0;
   1120                         break;
   1121                     case 1:
   1122                         delay = TimeUnit.SECONDS.toMillis(5);
   1123                         break;
   1124                     case 2:
   1125                         delay = TimeUnit.SECONDS.toMillis(30);
   1126                         break;
   1127                     default:
   1128                         delay = TimeUnit.MINUTES.toMillis(5);
   1129                         break;
   1130                 }
   1131                 if (DEBUG) {
   1132                     Log.d(TAG,
   1133                             "No last valid  program. Already tried " + mEmptyFetchCount + " times");
   1134                 }
   1135             }
   1136             mHandler.sendEmptyMessageDelayed(MSG_PREFETCH_PROGRAM, delay);
   1137             if (DEBUG) Log.d(TAG, "Scheduling with " + delay + "(ms) delays.");
   1138         }
   1139 
   1140         // Prefetch programs within PREFETCH_DURATION_FOR_NEXT from now.
   1141         private void prefetchPrograms() {
   1142             long startTimeMs;
   1143             Program lastValidProgram = getLastValidProgram();
   1144             if (lastValidProgram == null) {
   1145                 startTimeMs = System.currentTimeMillis();
   1146             } else {
   1147                 startTimeMs = lastValidProgram.getEndTimeUtcMillis();
   1148             }
   1149             long endTimeMs = System.currentTimeMillis() + PREFETCH_DURATION_FOR_NEXT;
   1150             if (startTimeMs <= endTimeMs) {
   1151                 if (DEBUG) {
   1152                     Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs)
   1153                             + ", endTime=" + Utils.toTimeString(endTimeMs) + "}");
   1154                 }
   1155                 mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs));
   1156             }
   1157             startTaskIfNeeded();
   1158         }
   1159 
   1160         private class LoadProgramsForCurrentChannelTask
   1161                 extends AsyncDbTask.LoadProgramsForChannelTask {
   1162 
   1163             LoadProgramsForCurrentChannelTask(ContentResolver contentResolver,
   1164                     Range<Long> period) {
   1165                 super(contentResolver, mChannel.getId(), period);
   1166             }
   1167 
   1168             @Override
   1169             protected void onPostExecute(List<Program> programs) {
   1170                 if (DEBUG) {
   1171                     Log.d(TAG, "Programs are loaded {channelId=" + mChannelId +
   1172                             ", from=" + Utils.toTimeString(mPeriod.getLower()) +
   1173                             ", to=" + Utils.toTimeString(mPeriod.getUpper()) +
   1174                             "}");
   1175                 }
   1176                 //remove pending tasks that are fully satisfied by this query.
   1177                 Iterator<Range<Long>> it = mProgramLoadQueue.iterator();
   1178                 while (it.hasNext()) {
   1179                     Range<Long> r = it.next();
   1180                     if (mPeriod.contains(r)) {
   1181                         it.remove();
   1182                     }
   1183                 }
   1184                 if (programs == null || programs.isEmpty()) {
   1185                     mEmptyFetchCount++;
   1186                     if (addDummyPrograms(mPeriod)) {
   1187                         TimeShiftManager.this.onProgramInfoChanged();
   1188                     }
   1189                     schedulePrefetchPrograms();
   1190                     startNextLoadingIfNeeded();
   1191                     return;
   1192                 }
   1193                 mEmptyFetchCount = 0;
   1194                 if(!mPrograms.isEmpty()) {
   1195                     removeDummyPrograms();
   1196                     removeOverlappedPrograms(programs);
   1197                     Program loadedProgram = programs.get(0);
   1198                     for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) {
   1199                         Program program = mPrograms.get(i);
   1200                         while (program.getStartTimeUtcMillis() > loadedProgram
   1201                                 .getStartTimeUtcMillis()) {
   1202                             mPrograms.add(i++, loadedProgram);
   1203                             programs.remove(0);
   1204                             if (programs.isEmpty()) {
   1205                                 break;
   1206                             }
   1207                             loadedProgram = programs.get(0);
   1208                         }
   1209                     }
   1210                 }
   1211                 mPrograms.addAll(programs);
   1212                 addDummyPrograms(mPeriod);
   1213                 TimeShiftManager.this.onProgramInfoChanged();
   1214                 schedulePrefetchPrograms();
   1215                 startNextLoadingIfNeeded();
   1216             }
   1217 
   1218             @Override
   1219             protected void onCancelled(List<Program> programs) {
   1220                 if (DEBUG) {
   1221                     Log.d(TAG, "Program loading has been canceled {channelId=" + (mChannel == null
   1222                             ? "null" : mChannelId) + ", from=" + Utils
   1223                             .toTimeString(mPeriod.getLower()) + ", to=" + Utils
   1224                             .toTimeString(mPeriod.getUpper()) + "}");
   1225                 }
   1226                 startNextLoadingIfNeeded();
   1227             }
   1228 
   1229             private void startNextLoadingIfNeeded() {
   1230                 if (mProgramLoadTask == this) {
   1231                     mProgramLoadTask = null;
   1232                 }
   1233                 // Need to post to handler, because the task is still running.
   1234                 mHandler.post(new Runnable() {
   1235                     @Override
   1236                     public void run() {
   1237                         startTaskIfNeeded();
   1238                     }
   1239                 });
   1240             }
   1241 
   1242             boolean overlaps(Queue<Range<Long>> programLoadQueue) {
   1243                 for (Range<Long> r : programLoadQueue) {
   1244                     if (mPeriod.contains(r.getLower()) || mPeriod.contains(r.getUpper())) {
   1245                         return true;
   1246                     }
   1247                 }
   1248                 return false;
   1249             }
   1250         }
   1251     }
   1252 
   1253     @VisibleForTesting
   1254     final class CurrentPositionMediator {
   1255         long mCurrentPositionMs;
   1256         long mSeekRequestTimeMs;
   1257 
   1258         void initialize(long timeMs) {
   1259             mSeekRequestTimeMs = INVALID_TIME;
   1260             mCurrentPositionMs = timeMs;
   1261             if (timeMs != INVALID_TIME) {
   1262                 TimeShiftManager.this.onCurrentPositionChanged();
   1263             }
   1264         }
   1265 
   1266         void onSeekRequested(long seekTimeMs) {
   1267             mSeekRequestTimeMs = System.currentTimeMillis();
   1268             mCurrentPositionMs = seekTimeMs;
   1269             TimeShiftManager.this.onCurrentPositionChanged();
   1270         }
   1271 
   1272         void onCurrentPositionChanged(long currentPositionMs) {
   1273             if (mSeekRequestTimeMs == INVALID_TIME) {
   1274                 mCurrentPositionMs = currentPositionMs;
   1275                 TimeShiftManager.this.onCurrentPositionChanged();
   1276                 return;
   1277             }
   1278             long currentTimeMs = System.currentTimeMillis();
   1279             boolean isValid = Math.abs(currentPositionMs - mCurrentPositionMs) < REQUEST_TIMEOUT_MS;
   1280             boolean isTimeout = currentTimeMs > mSeekRequestTimeMs + REQUEST_TIMEOUT_MS;
   1281             if (isValid || isTimeout) {
   1282                 initialize(currentPositionMs);
   1283             } else {
   1284                 if (getPlayStatus() == PLAY_STATUS_PLAYING) {
   1285                     if (getPlayDirection() == PLAY_DIRECTION_FORWARD) {
   1286                         mCurrentPositionMs += (currentTimeMs - mSeekRequestTimeMs)
   1287                                 * getPlaybackSpeed();
   1288                     } else {
   1289                         mCurrentPositionMs -= (currentTimeMs - mSeekRequestTimeMs)
   1290                                 * getPlaybackSpeed();
   1291                     }
   1292                 }
   1293                 TimeShiftManager.this.onCurrentPositionChanged();
   1294             }
   1295         }
   1296     }
   1297 
   1298     /**
   1299      * The listener used to receive the events by the time-shift manager
   1300      */
   1301     public interface Listener {
   1302         /**
   1303          * Called when the availability of the time-shift for the current channel has been changed.
   1304          * If the time shift is available, {@link TimeShiftManager#getRecordStartTimeMs} should
   1305          * return the valid time.
   1306          */
   1307         void onAvailabilityChanged();
   1308 
   1309         /**
   1310          * Called when the play status is changed between {@link #PLAY_STATUS_PLAYING} and
   1311          * {@link #PLAY_STATUS_PAUSED}
   1312          *
   1313          * @param status The new play state.
   1314          */
   1315         void onPlayStatusChanged(int status);
   1316 
   1317         /**
   1318          * Called when the recordStartTime has been changed.
   1319          */
   1320         void onRecordTimeRangeChanged();
   1321 
   1322         /**
   1323          * Called when the current position is changed.
   1324          */
   1325         void onCurrentPositionChanged();
   1326 
   1327         /**
   1328          * Called when the program information is updated.
   1329          */
   1330         void onProgramInfoChanged();
   1331 
   1332         /**
   1333          * Called when an action becomes enabled or disabled.
   1334          */
   1335         void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled);
   1336     }
   1337 
   1338     private static class TimeShiftHandler extends WeakHandler<TimeShiftManager> {
   1339         TimeShiftHandler(TimeShiftManager ref) {
   1340             super(ref);
   1341         }
   1342 
   1343         @Override
   1344         public void handleMessage(Message msg, @NonNull TimeShiftManager timeShiftManager) {
   1345             switch (msg.what) {
   1346                 case MSG_GET_CURRENT_POSITION:
   1347                     timeShiftManager.mPlayController.handleGetCurrentPosition();
   1348                     break;
   1349                 case MSG_PREFETCH_PROGRAM:
   1350                     timeShiftManager.mProgramManager.prefetchPrograms();
   1351                     break;
   1352             }
   1353         }
   1354     }
   1355 }
   1356