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
17package com.android.tv;
18
19import android.annotation.SuppressLint;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.os.Handler;
23import android.os.Message;
24import android.support.annotation.IntDef;
25import android.support.annotation.NonNull;
26import android.support.annotation.Nullable;
27import android.support.annotation.VisibleForTesting;
28import android.util.Log;
29import android.util.Range;
30
31import com.android.tv.analytics.Tracker;
32import com.android.tv.common.SoftPreconditions;
33import com.android.tv.common.WeakHandler;
34import com.android.tv.data.Channel;
35import com.android.tv.data.OnCurrentProgramUpdatedListener;
36import com.android.tv.data.Program;
37import com.android.tv.data.ProgramDataManager;
38import com.android.tv.ui.TunableTvView;
39import com.android.tv.ui.TunableTvView.TimeShiftListener;
40import com.android.tv.util.AsyncDbTask;
41import com.android.tv.util.TimeShiftUtils;
42import com.android.tv.util.Utils;
43
44import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.Iterator;
49import java.util.LinkedList;
50import java.util.List;
51import java.util.Objects;
52import java.util.Queue;
53import 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 */
62public 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