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