• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv;
18 
19 import android.annotation.SuppressLint;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.os.Handler;
23 import android.os.Message;
24 import android.support.annotation.IntDef;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.annotation.VisibleForTesting;
28 import android.util.Log;
29 import android.util.Range;
30 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.Program;
35 import com.android.tv.data.ProgramDataManager;
36 import com.android.tv.data.api.Channel;
37 import com.android.tv.ui.TunableTvView;
38 import com.android.tv.ui.TunableTvViewPlayingApi.TimeShiftListener;
39 import com.android.tv.util.AsyncDbTask;
40 import com.android.tv.util.TimeShiftUtils;
41 import com.android.tv.util.Utils;
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.Iterator;
47 import java.util.LinkedList;
48 import java.util.List;
49 import java.util.Objects;
50 import java.util.Queue;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * A class which manages the time shift feature in Live TV. It consists of two parts. {@link
55  * PlayController} controls the playback such as play/pause, rewind and fast-forward using {@link
56  * TunableTvView} which communicates with TvInputService through {@link
57  * android.media.tv.TvInputService.Session}. {@link ProgramManager} loads programs of the current
58  * channel in the background.
59  */
60 public class TimeShiftManager {
61     private static final String TAG = "TimeShiftManager";
62     private static final boolean DEBUG = false;
63 
64     @Retention(RetentionPolicy.SOURCE)
65     @IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING})
66     public @interface PlayStatus {}
67 
68     public static final int PLAY_STATUS_PAUSED = 0;
69     public static final int PLAY_STATUS_PLAYING = 1;
70 
71     @Retention(RetentionPolicy.SOURCE)
72     @IntDef({PLAY_SPEED_1X, PLAY_SPEED_2X, PLAY_SPEED_3X, PLAY_SPEED_4X, PLAY_SPEED_5X})
73     public @interface PlaySpeed {}
74 
75     public static final int PLAY_SPEED_1X = 1;
76     public static final int PLAY_SPEED_2X = 2;
77     public static final int PLAY_SPEED_3X = 3;
78     public static final int PLAY_SPEED_4X = 4;
79     public static final int PLAY_SPEED_5X = 5;
80 
81     @Retention(RetentionPolicy.SOURCE)
82     @IntDef({PLAY_DIRECTION_FORWARD, PLAY_DIRECTION_BACKWARD})
83     public @interface PlayDirection {}
84 
85     public static final int PLAY_DIRECTION_FORWARD = 0;
86     public static final int PLAY_DIRECTION_BACKWARD = 1;
87 
88     @Retention(RetentionPolicy.SOURCE)
89     @IntDef(
90         flag = true,
91         value = {
92             TIME_SHIFT_ACTION_ID_PLAY,
93             TIME_SHIFT_ACTION_ID_PAUSE,
94             TIME_SHIFT_ACTION_ID_REWIND,
95             TIME_SHIFT_ACTION_ID_FAST_FORWARD,
96             TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
97             TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
98         }
99     )
100     public @interface TimeShiftActionId {}
101 
102     public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
103     public static final int TIME_SHIFT_ACTION_ID_PAUSE = 1 << 1;
104     public static final int TIME_SHIFT_ACTION_ID_REWIND = 1 << 2;
105     public static final int TIME_SHIFT_ACTION_ID_FAST_FORWARD = 1 << 3;
106     public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS = 1 << 4;
107     public static final int TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT = 1 << 5;
108 
109     private static final int MSG_GET_CURRENT_POSITION = 1000;
110     private static final int MSG_PREFETCH_PROGRAM = 1001;
111     private static final long REQUEST_CURRENT_POSITION_INTERVAL = TimeUnit.SECONDS.toMillis(1);
112     private static final long MAX_DUMMY_PROGRAM_DURATION = TimeUnit.MINUTES.toMillis(30);
113     @VisibleForTesting static final long INVALID_TIME = -1;
114     static final long CURRENT_TIME = -2;
115     private static final long PREFETCH_TIME_OFFSET_FROM_PROGRAM_END = TimeUnit.MINUTES.toMillis(1);
116     private static final long PREFETCH_DURATION_FOR_NEXT = TimeUnit.HOURS.toMillis(2);
117 
118     private static final long ALLOWED_START_TIME_OFFSET = TimeUnit.DAYS.toMillis(14);
119     private static final long TWO_WEEKS_MS = TimeUnit.DAYS.toMillis(14);
120 
121     @VisibleForTesting static final long REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3);
122 
123     /**
124      * If the user presses the {@link android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS} button within
125      * this threshold from the program start time, the play position moves to the start of the
126      * previous program. Otherwise, the play position moves to the start of the current program.
127      * This value is specified in the UX document.
128      */
129     private static final long PROGRAM_START_TIME_THRESHOLD = TimeUnit.SECONDS.toMillis(3);
130     /**
131      * If the current position enters within this range from the recording start time, rewind action
132      * and jump to previous action is disabled. Similarly, if the current position enters within
133      * this range from the current system time, fast forward action and jump to next action is
134      * disabled. It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at
135      * least.
136      */
137     private static final long DISABLE_ACTION_THRESHOLD = 3 * REQUEST_CURRENT_POSITION_INTERVAL;
138     /**
139      * If the current position goes out of this range from the recording start time, rewind action
140      * and jump to previous action is enabled. Similarly, if the current position goes out of this
141      * range from the current system time, fast forward action and jump to next action is enabled.
142      * Enable threshold and disable threshold must be different because the current position does
143      * not have the continuous value. It changes every one second.
144      */
145     private static final long ENABLE_ACTION_THRESHOLD =
146             DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL;
147     /**
148      * The current position sent from TIS can not be exactly the same as the current system time due
149      * to the elapsed time to pass the message from TIS to Live TV. So the boundary threshold
150      * is necessary. The same goes for the recording start time. It's the same {@link
151      * #REQUEST_CURRENT_POSITION_INTERVAL}.
152      */
153     private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL;
154 
155     private final PlayController mPlayController;
156     private final ProgramManager mProgramManager;
157     private final Tracker mTracker;
158 
159     @VisibleForTesting
160     final CurrentPositionMediator mCurrentPositionMediator = new CurrentPositionMediator();
161 
162     private Listener mListener;
163     private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener;
164     private int mEnabledActionIds =
165             TIME_SHIFT_ACTION_ID_PLAY
166                     | TIME_SHIFT_ACTION_ID_PAUSE
167                     | TIME_SHIFT_ACTION_ID_REWIND
168                     | TIME_SHIFT_ACTION_ID_FAST_FORWARD
169                     | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS
170                     | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT;
171     @TimeShiftActionId private int mLastActionId = 0;
172 
173     private final Context mContext;
174 
175     private Program mCurrentProgram;
176     // This variable is used to block notification while changing the availability status.
177     private boolean mNotificationEnabled;
178 
179     private final Handler mHandler = new TimeShiftHandler(this);
180 
TimeShiftManager( Context context, TunableTvView tvView, ProgramDataManager programDataManager, Tracker tracker, OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener)181     public TimeShiftManager(
182             Context context,
183             TunableTvView tvView,
184             ProgramDataManager programDataManager,
185             Tracker tracker,
186             OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener) {
187         mContext = context;
188         mPlayController = new PlayController(tvView);
189         mProgramManager = new ProgramManager(programDataManager);
190         mTracker = tracker;
191         mOnCurrentProgramUpdatedListener = onCurrentProgramUpdatedListener;
192     }
193 
194     /** Sets a listener which will receive events from this class. */
setListener(Listener listener)195     public void setListener(Listener listener) {
196         mListener = listener;
197     }
198 
199     /** Checks if the trick play is available for the current channel. */
isAvailable()200     public boolean isAvailable() {
201         return mPlayController.mAvailable;
202     }
203 
204     /** Returns the current time position in milliseconds. */
getCurrentPositionMs()205     public long getCurrentPositionMs() {
206         return mCurrentPositionMediator.mCurrentPositionMs;
207     }
208 
setCurrentPositionMs(long currentTimeMs)209     void setCurrentPositionMs(long currentTimeMs) {
210         mCurrentPositionMediator.onCurrentPositionChanged(currentTimeMs);
211     }
212 
213     /** Returns the start time of the recording in milliseconds. */
getRecordStartTimeMs()214     public long getRecordStartTimeMs() {
215         long oldestProgramStartTime = mProgramManager.getOldestProgramStartTime();
216         return oldestProgramStartTime == INVALID_TIME
217                 ? INVALID_TIME
218                 : mPlayController.mRecordStartTimeMs;
219     }
220 
221     /** Returns the end time of the recording in milliseconds. */
getRecordEndTimeMs()222     public long getRecordEndTimeMs() {
223         if (mPlayController.mRecordEndTimeMs == CURRENT_TIME) {
224             return System.currentTimeMillis();
225         } else {
226             return mPlayController.mRecordEndTimeMs;
227         }
228     }
229 
230     /**
231      * Plays the media.
232      *
233      * @throws IllegalStateException if the trick play is not available.
234      */
play()235     public void play() {
236         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PLAY)) {
237             return;
238         }
239         mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY);
240         mLastActionId = TIME_SHIFT_ACTION_ID_PLAY;
241         mPlayController.play();
242         updateActions();
243     }
244 
245     /**
246      * Pauses the playback.
247      *
248      * @throws IllegalStateException if the trick play is not available.
249      */
pause()250     public void pause() {
251         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_PAUSE)) {
252             return;
253         }
254         mLastActionId = TIME_SHIFT_ACTION_ID_PAUSE;
255         mTracker.sendTimeShiftAction(mLastActionId);
256         mPlayController.pause();
257         updateActions();
258     }
259 
260     /**
261      * Toggles the playing and paused state.
262      *
263      * @throws IllegalStateException if the trick play is not available.
264      */
togglePlayPause()265     public void togglePlayPause() {
266         mPlayController.togglePlayPause();
267     }
268 
269     /**
270      * Plays the media in backward direction. The playback speed is increased by 1x each time this
271      * is called. The range of the speed is from 2x to 5x. If the playing position is considered the
272      * same as the record start time, it does nothing
273      *
274      * @throws IllegalStateException if the trick play is not available.
275      */
rewind()276     public void rewind() {
277         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND)) {
278             return;
279         }
280         mLastActionId = TIME_SHIFT_ACTION_ID_REWIND;
281         mTracker.sendTimeShiftAction(mLastActionId);
282         mPlayController.rewind();
283         updateActions();
284     }
285 
286     /**
287      * Plays the media in forward direction. The playback speed is increased by 1x each time this is
288      * called. The range of the speed is from 2x to 5x. If the playing position is the same as the
289      * current time, it does nothing.
290      *
291      * @throws IllegalStateException if the trick play is not available.
292      */
fastForward()293     public void fastForward() {
294         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)) {
295             return;
296         }
297         mLastActionId = TIME_SHIFT_ACTION_ID_FAST_FORWARD;
298         mTracker.sendTimeShiftAction(mLastActionId);
299         mPlayController.fastForward();
300         updateActions();
301     }
302 
303     /**
304      * Jumps to the start of the current program. If the currently playing position is within 3
305      * seconds (={@link #PROGRAM_START_TIME_THRESHOLD})from the start time of the program, it goes
306      * to the start of the previous program if exists. If the playing position is the same as the
307      * record start time, it does nothing.
308      *
309      * @throws IllegalStateException if the trick play is not available.
310      */
jumpToPrevious()311     public void jumpToPrevious() {
312         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) {
313             return;
314         }
315         Program program =
316                 mProgramManager.getProgramAt(
317                         mCurrentPositionMediator.mCurrentPositionMs - PROGRAM_START_TIME_THRESHOLD);
318         if (program == null) {
319             return;
320         }
321         long seekPosition =
322                 Math.max(program.getStartTimeUtcMillis(), mPlayController.mRecordStartTimeMs);
323         mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS;
324         mTracker.sendTimeShiftAction(mLastActionId);
325         mPlayController.seekTo(seekPosition);
326         mCurrentPositionMediator.onSeekRequested(seekPosition);
327         updateActions();
328     }
329 
330     /**
331      * Jumps to the start of the next program if exists. If there's no next program, it jumps to the
332      * current system time and shows the live TV. If the playing position is considered the same as
333      * the current time, it does nothing.
334      *
335      * @throws IllegalStateException if the trick play is not available.
336      */
jumpToNext()337     public void jumpToNext() {
338         if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) {
339             return;
340         }
341         Program currentProgram =
342                 mProgramManager.getProgramAt(mCurrentPositionMediator.mCurrentPositionMs);
343         if (currentProgram == null) {
344             return;
345         }
346         Program nextProgram = mProgramManager.getProgramAt(currentProgram.getEndTimeUtcMillis());
347         long currentTimeMs = System.currentTimeMillis();
348         mLastActionId = TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT;
349         mTracker.sendTimeShiftAction(mLastActionId);
350         if (nextProgram == null || nextProgram.getStartTimeUtcMillis() > currentTimeMs) {
351             mPlayController.seekTo(currentTimeMs);
352             if (mPlayController.isForwarding()) {
353                 // The current position will be the current system time from now.
354                 mPlayController.mIsPlayOffsetChanged = false;
355                 mCurrentPositionMediator.initialize(currentTimeMs);
356             } else {
357                 // The current position would not be the current system time.
358                 // So need to wait for the correct time from TIS.
359                 mCurrentPositionMediator.onSeekRequested(currentTimeMs);
360             }
361         } else {
362             mPlayController.seekTo(nextProgram.getStartTimeUtcMillis());
363             mCurrentPositionMediator.onSeekRequested(nextProgram.getStartTimeUtcMillis());
364         }
365         updateActions();
366     }
367 
368     /** Returns the playback status. The value is PLAY_STATUS_PAUSED or PLAY_STATUS_PLAYING. */
369     @PlayStatus
getPlayStatus()370     public int getPlayStatus() {
371         return mPlayController.mPlayStatus;
372     }
373 
374     /**
375      * Returns the displayed playback speed. The value is one of PLAY_SPEED_1X, PLAY_SPEED_2X,
376      * PLAY_SPEED_3X, PLAY_SPEED_4X and PLAY_SPEED_5X.
377      */
378     @PlaySpeed
getDisplayedPlaySpeed()379     public int getDisplayedPlaySpeed() {
380         return mPlayController.mDisplayedPlaySpeed;
381     }
382 
383     /**
384      * Returns the playback speed. The value is PLAY_DIRECTION_FORWARD or PLAY_DIRECTION_BACKWARD.
385      */
386     @PlayDirection
getPlayDirection()387     public int getPlayDirection() {
388         return mPlayController.mPlayDirection;
389     }
390 
391     /** Returns the ID of the last action.. */
392     @TimeShiftActionId
getLastActionId()393     public int getLastActionId() {
394         return mLastActionId;
395     }
396 
397     /** Enables or disables the time-shift actions. */
398     @VisibleForTesting
enableAction(@imeShiftActionId int actionId, boolean enable)399     void enableAction(@TimeShiftActionId int actionId, boolean enable) {
400         int oldEnabledActionIds = mEnabledActionIds;
401         if (enable) {
402             mEnabledActionIds |= actionId;
403         } else {
404             mEnabledActionIds &= ~actionId;
405         }
406         if (mNotificationEnabled && mListener != null && oldEnabledActionIds != mEnabledActionIds) {
407             mListener.onActionEnabledChanged(actionId, enable);
408         }
409     }
410 
isActionEnabled(@imeShiftActionId int actionId)411     public boolean isActionEnabled(@TimeShiftActionId int actionId) {
412         return (mEnabledActionIds & actionId) == actionId;
413     }
414 
updateActions()415     private void updateActions() {
416         if (isAvailable()) {
417             enableAction(TIME_SHIFT_ACTION_ID_PLAY, true);
418             enableAction(TIME_SHIFT_ACTION_ID_PAUSE, true);
419             // Rewind action and jump to previous action.
420             long threshold =
421                     isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND)
422                             ? DISABLE_ACTION_THRESHOLD
423                             : ENABLE_ACTION_THRESHOLD;
424             boolean enabled =
425                     mCurrentPositionMediator.mCurrentPositionMs - mPlayController.mRecordStartTimeMs
426                             > threshold;
427             enableAction(TIME_SHIFT_ACTION_ID_REWIND, enabled);
428             enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, enabled);
429             // Fast forward action and jump to next action
430             threshold =
431                     isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)
432                             ? DISABLE_ACTION_THRESHOLD
433                             : ENABLE_ACTION_THRESHOLD;
434             enabled =
435                     getRecordEndTimeMs() - mCurrentPositionMediator.mCurrentPositionMs > threshold;
436             enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled);
437             enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled);
438         } else {
439             enableAction(TIME_SHIFT_ACTION_ID_PLAY, false);
440             enableAction(TIME_SHIFT_ACTION_ID_PAUSE, false);
441             enableAction(TIME_SHIFT_ACTION_ID_REWIND, false);
442             enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, false);
443             enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, false);
444             enableAction(TIME_SHIFT_ACTION_ID_PLAY, false);
445         }
446     }
447 
updateCurrentProgram()448     private void updateCurrentProgram() {
449         SoftPreconditions.checkState(isAvailable(), TAG, "Time shift is not available");
450         SoftPreconditions.checkState(mCurrentPositionMediator.mCurrentPositionMs != INVALID_TIME);
451         Program currentProgram = getProgramAt(mCurrentPositionMediator.mCurrentPositionMs);
452         if (!Program.isProgramValid(currentProgram)) {
453             currentProgram = null;
454         }
455         if (!Objects.equals(mCurrentProgram, currentProgram)) {
456             if (DEBUG) Log.d(TAG, "Current program has been updated. " + currentProgram);
457             mCurrentProgram = currentProgram;
458             if (mNotificationEnabled && mOnCurrentProgramUpdatedListener != null) {
459                 Channel channel = mPlayController.getCurrentChannel();
460                 if (channel != null) {
461                     mOnCurrentProgramUpdatedListener.onCurrentProgramUpdated(
462                             channel.getId(), mCurrentProgram);
463                     mPlayController.onCurrentProgramChanged();
464                 }
465             }
466         }
467     }
468 
469     /**
470      * Returns {@code true} if the trick play is available and it's playing to the forward direction
471      * with normal speed, otherwise {@code false}.
472      */
isNormalPlaying()473     public boolean isNormalPlaying() {
474         return mPlayController.mAvailable
475                 && mPlayController.mPlayStatus == PLAY_STATUS_PLAYING
476                 && mPlayController.mPlayDirection == PLAY_DIRECTION_FORWARD
477                 && mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X;
478     }
479 
480     /** Checks if the trick play is available and it's playback status is paused. */
isPaused()481     public boolean isPaused() {
482         return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED;
483     }
484 
485     /** Returns the program which airs at the given time. */
486     @NonNull
getProgramAt(long timeMs)487     public Program getProgramAt(long timeMs) {
488         Program program = mProgramManager.getProgramAt(timeMs);
489         if (program == null) {
490             // Guard just in case when the program prefetch handler doesn't work on time.
491             mProgramManager.addDummyProgramsAt(timeMs);
492             program = mProgramManager.getProgramAt(timeMs);
493         }
494         return program;
495     }
496 
onAvailabilityChanged()497     void onAvailabilityChanged() {
498         mCurrentPositionMediator.initialize(mPlayController.mRecordStartTimeMs);
499         mProgramManager.onAvailabilityChanged(
500                 mPlayController.mAvailable,
501                 mPlayController.getCurrentChannel(),
502                 mPlayController.mRecordStartTimeMs);
503         updateActions();
504         // Availability change notification should be always sent
505         // even if mNotificationEnabled is false.
506         if (mListener != null) {
507             mListener.onAvailabilityChanged();
508         }
509     }
510 
onRecordTimeRangeChanged()511     void onRecordTimeRangeChanged() {
512         if (mPlayController.mAvailable) {
513             mProgramManager.onRecordTimeRangeChanged(
514                     mPlayController.mRecordStartTimeMs, mPlayController.mRecordEndTimeMs);
515         }
516         updateActions();
517         if (mNotificationEnabled && mListener != null) {
518             mListener.onRecordTimeRangeChanged();
519         }
520     }
521 
onCurrentPositionChanged()522     void onCurrentPositionChanged() {
523         updateActions();
524         updateCurrentProgram();
525         if (mNotificationEnabled && mListener != null) {
526             mListener.onCurrentPositionChanged();
527         }
528     }
529 
onPlayStatusChanged(@layStatus int status)530     void onPlayStatusChanged(@PlayStatus int status) {
531         if (mNotificationEnabled && mListener != null) {
532             mListener.onPlayStatusChanged(status);
533         }
534     }
535 
onProgramInfoChanged()536     void onProgramInfoChanged() {
537         updateCurrentProgram();
538         if (mNotificationEnabled && mListener != null) {
539             mListener.onProgramInfoChanged();
540         }
541     }
542 
543     /**
544      * Returns the current program which airs right now.
545      *
546      * <p>If the program is a dummy program, which means there's no program information, returns
547      * {@code null}.
548      */
549     @Nullable
getCurrentProgram()550     public Program getCurrentProgram() {
551         if (isAvailable()) {
552             return mCurrentProgram;
553         }
554         return null;
555     }
556 
getPlaybackSpeed()557     private int getPlaybackSpeed() {
558         if (mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X) {
559             return 1;
560         } else {
561             long durationMs =
562                     (getCurrentProgram() == null ? 0 : getCurrentProgram().getDurationMillis());
563             if (mPlayController.mDisplayedPlaySpeed > PLAY_SPEED_5X) {
564                 Log.w(
565                         TAG,
566                         "Unknown displayed play speed is chosen : "
567                                 + mPlayController.mDisplayedPlaySpeed);
568                 return TimeShiftUtils.getMaxPlaybackSpeed(durationMs);
569             } else {
570                 return TimeShiftUtils.getPlaybackSpeed(
571                         mPlayController.mDisplayedPlaySpeed - PLAY_SPEED_2X, durationMs);
572             }
573         }
574     }
575 
576     /** A class which controls the trick play. */
577     private class PlayController {
578         private final TunableTvView mTvView;
579 
580         private long mAvailablityChangedTimeMs;
581         private long mRecordStartTimeMs;
582         private long mRecordEndTimeMs;
583 
584         @PlayStatus private int mPlayStatus = PLAY_STATUS_PAUSED;
585         @PlaySpeed private int mDisplayedPlaySpeed = PLAY_SPEED_1X;
586         @PlayDirection private int mPlayDirection = PLAY_DIRECTION_FORWARD;
587         private int mPlaybackSpeed;
588         private boolean mAvailable;
589 
590         /**
591          * Indicates that the trick play is not playing the current time position. It is set true
592          * when {@link PlayController#pause}, {@link PlayController#rewind}, {@link
593          * PlayController#fastForward} and {@link PlayController#seekTo} is called. If it is true,
594          * the current time is equal to System.currentTimeMillis().
595          */
596         private boolean mIsPlayOffsetChanged;
597 
PlayController(TunableTvView tvView)598         PlayController(TunableTvView tvView) {
599             mTvView = tvView;
600             mTvView.setTimeShiftListener(
601                     new TimeShiftListener() {
602                         @Override
603                         public void onAvailabilityChanged() {
604                             if (DEBUG) {
605                                 Log.d(
606                                         TAG,
607                                         "onAvailabilityChanged(available="
608                                                 + mTvView.isTimeShiftAvailable()
609                                                 + ")");
610                             }
611                             PlayController.this.onAvailabilityChanged();
612                         }
613 
614                         @Override
615                         public void onRecordStartTimeChanged(long recordStartTimeMs) {
616                             if (!SoftPreconditions.checkState(
617                                     mAvailable, TAG, "Trick play is not available.")) {
618                                 return;
619                             }
620                             if (recordStartTimeMs
621                                     < mAvailablityChangedTimeMs - ALLOWED_START_TIME_OFFSET) {
622                                 Log.e(
623                                         TAG,
624                                         "The start time is too earlier than the time of availability: {"
625                                                 + "startTime: "
626                                                 + recordStartTimeMs
627                                                 + ", availability: "
628                                                 + mAvailablityChangedTimeMs);
629                                 return;
630                             }
631                             if (recordStartTimeMs > System.currentTimeMillis()) {
632                                 // The time reported by TvInputService might not consistent with
633                                 // system
634                                 // clock,, use system's current time instead.
635                                 Log.e(
636                                         TAG,
637                                         "The start time should not be earlier than the current time, "
638                                                 + "reset the start time to the system's current time: {"
639                                                 + "startTime: "
640                                                 + recordStartTimeMs
641                                                 + ", current time: "
642                                                 + System.currentTimeMillis());
643                                 recordStartTimeMs = System.currentTimeMillis();
644                             }
645                             if (mRecordStartTimeMs == recordStartTimeMs) {
646                                 return;
647                             }
648                             mRecordStartTimeMs = recordStartTimeMs;
649                             TimeShiftManager.this.onRecordTimeRangeChanged();
650 
651                             // According to the UX guidelines, the stream should be resumed if the
652                             // recording buffer fills up while paused, which means that the current
653                             // time
654                             // position is the same as or before the recording start time.
655                             // But, for this application and the TIS, it's an erroneous and
656                             // confusing
657                             // situation if the current time position is before the recording start
658                             // time.
659                             // So, we recommend the TIS to keep the current time position greater
660                             // than or
661                             // equal to the recording start time.
662                             // And here, we assume that the buffer is full if the current time
663                             // position
664                             // is nearly equal to the recording start time.
665                             if (mPlayStatus == PLAY_STATUS_PAUSED
666                                     && getCurrentPositionMs() - mRecordStartTimeMs
667                                             < RECORDING_BOUNDARY_THRESHOLD) {
668                                 TimeShiftManager.this.play();
669                             }
670                         }
671                     });
672         }
673 
onAvailabilityChanged()674         void onAvailabilityChanged() {
675             boolean newAvailable = mTvView.isTimeShiftAvailable();
676             if (mAvailable == newAvailable) {
677                 return;
678             }
679             mAvailable = newAvailable;
680             // Do not send the notifications while the availability is changing,
681             // because the variables are in the intermediate state.
682             // For example, the current program can be null.
683             mNotificationEnabled = false;
684             mDisplayedPlaySpeed = PLAY_SPEED_1X;
685             mPlaybackSpeed = 1;
686             mPlayDirection = PLAY_DIRECTION_FORWARD;
687             mHandler.removeMessages(MSG_GET_CURRENT_POSITION);
688 
689             if (mAvailable) {
690                 mAvailablityChangedTimeMs = System.currentTimeMillis();
691                 mIsPlayOffsetChanged = false;
692                 mRecordStartTimeMs = mAvailablityChangedTimeMs;
693                 mRecordEndTimeMs = CURRENT_TIME;
694                 // When the media availability message has come.
695                 mPlayController.setPlayStatus(PLAY_STATUS_PLAYING);
696                 mHandler.sendEmptyMessageDelayed(
697                         MSG_GET_CURRENT_POSITION, REQUEST_CURRENT_POSITION_INTERVAL);
698             } else {
699                 mAvailablityChangedTimeMs = INVALID_TIME;
700                 mIsPlayOffsetChanged = false;
701                 mRecordStartTimeMs = INVALID_TIME;
702                 mRecordEndTimeMs = INVALID_TIME;
703                 // When the tune command is sent.
704                 mPlayController.setPlayStatus(PLAY_STATUS_PAUSED);
705             }
706             TimeShiftManager.this.onAvailabilityChanged();
707             mNotificationEnabled = true;
708         }
709 
handleGetCurrentPosition()710         void handleGetCurrentPosition() {
711             if (mIsPlayOffsetChanged) {
712                 long currentTimeMs =
713                         mRecordEndTimeMs == CURRENT_TIME
714                                 ? System.currentTimeMillis()
715                                 : mRecordEndTimeMs;
716                 long currentPositionMs =
717                         Math.max(
718                                 Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs),
719                                 mRecordStartTimeMs);
720                 boolean isCurrentTime =
721                         currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD;
722                 long newCurrentPositionMs;
723                 if (isCurrentTime && isForwarding()) {
724                     // It's playing forward and the current playing position reached
725                     // the current system time. i.e. The live stream is played.
726                     // Therefore no need to call TvView.timeshiftGetCurrentPositionMs
727                     // any more.
728                     newCurrentPositionMs = currentTimeMs;
729                     mIsPlayOffsetChanged = false;
730                     if (mDisplayedPlaySpeed > PLAY_SPEED_1X) {
731                         TimeShiftManager.this.play();
732                     }
733                 } else {
734                     newCurrentPositionMs = currentPositionMs;
735                     boolean isRecordStartTime =
736                             currentPositionMs - mRecordStartTimeMs < RECORDING_BOUNDARY_THRESHOLD;
737                     if (isRecordStartTime && isRewinding()) {
738                         TimeShiftManager.this.play();
739                     }
740                 }
741                 setCurrentPositionMs(newCurrentPositionMs);
742             } else {
743                 setCurrentPositionMs(System.currentTimeMillis());
744                 TimeShiftManager.this.onCurrentPositionChanged();
745             }
746             // Need to send message here just in case there is no or invalid response
747             // for the current time position request from TIS.
748             mHandler.sendEmptyMessageDelayed(
749                     MSG_GET_CURRENT_POSITION, REQUEST_CURRENT_POSITION_INTERVAL);
750         }
751 
play()752         void play() {
753             mDisplayedPlaySpeed = PLAY_SPEED_1X;
754             mPlaybackSpeed = 1;
755             mPlayDirection = PLAY_DIRECTION_FORWARD;
756             mTvView.timeshiftPlay();
757             setPlayStatus(PLAY_STATUS_PLAYING);
758         }
759 
pause()760         void pause() {
761             mDisplayedPlaySpeed = PLAY_SPEED_1X;
762             mPlaybackSpeed = 1;
763             mTvView.timeshiftPause();
764             setPlayStatus(PLAY_STATUS_PAUSED);
765             mIsPlayOffsetChanged = true;
766         }
767 
togglePlayPause()768         void togglePlayPause() {
769             if (mPlayStatus == PLAY_STATUS_PAUSED) {
770                 play();
771                 mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PLAY);
772             } else {
773                 pause();
774                 mTracker.sendTimeShiftAction(TIME_SHIFT_ACTION_ID_PAUSE);
775             }
776         }
777 
rewind()778         void rewind() {
779             if (mPlayDirection == PLAY_DIRECTION_BACKWARD) {
780                 increaseDisplayedPlaySpeed();
781             } else {
782                 mDisplayedPlaySpeed = PLAY_SPEED_2X;
783             }
784             mPlayDirection = PLAY_DIRECTION_BACKWARD;
785             mPlaybackSpeed = getPlaybackSpeed();
786             mTvView.timeshiftRewind(mPlaybackSpeed);
787             setPlayStatus(PLAY_STATUS_PLAYING);
788             mIsPlayOffsetChanged = true;
789         }
790 
fastForward()791         void fastForward() {
792             if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
793                 increaseDisplayedPlaySpeed();
794             } else {
795                 mDisplayedPlaySpeed = PLAY_SPEED_2X;
796             }
797             mPlayDirection = PLAY_DIRECTION_FORWARD;
798             mPlaybackSpeed = getPlaybackSpeed();
799             mTvView.timeshiftFastForward(mPlaybackSpeed);
800             setPlayStatus(PLAY_STATUS_PLAYING);
801             mIsPlayOffsetChanged = true;
802         }
803 
804         /** Moves to the specified time. */
seekTo(long timeMs)805         void seekTo(long timeMs) {
806             mTvView.timeshiftSeekTo(
807                     Math.min(
808                             mRecordEndTimeMs == CURRENT_TIME
809                                     ? System.currentTimeMillis()
810                                     : mRecordEndTimeMs,
811                             Math.max(mRecordStartTimeMs, timeMs)));
812             mIsPlayOffsetChanged = true;
813         }
814 
onCurrentProgramChanged()815         void onCurrentProgramChanged() {
816             // Update playback speed
817             if (mDisplayedPlaySpeed == PLAY_SPEED_1X) {
818                 return;
819             }
820             int playbackSpeed = getPlaybackSpeed();
821             if (playbackSpeed != mPlaybackSpeed) {
822                 mPlaybackSpeed = playbackSpeed;
823                 if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
824                     mTvView.timeshiftFastForward(mPlaybackSpeed);
825                 } else {
826                     mTvView.timeshiftRewind(mPlaybackSpeed);
827                 }
828             }
829         }
830 
831         @SuppressLint("SwitchIntDef")
increaseDisplayedPlaySpeed()832         private void increaseDisplayedPlaySpeed() {
833             switch (mDisplayedPlaySpeed) {
834                 case PLAY_SPEED_1X:
835                     mDisplayedPlaySpeed = PLAY_SPEED_2X;
836                     break;
837                 case PLAY_SPEED_2X:
838                     mDisplayedPlaySpeed = PLAY_SPEED_3X;
839                     break;
840                 case PLAY_SPEED_3X:
841                     mDisplayedPlaySpeed = PLAY_SPEED_4X;
842                     break;
843                 case PLAY_SPEED_4X:
844                     mDisplayedPlaySpeed = PLAY_SPEED_5X;
845                     break;
846             }
847         }
848 
setPlayStatus(@layStatus int status)849         private void setPlayStatus(@PlayStatus int status) {
850             mPlayStatus = status;
851             TimeShiftManager.this.onPlayStatusChanged(status);
852         }
853 
isForwarding()854         boolean isForwarding() {
855             return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_FORWARD;
856         }
857 
isRewinding()858         private boolean isRewinding() {
859             return mPlayStatus == PLAY_STATUS_PLAYING && mPlayDirection == PLAY_DIRECTION_BACKWARD;
860         }
861 
getCurrentChannel()862         Channel getCurrentChannel() {
863             return mTvView.getCurrentChannel();
864         }
865     }
866 
867     private class ProgramManager {
868         private final ProgramDataManager mProgramDataManager;
869         private Channel mChannel;
870         private final List<Program> mPrograms = new ArrayList<>();
871         private final Queue<Range<Long>> mProgramLoadQueue = new LinkedList<>();
872         private LoadProgramsForCurrentChannelTask mProgramLoadTask = null;
873         private int mEmptyFetchCount = 0;
874 
ProgramManager(ProgramDataManager programDataManager)875         ProgramManager(ProgramDataManager programDataManager) {
876             mProgramDataManager = programDataManager;
877         }
878 
onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs)879         void onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs) {
880             if (DEBUG) {
881                 Log.d(
882                         TAG,
883                         "onAvailabilityChanged("
884                                 + available
885                                 + "+,"
886                                 + channel
887                                 + ", "
888                                 + currentPositionMs
889                                 + ")");
890             }
891 
892             mProgramLoadQueue.clear();
893             if (mProgramLoadTask != null) {
894                 mProgramLoadTask.cancel(true);
895             }
896             mHandler.removeMessages(MSG_PREFETCH_PROGRAM);
897             mPrograms.clear();
898             mEmptyFetchCount = 0;
899             mChannel = channel;
900             if (channel == null || channel.isPassthrough() || currentPositionMs == INVALID_TIME) {
901                 return;
902             }
903             if (available) {
904                 Program program = mProgramDataManager.getCurrentProgram(channel.getId());
905                 long prefetchStartTimeMs;
906                 if (program != null) {
907                     mPrograms.add(program);
908                     prefetchStartTimeMs = program.getEndTimeUtcMillis();
909                 } else {
910                     prefetchStartTimeMs =
911                             Utils.floorTime(currentPositionMs, MAX_DUMMY_PROGRAM_DURATION);
912                 }
913                 // Create dummy program
914                 mPrograms.addAll(
915                         createDummyPrograms(
916                                 prefetchStartTimeMs,
917                                 currentPositionMs + PREFETCH_DURATION_FOR_NEXT));
918                 schedulePrefetchPrograms();
919                 TimeShiftManager.this.onProgramInfoChanged();
920             }
921         }
922 
onRecordTimeRangeChanged(long startTimeMs, long endTimeMs)923         void onRecordTimeRangeChanged(long startTimeMs, long endTimeMs) {
924             if (mChannel == null || mChannel.isPassthrough()) {
925                 return;
926             }
927             if (endTimeMs == CURRENT_TIME) {
928                 endTimeMs = System.currentTimeMillis();
929             }
930 
931             long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION);
932             long fetchEndTimeMs =
933                     Utils.ceilTime(
934                             endTimeMs + PREFETCH_DURATION_FOR_NEXT, MAX_DUMMY_PROGRAM_DURATION);
935             removeOutdatedPrograms(fetchStartTimeMs);
936             boolean needToLoad = addDummyPrograms(fetchStartTimeMs, fetchEndTimeMs);
937             if (needToLoad) {
938                 Range<Long> period = Range.create(fetchStartTimeMs, fetchEndTimeMs);
939                 mProgramLoadQueue.add(period);
940                 startTaskIfNeeded();
941             }
942         }
943 
startTaskIfNeeded()944         private void startTaskIfNeeded() {
945             if (mProgramLoadQueue.isEmpty()) {
946                 return;
947             }
948             if (mProgramLoadTask == null || mProgramLoadTask.isCancelled()) {
949                 startNext();
950             } else {
951                 // Remove pending task fully satisfied by the current
952                 Range<Long> current = mProgramLoadTask.getPeriod();
953                 Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
954                 while (i.hasNext()) {
955                     Range<Long> r = i.next();
956                     if (current.contains(r)) {
957                         i.remove();
958                     }
959                 }
960             }
961         }
962 
startNext()963         private void startNext() {
964             mProgramLoadTask = null;
965             if (mProgramLoadQueue.isEmpty()) {
966                 return;
967             }
968 
969             Range<Long> next = mProgramLoadQueue.poll();
970             // Extend next to include any overlapping Ranges.
971             Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
972             while (i.hasNext()) {
973                 Range<Long> r = i.next();
974                 if (next.contains(r.getLower()) || next.contains(r.getUpper())) {
975                     i.remove();
976                     next = next.extend(r);
977                 }
978             }
979             if (mChannel != null) {
980                 mProgramLoadTask =
981                         new LoadProgramsForCurrentChannelTask(mContext.getContentResolver(), 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 Program.Builder()
1110                                 .setStartTimeUtcMillis(start)
1111                                 .setEndTimeUtcMillis(end)
1112                                 .build());
1113                 start = end;
1114                 end += MAX_DUMMY_PROGRAM_DURATION;
1115             }
1116             programs.add(
1117                     new Program.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(ContentResolver contentResolver, Range<Long> period)1228             LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period) {
1229                 super(
1230                         TvSingletons.getSingletons(mContext).getDbExecutor(),
1231                         contentResolver,
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(
1313                         new Runnable() {
1314                             @Override
1315                             public void run() {
1316                                 startTaskIfNeeded();
1317                             }
1318                         });
1319             }
1320 
overlaps(Queue<Range<Long>> programLoadQueue)1321             boolean overlaps(Queue<Range<Long>> programLoadQueue) {
1322                 for (Range<Long> r : programLoadQueue) {
1323                     if (mPeriod.contains(r.getLower()) || mPeriod.contains(r.getUpper())) {
1324                         return true;
1325                     }
1326                 }
1327                 return false;
1328             }
1329         }
1330     }
1331 
1332     @VisibleForTesting
1333     final class CurrentPositionMediator {
1334         long mCurrentPositionMs;
1335         long mSeekRequestTimeMs;
1336 
initialize(long timeMs)1337         void initialize(long timeMs) {
1338             mSeekRequestTimeMs = INVALID_TIME;
1339             mCurrentPositionMs = timeMs;
1340             if (timeMs != INVALID_TIME) {
1341                 TimeShiftManager.this.onCurrentPositionChanged();
1342             }
1343         }
1344 
onSeekRequested(long seekTimeMs)1345         void onSeekRequested(long seekTimeMs) {
1346             mSeekRequestTimeMs = System.currentTimeMillis();
1347             mCurrentPositionMs = seekTimeMs;
1348             TimeShiftManager.this.onCurrentPositionChanged();
1349         }
1350 
onCurrentPositionChanged(long currentPositionMs)1351         void onCurrentPositionChanged(long currentPositionMs) {
1352             if (mSeekRequestTimeMs == INVALID_TIME) {
1353                 mCurrentPositionMs = currentPositionMs;
1354                 TimeShiftManager.this.onCurrentPositionChanged();
1355                 return;
1356             }
1357             long currentTimeMs = System.currentTimeMillis();
1358             boolean isValid = Math.abs(currentPositionMs - mCurrentPositionMs) < REQUEST_TIMEOUT_MS;
1359             boolean isTimeout = currentTimeMs > mSeekRequestTimeMs + REQUEST_TIMEOUT_MS;
1360             if (isValid || isTimeout) {
1361                 initialize(currentPositionMs);
1362             } else {
1363                 if (getPlayStatus() == PLAY_STATUS_PLAYING) {
1364                     if (getPlayDirection() == PLAY_DIRECTION_FORWARD) {
1365                         mCurrentPositionMs +=
1366                                 (currentTimeMs - mSeekRequestTimeMs) * getPlaybackSpeed();
1367                     } else {
1368                         mCurrentPositionMs -=
1369                                 (currentTimeMs - mSeekRequestTimeMs) * getPlaybackSpeed();
1370                     }
1371                 }
1372                 TimeShiftManager.this.onCurrentPositionChanged();
1373             }
1374         }
1375     }
1376 
1377     /** The listener used to receive the events by the time-shift manager */
1378     public interface Listener {
1379         /**
1380          * Called when the availability of the time-shift for the current channel has been changed.
1381          * If the time shift is available, {@link TimeShiftManager#getRecordStartTimeMs} should
1382          * return the valid time.
1383          */
onAvailabilityChanged()1384         void onAvailabilityChanged();
1385 
1386         /**
1387          * Called when the play status is changed between {@link #PLAY_STATUS_PLAYING} and {@link
1388          * #PLAY_STATUS_PAUSED}
1389          *
1390          * @param status The new play state.
1391          */
onPlayStatusChanged(int status)1392         void onPlayStatusChanged(int status);
1393 
1394         /** Called when the recordStartTime has been changed. */
onRecordTimeRangeChanged()1395         void onRecordTimeRangeChanged();
1396 
1397         /** Called when the current position is changed. */
onCurrentPositionChanged()1398         void onCurrentPositionChanged();
1399 
1400         /** Called when the program information is updated. */
onProgramInfoChanged()1401         void onProgramInfoChanged();
1402 
1403         /** Called when an action becomes enabled or disabled. */
onActionEnabledChanged(@imeShiftActionId int actionId, boolean enabled)1404         void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled);
1405     }
1406 
1407     private static class TimeShiftHandler extends WeakHandler<TimeShiftManager> {
TimeShiftHandler(TimeShiftManager ref)1408         TimeShiftHandler(TimeShiftManager ref) {
1409             super(ref);
1410         }
1411 
1412         @Override
handleMessage(Message msg, @NonNull TimeShiftManager timeShiftManager)1413         public void handleMessage(Message msg, @NonNull TimeShiftManager timeShiftManager) {
1414             switch (msg.what) {
1415                 case MSG_GET_CURRENT_POSITION:
1416                     timeShiftManager.mPlayController.handleGetCurrentPosition();
1417                     break;
1418                 case MSG_PREFETCH_PROGRAM:
1419                     timeShiftManager.mProgramManager.prefetchPrograms();
1420                     break;
1421             }
1422         }
1423     }
1424 }
1425