1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv;
18 
19 import android.annotation.SuppressLint;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.os.Handler;
23 import android.os.Message;
24 import android.support.annotation.IntDef;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.annotation.VisibleForTesting;
28 import android.util.Log;
29 import android.util.Range;
30 
31 import com.android.tv.analytics.Tracker;
32 import com.android.tv.common.SoftPreconditions;
33 import com.android.tv.common.WeakHandler;
34 import com.android.tv.data.Channel;
35 import com.android.tv.data.OnCurrentProgramUpdatedListener;
36 import com.android.tv.data.Program;
37 import com.android.tv.data.ProgramDataManager;
38 import com.android.tv.ui.TunableTvView;
39 import com.android.tv.ui.TunableTvView.TimeShiftListener;
40 import com.android.tv.util.AsyncDbTask;
41 import com.android.tv.util.TimeShiftUtils;
42 import com.android.tv.util.Utils;
43 
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.Iterator;
49 import java.util.LinkedList;
50 import java.util.List;
51 import java.util.Objects;
52 import java.util.Queue;
53 import java.util.concurrent.TimeUnit;
54 
55 /**
56  * A class which manages the time shift feature in Live TV. It consists of two parts.
57  * {@link PlayController} controls the playback such as play/pause, rewind and fast-forward using
58  * {@link TunableTvView} which communicates with TvInputService through
59  * {@link android.media.tv.TvInputService.Session}.
60  * {@link ProgramManager} loads programs of the current channel in the background.
61  */
62 public class TimeShiftManager {
63     private static final String TAG = "TimeShiftManager";
64     private static final boolean DEBUG = false;
65 
66     @Retention(RetentionPolicy.SOURCE)
67     @IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING})
68     public @interface PlayStatus {}
69     public static final int PLAY_STATUS_PAUSED  = 0;
70     public static final int PLAY_STATUS_PLAYING = 1;
71 
72     @Retention(RetentionPolicy.SOURCE)
73     @IntDef({PLAY_SPEED_1X, PLAY_SPEED_2X, PLAY_SPEED_3X, PLAY_SPEED_4X, PLAY_SPEED_5X})
74     public @interface PlaySpeed{}
75     public static final int PLAY_SPEED_1X = 1;
76     public static final int PLAY_SPEED_2X = 2;
77     public static final int PLAY_SPEED_3X = 3;
78     public static final int PLAY_SPEED_4X = 4;
79     public static final int PLAY_SPEED_5X = 5;
80 
81     @Retention(RetentionPolicy.SOURCE)
82     @IntDef({PLAY_DIRECTION_FORWARD, PLAY_DIRECTION_BACKWARD})
83     public @interface PlayDirection{}
84     public static final int PLAY_DIRECTION_FORWARD  = 0;
85     public static final int PLAY_DIRECTION_BACKWARD = 1;
86 
87     @Retention(RetentionPolicy.SOURCE)
88     @IntDef(flag = true, value = {TIME_SHIFT_ACTION_ID_PLAY, TIME_SHIFT_ACTION_ID_PAUSE,
89             TIME_SHIFT_ACTION_ID_REWIND, TIME_SHIFT_ACTION_ID_FAST_FORWARD,
90             TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT})
91     public @interface TimeShiftActionId{}
92     public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
93     public static final int TIME_SHIFT_ACTION_ID_PAUSE = 1 << 1;
94     public static final int TIME_SHIFT_ACTION_ID_REWIND = 1 << 2;
95     public static final int TIME_SHIFT_ACTION_ID_FAST_FORWARD = 1 << 3;
96     public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS = 1 << 4;
97     public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT = 1 << 5;
98 
99     private static final int MSG_GET_CURRENT_POSITION = 1000;
100     private static final int MSG_PREFETCH_PROGRAM = 1001;
101     private static final long REQUEST_CURRENT_POSITION_INTERVAL = TimeUnit.SECONDS.toMillis(1);
102     private static final long MAX_DUMMY_PROGRAM_DURATION = TimeUnit.MINUTES.toMillis(30);
103     @VisibleForTesting
104     static final long INVALID_TIME = -1;
105     static final long CURRENT_TIME = -2;
106     private static final long PREFETCH_TIME_OFFSET_FROM_PROGRAM_END = TimeUnit.MINUTES.toMillis(1);
107     private static final long PREFETCH_DURATION_FOR_NEXT = TimeUnit.HOURS.toMillis(2);
108 
109     private static final long ALLOWED_START_TIME_OFFSET = TimeUnit.DAYS.toMillis(14);
110     private static final long TWO_WEEKS_MS = TimeUnit.DAYS.toMillis(14);
111 
112     @VisibleForTesting
113     static final long REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3);
114 
115     /**
116      * If the user presses the {@link android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS} button within
117      * this threshold from the program start time, the play position moves to the start of the
118      * previous program.
119      * Otherwise, the play position moves to the start of the current program.
120      * This value is specified in the UX document.
121      */
122     private static final long PROGRAM_START_TIME_THRESHOLD = TimeUnit.SECONDS.toMillis(3);
123     /**
124      * If the current position enters within this range from the recording start time, rewind action
125      * and jump to previous action is disabled.
126      * Similarly, if the current position enters within this range from the current system time,
127      * fast forward action and jump to next action is disabled.
128      * It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at least.
129      */
130     private static final long DISABLE_ACTION_THRESHOLD = 3 * REQUEST_CURRENT_POSITION_INTERVAL;
131     /**
132      * If the current position goes out of this range from the recording start time, rewind action
133      * and jump to previous action is enabled.
134      * Similarly, if the current position goes out of this range from the current system time,
135      * fast forward action and jump to next action is enabled.
136      * Enable threshold and disable threshold must be different because the current position
137      * does not have the continuous value. It changes every one second.
138      */
139     private static final long ENABLE_ACTION_THRESHOLD =
140             DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL;
141     /**
142      * The current position sent from TIS can not be exactly the same as the current system time
143      * due to the elapsed time to pass the message from TIS to Live TV.
144      * So the boundary threshold is necessary.
145      * The same goes for the recording start time.
146      * It's the same {@link #REQUEST_CURRENT_POSITION_INTERVAL}.
147      */
148     private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL;
149 
150     private final PlayController mPlayController;
151     private final ProgramManager mProgramManager;
152     private final Tracker mTracker;
153     @VisibleForTesting
154     final CurrentPositionMediator mCurrentPositionMediator = new CurrentPositionMediator();
155 
156     private Listener mListener;
157     private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener;
158     private int mEnabledActionIds = TIME_SHIFT_ACTION_ID_PLAY | TIME_SHIFT_ACTION_ID_PAUSE
159             | TIME_SHIFT_ACTION_ID_REWIND | TIME_SHIFT_ACTION_ID_FAST_FORWARD
160             | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT;
161     @TimeShiftActionId
162     private int mLastActionId = 0;
163 
164     // 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 
TimeShiftManager(Context context, TunableTvView tvView, ProgramDataManager programDataManager, Tracker tracker, OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener)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      */
setListener(Listener listener)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      */
isAvailable()193     public boolean isAvailable() {
194         return mPlayController.mAvailable;
195     }
196 
197     /**
198      * Returns the current time position in milliseconds.
199      */
getCurrentPositionMs()200     public long getCurrentPositionMs() {
201         return mCurrentPositionMediator.mCurrentPositionMs;
202     }
203 
setCurrentPositionMs(long currentTimeMs)204     void setCurrentPositionMs(long currentTimeMs) {
205         mCurrentPositionMediator.onCurrentPositionChanged(currentTimeMs);
206     }
207 
208     /**
209      * Returns the start time of the recording in milliseconds.
210      */
getRecordStartTimeMs()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      */
getRecordEndTimeMs()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      */
play()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      */
pause()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      */
togglePlayPause()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      */
rewind()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      */
fastForward()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      */
jumpToPrevious()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      */
jumpToNext()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      */
getPlayStatus()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      */
getDisplayedPlaySpeed()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      */
getPlayDirection()384     @PlayDirection public int getPlayDirection() {
385         return mPlayController.mPlayDirection;
386     }
387 
388     /**
389      * Returns the ID of the last action..
390      */
getLastActionId()391     @TimeShiftActionId public int getLastActionId() {
392         return mLastActionId;
393     }
394 
395     /**
396      * Enables or disables the time-shift actions.
397      */
398     @VisibleForTesting
enableAction(@imeShiftActionId int actionId, boolean enable)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 
isActionEnabled(@imeShiftActionId int actionId)412     public boolean isActionEnabled(@TimeShiftActionId int actionId) {
413         return (mEnabledActionIds & actionId) == actionId;
414     }
415 
updateActions()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 
updateCurrentProgram()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      */
isNormalPlaying()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      */
isPaused()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
getProgramAt(long timeMs)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 
onAvailabilityChanged()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 
onRecordTimeRangeChanged()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 
onCurrentPositionChanged()520     void onCurrentPositionChanged() {
521         updateActions();
522         updateCurrentProgram();
523         if (mNotificationEnabled && mListener != null) {
524             mListener.onCurrentPositionChanged();
525         }
526     }
527 
onPlayStatusChanged(@layStatus int status)528     void onPlayStatusChanged(@PlayStatus int status) {
529         if (mNotificationEnabled && mListener != null) {
530             mListener.onPlayStatusChanged(status);
531         }
532     }
533 
onProgramInfoChanged()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
getCurrentProgram()548     public Program getCurrentProgram() {
549         if (isAvailable()) {
550             return mCurrentProgram;
551         }
552         return null;
553     }
554 
getPlaybackSpeed()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 
PlayController(TunableTvView tvView)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 
onAvailabilityChanged()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 
handleGetCurrentPosition()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 
play()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 
pause()728         void pause() {
729             mDisplayedPlaySpeed = PLAY_SPEED_1X;
730             mPlaybackSpeed = 1;
731             mTvView.timeshiftPause();
732             setPlayStatus(PLAY_STATUS_PAUSED);
733             mIsPlayOffsetChanged = true;
734         }
735 
togglePlayPause()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 
rewind()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 
fastForward()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          */
seekTo(long timeMs)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 
onCurrentProgramChanged()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")
increaseDisplayedPlaySpeed()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 
setPlayStatus(@layStatus int status)816         private void setPlayStatus(@PlayStatus int status) {
817             mPlayStatus = status;
818             TimeShiftManager.this.onPlayStatusChanged(status);
819         }
820 
isForwarding()821         boolean isForwarding() {
822             return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_FORWARD;
823         }
824 
isRewinding()825         private boolean isRewinding() {
826             return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_BACKWARD;
827         }
828 
getCurrentChannel()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 
ProgramManager(ProgramDataManager programDataManager)842         ProgramManager(ProgramDataManager programDataManager) {
843             mProgramDataManager = programDataManager;
844         }
845 
onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs)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 
onRecordTimeRangeChanged(long startTimeMs, long endTimeMs)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 
startTaskIfNeeded()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 
startNext()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 
addDummyProgramsAt(long timeMs)941         void addDummyProgramsAt(long timeMs) {
942             addDummyPrograms(timeMs, timeMs + PREFETCH_DURATION_FOR_NEXT);
943         }
944 
addDummyPrograms(Range<Long> period)945         private boolean addDummyPrograms(Range<Long> period) {
946             return addDummyPrograms(period.getLower(), period.getUpper());
947         }
948 
addDummyPrograms(long startTimeMs, long endTimeMs)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 
removeDummyPrograms()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 
removeOverlappedPrograms(List<Program> loadedPrograms)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.
createDummyPrograms(long startTimeMs, long endTimeMs)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 
getProgramAt(long timeMs)1067         Program getProgramAt(long timeMs) {
1068             return getProgramAt(timeMs, 0, mPrograms.size() - 1);
1069         }
1070 
getProgramAt(long timeMs, int start, int end)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 
getOldestProgramStartTime()1086         private long getOldestProgramStartTime() {
1087             if (mPrograms.isEmpty()) {
1088                 return INVALID_TIME;
1089             }
1090             return mPrograms.get(0).getStartTimeUtcMillis();
1091         }
1092 
getLastValidProgram()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 
schedulePrefetchPrograms()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.
prefetchPrograms()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 
LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period)1163             LoadProgramsForCurrentChannelTask(ContentResolver contentResolver,
1164                     Range<Long> period) {
1165                 super(contentResolver, mChannel.getId(), period);
1166             }
1167 
1168             @Override
onPostExecute(List<Program> programs)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
onCancelled(List<Program> programs)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 
startNextLoadingIfNeeded()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 
overlaps(Queue<Range<Long>> programLoadQueue)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 
initialize(long timeMs)1258         void initialize(long timeMs) {
1259             mSeekRequestTimeMs = INVALID_TIME;
1260             mCurrentPositionMs = timeMs;
1261             if (timeMs != INVALID_TIME) {
1262                 TimeShiftManager.this.onCurrentPositionChanged();
1263             }
1264         }
1265 
onSeekRequested(long seekTimeMs)1266         void onSeekRequested(long seekTimeMs) {
1267             mSeekRequestTimeMs = System.currentTimeMillis();
1268             mCurrentPositionMs = seekTimeMs;
1269             TimeShiftManager.this.onCurrentPositionChanged();
1270         }
1271 
onCurrentPositionChanged(long currentPositionMs)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          */
onAvailabilityChanged()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          */
onPlayStatusChanged(int status)1315         void onPlayStatusChanged(int status);
1316 
1317         /**
1318          * Called when the recordStartTime has been changed.
1319          */
onRecordTimeRangeChanged()1320         void onRecordTimeRangeChanged();
1321 
1322         /**
1323          * Called when the current position is changed.
1324          */
onCurrentPositionChanged()1325         void onCurrentPositionChanged();
1326 
1327         /**
1328          * Called when the program information is updated.
1329          */
onProgramInfoChanged()1330         void onProgramInfoChanged();
1331 
1332         /**
1333          * Called when an action becomes enabled or disabled.
1334          */
onActionEnabledChanged(@imeShiftActionId int actionId, boolean enabled)1335         void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled);
1336     }
1337 
1338     private static class TimeShiftHandler extends WeakHandler<TimeShiftManager> {
TimeShiftHandler(TimeShiftManager ref)1339         TimeShiftHandler(TimeShiftManager ref) {
1340             super(ref);
1341         }
1342 
1343         @Override
handleMessage(Message msg, @NonNull TimeShiftManager timeShiftManager)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