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