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