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