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