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