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