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