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