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