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