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