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