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