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