1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.ui; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.TimeInterpolator; 22 import android.annotation.SuppressLint; 23 import android.annotation.TargetApi; 24 import android.content.ContentUris; 25 import android.content.Context; 26 import android.content.pm.PackageManager; 27 import android.media.PlaybackParams; 28 import android.media.tv.TvContentRating; 29 import android.media.tv.TvInputInfo; 30 import android.media.tv.TvInputManager; 31 import android.media.tv.TvTrackInfo; 32 import android.media.tv.TvView; 33 import android.media.tv.TvView.OnUnhandledInputEventListener; 34 import android.media.tv.TvView.TvInputCallback; 35 import android.net.ConnectivityManager; 36 import android.net.Uri; 37 import android.os.AsyncTask; 38 import android.os.Build; 39 import android.os.Bundle; 40 import android.support.annotation.IntDef; 41 import android.support.annotation.Nullable; 42 import android.support.annotation.UiThread; 43 import android.support.v4.os.BuildCompat; 44 import android.text.TextUtils; 45 import android.util.AttributeSet; 46 import android.util.Log; 47 import android.view.KeyEvent; 48 import android.view.MotionEvent; 49 import android.view.SurfaceView; 50 import android.view.View; 51 import android.view.ViewGroup; 52 import android.widget.FrameLayout; 53 import android.widget.ImageView; 54 55 import com.android.tv.ApplicationSingletons; 56 import com.android.tv.R; 57 import com.android.tv.TvApplication; 58 import com.android.tv.analytics.DurationTimer; 59 import com.android.tv.analytics.Tracker; 60 import com.android.tv.common.feature.CommonFeatures; 61 import com.android.tv.common.recording.RecordedProgram; 62 import com.android.tv.data.Channel; 63 import com.android.tv.data.ChannelDataManager; 64 import com.android.tv.data.StreamInfo; 65 import com.android.tv.data.WatchedHistoryManager; 66 import com.android.tv.dvr.DvrDataManager; 67 import com.android.tv.parental.ContentRatingsManager; 68 import com.android.tv.recommendation.NotificationService; 69 import com.android.tv.util.NetworkUtils; 70 import com.android.tv.util.PermissionUtils; 71 import com.android.tv.util.TvInputManagerHelper; 72 import com.android.tv.util.Utils; 73 74 import java.lang.annotation.Retention; 75 import java.lang.annotation.RetentionPolicy; 76 import java.lang.reflect.InvocationTargetException; 77 import java.lang.reflect.Method; 78 import java.util.List; 79 80 public class TunableTvView extends FrameLayout implements StreamInfo { 81 private static final boolean DEBUG = false; 82 private static final String TAG = "TunableTvView"; 83 84 public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1; 85 86 @Retention(RetentionPolicy.SOURCE) 87 @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL}) 88 public @interface BlockScreenType {} 89 public static final int BLOCK_SCREEN_TYPE_NO_UI = 0; 90 public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1; 91 public static final int BLOCK_SCREEN_TYPE_NORMAL = 2; 92 93 private static final String PERMISSION_RECEIVE_INPUT_EVENT = 94 "com.android.tv.permission.RECEIVE_INPUT_EVENT"; 95 96 @Retention(RetentionPolicy.SOURCE) 97 @IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE, 98 TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD }) 99 private @interface TimeShiftState {} 100 private static final int TIME_SHIFT_STATE_NONE = 0; 101 private static final int TIME_SHIFT_STATE_PLAY = 1; 102 private static final int TIME_SHIFT_STATE_PAUSE = 2; 103 private static final int TIME_SHIFT_STATE_REWIND = 3; 104 private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4; 105 106 private static final int FADED_IN = 0; 107 private static final int FADED_OUT = 1; 108 private static final int FADING_IN = 2; 109 private static final int FADING_OUT = 3; 110 111 private static final long INVALID_TIME = -1; 112 113 // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR. 114 private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f; 115 116 private AppLayerTvView mTvView; 117 private Channel mCurrentChannel; 118 private RecordedProgram mRecordedProgram; 119 private TvInputManagerHelper mInputManagerHelper; 120 private ContentRatingsManager mContentRatingsManager; 121 @Nullable 122 private WatchedHistoryManager mWatchedHistoryManager; 123 private boolean mStarted; 124 private TvInputInfo mInputInfo; 125 private OnTuneListener mOnTuneListener; 126 private int mVideoWidth; 127 private int mVideoHeight; 128 private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 129 private float mVideoFrameRate; 130 private float mVideoDisplayAspectRatio; 131 private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 132 private boolean mHasClosedCaption = false; 133 private boolean mVideoAvailable; 134 private boolean mScreenBlocked; 135 private OnScreenBlockingChangedListener mOnScreenBlockedListener; 136 private TvContentRating mBlockedContentRating; 137 private int mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED; 138 private boolean mCanReceiveInputEvent; 139 private boolean mIsMuted; 140 private float mVolume; 141 private boolean mParentControlEnabled; 142 private int mFixedSurfaceWidth; 143 private int mFixedSurfaceHeight; 144 private boolean mIsPip; 145 private int mScreenHeight; 146 private int mShrunkenTvViewHeight; 147 private final boolean mCanModifyParentalControls; 148 149 @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; 150 private TimeShiftListener mTimeShiftListener; 151 private boolean mTimeShiftAvailable; 152 private long mTimeShiftCurrentPositionMs = INVALID_TIME; 153 154 private final Tracker mTracker; 155 private final DurationTimer mChannelViewTimer = new DurationTimer(); 156 private InternetCheckTask mInternetCheckTask; 157 158 // A block screen view which has lock icon with black background. 159 // This indicates that user's action is needed to play video. 160 private final BlockScreenView mBlockScreenView; 161 162 // A View to hide screen when there's problem in video playback. 163 private final BlockScreenView mHideScreenView; 164 165 // A View to block screen until onContentAllowed is received if parental control is on. 166 private final View mBlockScreenForTuneView; 167 168 // A spinner view to show buffering status. 169 private final View mBufferingSpinnerView; 170 171 // A View for fade-in/out animation 172 private final View mDimScreenView; 173 private int mFadeState = FADED_IN; 174 private Runnable mActionAfterFade; 175 176 @BlockScreenType private int mBlockScreenType; 177 178 private final DvrDataManager mDvrDataManager; 179 private final ChannelDataManager mChannelDataManager; 180 private final ConnectivityManager mConnectivityManager; 181 182 private final TvInputCallback mCallback = 183 new TvInputCallback() { 184 @Override 185 public void onConnectionFailed(String inputId) { 186 Log.w(TAG, "Failed to bind an input"); 187 mTracker.sendInputConnectionFailure(inputId); 188 Channel channel = mCurrentChannel; 189 mCurrentChannel = null; 190 mInputInfo = null; 191 mCanReceiveInputEvent = false; 192 if (mOnTuneListener != null) { 193 // If tune is called inside onTuneFailed, mOnTuneListener will be set to 194 // a new instance. In order to avoid to clear the new mOnTuneListener, 195 // we copy mOnTuneListener to l and clear mOnTuneListener before 196 // calling onTuneFailed. 197 OnTuneListener listener = mOnTuneListener; 198 mOnTuneListener = null; 199 listener.onTuneFailed(channel); 200 } 201 } 202 203 @Override 204 public void onDisconnected(String inputId) { 205 Log.w(TAG, "Session is released by crash"); 206 mTracker.sendInputDisconnected(inputId); 207 Channel channel = mCurrentChannel; 208 mCurrentChannel = null; 209 mInputInfo = null; 210 mCanReceiveInputEvent = false; 211 if (mOnTuneListener != null) { 212 OnTuneListener listener = mOnTuneListener; 213 mOnTuneListener = null; 214 listener.onUnexpectedStop(channel); 215 } 216 } 217 218 @Override 219 public void onChannelRetuned(String inputId, Uri channelUri) { 220 if (DEBUG) { 221 Log.d(TAG, "onChannelRetuned(inputId=" + inputId + ", channelUri=" 222 + channelUri + ")"); 223 } 224 if (mOnTuneListener != null) { 225 mOnTuneListener.onChannelRetuned(channelUri); 226 } 227 } 228 229 @Override 230 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 231 mHasClosedCaption = false; 232 for (TvTrackInfo track : tracks) { 233 if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) { 234 mHasClosedCaption = true; 235 break; 236 } 237 } 238 if (mOnTuneListener != null) { 239 mOnTuneListener.onStreamInfoChanged(TunableTvView.this); 240 } 241 } 242 243 @Override 244 public void onTrackSelected(String inputId, int type, String trackId) { 245 if (trackId == null) { 246 // A track is unselected. 247 if (type == TvTrackInfo.TYPE_VIDEO) { 248 mVideoWidth = 0; 249 mVideoHeight = 0; 250 mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 251 mVideoFrameRate = 0f; 252 mVideoDisplayAspectRatio = 0f; 253 } else if (type == TvTrackInfo.TYPE_AUDIO) { 254 mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 255 } 256 } else { 257 List<TvTrackInfo> tracks = getTracks(type); 258 boolean trackFound = false; 259 if (tracks != null) { 260 for (TvTrackInfo track : tracks) { 261 if (track.getId().equals(trackId)) { 262 if (type == TvTrackInfo.TYPE_VIDEO) { 263 mVideoWidth = track.getVideoWidth(); 264 mVideoHeight = track.getVideoHeight(); 265 mVideoFormat = Utils.getVideoDefinitionLevelFromSize( 266 mVideoWidth, mVideoHeight); 267 mVideoFrameRate = track.getVideoFrameRate(); 268 if (mVideoWidth <= 0 || mVideoHeight <= 0) { 269 mVideoDisplayAspectRatio = 0.0f; 270 } else if (android.os.Build.VERSION.SDK_INT >= 271 android.os.Build.VERSION_CODES.M) { 272 float VideoPixelAspectRatio = 273 track.getVideoPixelAspectRatio(); 274 mVideoDisplayAspectRatio = VideoPixelAspectRatio 275 * mVideoWidth / mVideoHeight; 276 } else { 277 mVideoDisplayAspectRatio = mVideoWidth 278 / (float) mVideoHeight; 279 } 280 } else if (type == TvTrackInfo.TYPE_AUDIO) { 281 mAudioChannelCount = track.getAudioChannelCount(); 282 } 283 trackFound = true; 284 break; 285 } 286 } 287 } 288 if (!trackFound) { 289 Log.w(TAG, "Invalid track ID: " + trackId); 290 } 291 } 292 if (mOnTuneListener != null) { 293 mOnTuneListener.onStreamInfoChanged(TunableTvView.this); 294 } 295 } 296 297 @Override 298 public void onVideoAvailable(String inputId) { 299 unhideScreenByVideoAvailability(); 300 if (mOnTuneListener != null) { 301 mOnTuneListener.onStreamInfoChanged(TunableTvView.this); 302 } 303 } 304 305 @Override 306 public void onVideoUnavailable(String inputId, int reason) { 307 hideScreenByVideoAvailability(reason); 308 if (mOnTuneListener != null) { 309 mOnTuneListener.onStreamInfoChanged(TunableTvView.this); 310 } 311 switch (reason) { 312 case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: 313 case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: 314 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: 315 mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); 316 default: 317 // do nothing 318 } 319 } 320 321 @Override 322 public void onContentAllowed(String inputId) { 323 mBlockScreenForTuneView.setVisibility(View.GONE); 324 unblockScreenByContentRating(); 325 if (mOnTuneListener != null) { 326 mOnTuneListener.onContentAllowed(); 327 } 328 } 329 330 @Override 331 public void onContentBlocked(String inputId, TvContentRating rating) { 332 blockScreenByContentRating(rating); 333 if (mOnTuneListener != null) { 334 mOnTuneListener.onContentBlocked(); 335 } 336 } 337 338 @Override 339 @TargetApi(Build.VERSION_CODES.M) 340 public void onTimeShiftStatusChanged(String inputId, int status) { 341 boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; 342 setTimeShiftAvailable(available); 343 } 344 }; 345 TunableTvView(Context context)346 public TunableTvView(Context context) { 347 this(context, null); 348 } 349 TunableTvView(Context context, AttributeSet attrs)350 public TunableTvView(Context context, AttributeSet attrs) { 351 this(context, attrs, 0); 352 } 353 TunableTvView(Context context, AttributeSet attrs, int defStyleAttr)354 public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) { 355 this(context, attrs, defStyleAttr, 0); 356 } 357 TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)358 public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 359 super(context, attrs, defStyleAttr, defStyleRes); 360 inflate(getContext(), R.layout.tunable_tv_view, this); 361 362 ApplicationSingletons appSingletons = TvApplication.getSingletons(context); 363 mDvrDataManager = CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN() 364 ? appSingletons.getDvrDataManager() 365 : null; 366 mChannelDataManager = appSingletons.getChannelDataManager(); 367 mConnectivityManager = (ConnectivityManager) context 368 .getSystemService(Context.CONNECTIVITY_SERVICE); 369 mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); 370 mTracker = appSingletons.getTracker(); 371 mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; 372 mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen); 373 if (!mCanModifyParentalControls) { 374 mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission); 375 mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER); 376 } else { 377 mBlockScreenView.setImage(R.drawable.ic_message_lock); 378 } 379 mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview); 380 mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() { 381 @Override 382 public void onAnimationEnd(Animator animation) { 383 adjustBlockScreenSpacingAndText(); 384 } 385 }); 386 387 mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen); 388 mHideScreenView.setImageVisibility(false); 389 mBufferingSpinnerView = findViewById(R.id.buffering_spinner); 390 mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune); 391 mDimScreenView = findViewById(R.id.dim); 392 mDimScreenView.animate().setListener(new AnimatorListenerAdapter() { 393 @Override 394 public void onAnimationEnd(Animator animation) { 395 if (mActionAfterFade != null) { 396 mActionAfterFade.run(); 397 } 398 } 399 400 @Override 401 public void onAnimationCancel(Animator animation) { 402 if (mActionAfterFade != null) { 403 mActionAfterFade.run(); 404 } 405 } 406 }); 407 } 408 initialize(AppLayerTvView tvView, boolean isPip, int screenHeight, int shrunkenTvViewHeight)409 public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight, 410 int shrunkenTvViewHeight) { 411 mTvView = tvView; 412 mIsPip = isPip; 413 mScreenHeight = screenHeight; 414 mShrunkenTvViewHeight = shrunkenTvViewHeight; 415 mTvView.setZOrderOnTop(isPip); 416 copyLayoutParamsToTvView(); 417 } 418 start(TvInputManagerHelper tvInputManagerHelper)419 public void start(TvInputManagerHelper tvInputManagerHelper) { 420 mInputManagerHelper = tvInputManagerHelper; 421 mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); 422 if (mStarted) { 423 return; 424 } 425 mStarted = true; 426 } 427 stop()428 public void stop() { 429 if (!mStarted) { 430 return; 431 } 432 mStarted = false; 433 if (mCurrentChannel != null) { 434 long duration = mChannelViewTimer.reset(); 435 mTracker.sendChannelViewStop(mCurrentChannel, duration); 436 if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { 437 mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, 438 System.currentTimeMillis(), duration); 439 } 440 } 441 reset(); 442 } 443 reset()444 public void reset() { 445 mTvView.reset(); 446 mCurrentChannel = null; 447 mRecordedProgram = null; 448 mInputInfo = null; 449 mCanReceiveInputEvent = false; 450 mOnTuneListener = null; 451 setTimeShiftAvailable(false); 452 hideScreenByVideoAvailability(VIDEO_UNAVAILABLE_REASON_NOT_TUNED); 453 } 454 setMain()455 public void setMain() { 456 mTvView.setMain(); 457 } 458 setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager)459 public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) { 460 mWatchedHistoryManager = watchedHistoryManager; 461 } 462 isPlaying()463 public boolean isPlaying() { 464 return mStarted; 465 } 466 467 /** 468 * Called when parental control is changed. 469 */ onParentalControlChanged(boolean enabled)470 public void onParentalControlChanged(boolean enabled) { 471 mParentControlEnabled = enabled; 472 if (!mParentControlEnabled) { 473 mBlockScreenForTuneView.setVisibility(View.GONE); 474 } 475 } 476 477 /** 478 * Returns {@code true}, if this view is the recording playback mode. 479 */ isRecordingPlayback()480 public boolean isRecordingPlayback() { 481 return mRecordedProgram != null; 482 } 483 484 /** 485 * Returns the recording which is being played right now. 486 */ getPlayingRecordedProgram()487 public RecordedProgram getPlayingRecordedProgram() { 488 return mRecordedProgram; 489 } 490 491 /** 492 * Plays a recording. 493 */ playRecording(Uri recordingUri, OnTuneListener listener)494 public boolean playRecording(Uri recordingUri, OnTuneListener listener) { 495 if (!mStarted) { 496 throw new IllegalStateException("TvView isn't started"); 497 } 498 if (!CommonFeatures.DVR.isEnabled(getContext()) || !BuildCompat.isAtLeastN()) { 499 return false; 500 } 501 if (DEBUG) Log.d(TAG, "playRecording " + recordingUri); 502 long recordingId = ContentUris.parseId(recordingUri); 503 mRecordedProgram = mDvrDataManager.getRecordedProgram(recordingId); 504 if (mRecordedProgram == null) { 505 Log.w(TAG, "No recorded program (Uri=" + recordingUri + ")"); 506 return false; 507 } 508 String inputId = mRecordedProgram.getInputId(); 509 TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(inputId); 510 if (inputInfo == null) { 511 return false; 512 } 513 mOnTuneListener = listener; 514 // mCurrentChannel can be null. 515 mCurrentChannel = mChannelDataManager.getChannel(mRecordedProgram.getChannelId()); 516 // For recording playback, input event should not be sent. 517 mCanReceiveInputEvent = false; 518 boolean needSurfaceSizeUpdate = false; 519 if (!inputInfo.equals(mInputInfo)) { 520 mInputInfo = inputInfo; 521 if (DEBUG) { 522 Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: " 523 + mCanReceiveInputEvent); 524 } 525 needSurfaceSizeUpdate = true; 526 } 527 mChannelViewTimer.start(); 528 mVideoWidth = 0; 529 mVideoHeight = 0; 530 mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 531 mVideoFrameRate = 0f; 532 mVideoDisplayAspectRatio = 0f; 533 mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 534 mHasClosedCaption = false; 535 mTvView.setCallback(mCallback); 536 mTimeShiftCurrentPositionMs = INVALID_TIME; 537 mTvView.setTimeShiftPositionCallback(null); 538 setTimeShiftAvailable(false); 539 mTvView.timeShiftPlay(inputId, recordingUri); 540 if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { 541 // When the input is changed, TvView recreates its SurfaceView internally. 542 // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. 543 getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); 544 } 545 hideScreenByVideoAvailability(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); 546 unblockScreenByContentRating(); 547 if (mParentControlEnabled) { 548 mBlockScreenForTuneView.setVisibility(View.VISIBLE); 549 } 550 if (mOnTuneListener != null) { 551 mOnTuneListener.onStreamInfoChanged(this); 552 } 553 return true; 554 } 555 556 /** 557 * Tunes to a channel with the {@code channelId}. 558 * 559 * @param params extra data to send it to TIS and store the data in TIMS. 560 * @return false, if the TV input is not a proper state to tune to a channel. For example, 561 * if the state is disconnected or channelId doesn't exist, it returns false. 562 */ tuneTo(Channel channel, Bundle params, OnTuneListener listener)563 public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) { 564 if (!mStarted) { 565 throw new IllegalStateException("TvView isn't started"); 566 } 567 if (DEBUG) Log.d(TAG, "tuneTo " + channel); 568 TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(channel.getInputId()); 569 if (inputInfo == null) { 570 return false; 571 } 572 if (mCurrentChannel != null) { 573 long duration = mChannelViewTimer.reset(); 574 mTracker.sendChannelViewStop(mCurrentChannel, duration); 575 if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { 576 mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, 577 System.currentTimeMillis(), duration); 578 } 579 } 580 mOnTuneListener = listener; 581 mCurrentChannel = channel; 582 mRecordedProgram = null; 583 boolean tunedByRecommendation = params != null 584 && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null; 585 boolean needSurfaceSizeUpdate = false; 586 if (!inputInfo.equals(mInputInfo)) { 587 mInputInfo = inputInfo; 588 mCanReceiveInputEvent = getContext().getPackageManager().checkPermission( 589 PERMISSION_RECEIVE_INPUT_EVENT, mInputInfo.getServiceInfo().packageName) 590 == PackageManager.PERMISSION_GRANTED; 591 if (DEBUG) { 592 Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: " 593 + mCanReceiveInputEvent); 594 } 595 needSurfaceSizeUpdate = true; 596 } 597 mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation); 598 mChannelViewTimer.start(); 599 mVideoWidth = 0; 600 mVideoHeight = 0; 601 mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 602 mVideoFrameRate = 0f; 603 mVideoDisplayAspectRatio = 0f; 604 mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 605 mHasClosedCaption = false; 606 mTvView.setCallback(mCallback); 607 mTimeShiftCurrentPositionMs = INVALID_TIME; 608 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 609 // To reduce the IPCs, unregister the callback here and register it when necessary. 610 mTvView.setTimeShiftPositionCallback(null); 611 } 612 setTimeShiftAvailable(false); 613 mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params); 614 if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { 615 // When the input is changed, TvView recreates its SurfaceView internally. 616 // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. 617 getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); 618 } 619 hideScreenByVideoAvailability(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); 620 unblockScreenByContentRating(); 621 if (channel.isPassthrough()) { 622 mBlockScreenForTuneView.setVisibility(View.GONE); 623 } else if (mParentControlEnabled) { 624 mBlockScreenForTuneView.setVisibility(View.VISIBLE); 625 } 626 if (mOnTuneListener != null) { 627 mOnTuneListener.onStreamInfoChanged(this); 628 } 629 return true; 630 } 631 632 @Override getCurrentChannel()633 public Channel getCurrentChannel() { 634 return mCurrentChannel; 635 } 636 637 /** 638 * Sets the current channel. Call this method only when setting the current channel without 639 * actually tuning to it. 640 * 641 * @param currentChannel The new current channel to set to. 642 */ setCurrentChannel(Channel currentChannel)643 public void setCurrentChannel(Channel currentChannel) { 644 mCurrentChannel = currentChannel; 645 } 646 setStreamVolume(float volume)647 public void setStreamVolume(float volume) { 648 if (!mStarted) { 649 throw new IllegalStateException("TvView isn't started"); 650 } 651 if (DEBUG) Log.d(TAG, "setStreamVolume " + volume); 652 mVolume = volume; 653 if (!mIsMuted) { 654 mTvView.setStreamVolume(volume); 655 } 656 } 657 658 /** 659 * Sets fixed size for the internal {@link android.view.Surface} of 660 * {@link android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, 661 * the {@link android.view.Surface}'s size will be matched to the layout. 662 * 663 * Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, 664 * {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size 665 * of {@link android.view.SurfaceView} is changed without changing either left position or top 666 * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow(). 667 */ setFixedSurfaceSize(int width, int height)668 public void setFixedSurfaceSize(int width, int height) { 669 mFixedSurfaceWidth = width; 670 mFixedSurfaceHeight = height; 671 if (mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { 672 // When the input is changed, TvView recreates its SurfaceView internally. 673 // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. 674 SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0); 675 surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); 676 } else { 677 SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0); 678 surfaceView.getHolder().setSizeFromLayout(); 679 } 680 } 681 682 @Override dispatchKeyEvent(KeyEvent event)683 public boolean dispatchKeyEvent(KeyEvent event) { 684 return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event); 685 } 686 687 @Override dispatchTouchEvent(MotionEvent event)688 public boolean dispatchTouchEvent(MotionEvent event) { 689 return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event); 690 } 691 692 @Override dispatchTrackballEvent(MotionEvent event)693 public boolean dispatchTrackballEvent(MotionEvent event) { 694 return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event); 695 } 696 697 @Override dispatchGenericMotionEvent(MotionEvent event)698 public boolean dispatchGenericMotionEvent(MotionEvent event) { 699 return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event); 700 } 701 702 public interface OnTuneListener { onTuneFailed(Channel channel)703 void onTuneFailed(Channel channel); onUnexpectedStop(Channel channel)704 void onUnexpectedStop(Channel channel); onStreamInfoChanged(StreamInfo info)705 void onStreamInfoChanged(StreamInfo info); onChannelRetuned(Uri channel)706 void onChannelRetuned(Uri channel); onContentBlocked()707 void onContentBlocked(); onContentAllowed()708 void onContentAllowed(); 709 } 710 unblockContent(TvContentRating rating)711 public void unblockContent(TvContentRating rating) { 712 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 713 try { 714 Method method = TvView.class.getMethod("requestUnblockContent", 715 TvContentRating.class); 716 method.invoke(mTvView, rating); 717 } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) { 718 e.printStackTrace(); 719 } 720 } else { 721 mTvView.unblockContent(rating); 722 } 723 } 724 725 @Override getVideoWidth()726 public int getVideoWidth() { 727 return mVideoWidth; 728 } 729 730 @Override getVideoHeight()731 public int getVideoHeight() { 732 return mVideoHeight; 733 } 734 735 @Override getVideoDefinitionLevel()736 public int getVideoDefinitionLevel() { 737 return mVideoFormat; 738 } 739 740 @Override getVideoFrameRate()741 public float getVideoFrameRate() { 742 return mVideoFrameRate; 743 } 744 745 /** 746 * Returns displayed aspect ratio (video width / video height * pixel ratio). 747 */ 748 @Override getVideoDisplayAspectRatio()749 public float getVideoDisplayAspectRatio() { 750 return mVideoDisplayAspectRatio; 751 } 752 753 @Override getAudioChannelCount()754 public int getAudioChannelCount() { 755 return mAudioChannelCount; 756 } 757 758 @Override hasClosedCaption()759 public boolean hasClosedCaption() { 760 return mHasClosedCaption; 761 } 762 763 @Override isVideoAvailable()764 public boolean isVideoAvailable() { 765 return mVideoAvailable; 766 } 767 768 @Override getVideoUnavailableReason()769 public int getVideoUnavailableReason() { 770 return mVideoUnavailableReason; 771 } 772 773 /** 774 * Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. 775 */ getSurfaceView()776 private SurfaceView getSurfaceView() { 777 return (SurfaceView) mTvView.getChildAt(0); 778 } 779 setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener)780 public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { 781 mTvView.setOnUnhandledInputEventListener(listener); 782 } 783 setClosedCaptionEnabled(boolean enabled)784 public void setClosedCaptionEnabled(boolean enabled) { 785 mTvView.setCaptionEnabled(enabled); 786 } 787 getTracks(int type)788 public List<TvTrackInfo> getTracks(int type) { 789 return mTvView.getTracks(type); 790 } 791 getSelectedTrack(int type)792 public String getSelectedTrack(int type) { 793 return mTvView.getSelectedTrack(type); 794 } 795 selectTrack(int type, String trackId)796 public void selectTrack(int type, String trackId) { 797 mTvView.selectTrack(type, trackId); 798 } 799 800 /** 801 * Returns if the screen is blocked by {@link #blockScreen()}. 802 */ isScreenBlocked()803 public boolean isScreenBlocked() { 804 return mScreenBlocked; 805 } 806 setOnScreenBlockedListener(OnScreenBlockingChangedListener listener)807 public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) { 808 mOnScreenBlockedListener = listener; 809 } 810 811 /** 812 * Returns currently blocked content rating. {@code null} if it's not blocked. 813 */ getBlockedContentRating()814 public TvContentRating getBlockedContentRating() { 815 return mBlockedContentRating; 816 } 817 818 /** 819 * Locks current TV screen and mutes. 820 * There would be black screen with lock icon in order to show that 821 * screen block is intended and not an error. 822 * TODO: Accept parameter to show lock icon or not. 823 */ blockScreen()824 public void blockScreen() { 825 mScreenBlocked = true; 826 checkBlockScreenAndMuteNeeded(); 827 if (mOnScreenBlockedListener != null) { 828 mOnScreenBlockedListener.onScreenBlockingChanged(true); 829 } 830 } 831 blockScreenByContentRating(TvContentRating rating)832 private void blockScreenByContentRating(TvContentRating rating) { 833 mBlockedContentRating = rating; 834 checkBlockScreenAndMuteNeeded(); 835 } 836 837 @Override 838 @SuppressLint("RtlHardcoded") onLayout(boolean changed, int left, int top, int right, int bottom)839 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 840 super.onLayout(changed, left, top, right, bottom); 841 if (mIsPip) { 842 int height = bottom - top; 843 float scale; 844 if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) { 845 scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight; 846 } else { 847 scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight; 848 } 849 // TODO: need to get UX confirmation. 850 mBlockScreenView.scaleContainerView(scale); 851 } 852 } 853 854 @Override setLayoutParams(ViewGroup.LayoutParams params)855 public void setLayoutParams(ViewGroup.LayoutParams params) { 856 super.setLayoutParams(params); 857 if (mTvView != null) { 858 copyLayoutParamsToTvView(); 859 } 860 } 861 copyLayoutParamsToTvView()862 private void copyLayoutParamsToTvView() { 863 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 864 FrameLayout.LayoutParams tvViewLp = (FrameLayout.LayoutParams) mTvView.getLayoutParams(); 865 if (tvViewLp.bottomMargin != lp.bottomMargin 866 || tvViewLp.topMargin != lp.topMargin 867 || tvViewLp.leftMargin != lp.leftMargin 868 || tvViewLp.rightMargin != lp.rightMargin 869 || tvViewLp.gravity != lp.gravity 870 || tvViewLp.height != lp.height 871 || tvViewLp.width != lp.width) { 872 if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin) { 873 // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is 874 // used, SurfaceView doesn't catch the width and height change. It causes a bug that 875 // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for 876 // small size PIP as a workaround. 877 tvViewLp.leftMargin = lp.leftMargin + 1; 878 } else { 879 tvViewLp.leftMargin = lp.leftMargin; 880 } 881 tvViewLp.topMargin = lp.topMargin; 882 tvViewLp.bottomMargin = lp.bottomMargin; 883 tvViewLp.rightMargin = lp.rightMargin; 884 tvViewLp.gravity = lp.gravity; 885 tvViewLp.height = lp.height; 886 tvViewLp.width = lp.width; 887 mTvView.setLayoutParams(tvViewLp); 888 } 889 } 890 891 @Override onVisibilityChanged(View changedView, int visibility)892 protected void onVisibilityChanged(View changedView, int visibility) { 893 super.onVisibilityChanged(changedView, visibility); 894 if (mTvView != null) { 895 mTvView.setVisibility(visibility); 896 } 897 } 898 899 /** 900 * Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the 901 * block screen will not show any description such as a lock icon and a text for the blocked 902 * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block screen 903 * will show the description for shrunken tv view (Small icon and short text), and if 904 * {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the 905 * description for normal tv view (Big icon and long text). 906 * 907 * @param type The type of block screen to set. 908 */ setBlockScreenType(@lockScreenType int type)909 public void setBlockScreenType(@BlockScreenType int type) { 910 // TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse. 911 if (mBlockScreenType != type) { 912 mBlockScreenType = type; 913 updateBlockScreenUI(true); 914 } 915 } 916 updateBlockScreenUI(boolean animation)917 private void updateBlockScreenUI(boolean animation) { 918 mBlockScreenView.endAnimations(); 919 920 if (!mScreenBlocked && mBlockedContentRating == null) { 921 mBlockScreenView.setVisibility(GONE); 922 return; 923 } 924 925 mBlockScreenView.setVisibility(VISIBLE); 926 if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) { 927 adjustBlockScreenSpacingAndText(); 928 } 929 mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation); 930 } 931 adjustBlockScreenSpacingAndText()932 private void adjustBlockScreenSpacingAndText() { 933 // TODO: need to add animation for padding change when the block screen type is changed 934 // NORMAL to SHRUNKEN and vice verse. 935 mBlockScreenView.setSpacing(mBlockScreenType); 936 String text = getBlockScreenText(); 937 if (text != null) { 938 mBlockScreenView.setText(text); 939 } 940 } 941 942 /** 943 * Returns the block screen text corresponding to the current status. 944 * Note that returning {@code null} value means that the current text should not be changed. 945 */ getBlockScreenText()946 private String getBlockScreenText() { 947 if (mScreenBlocked) { 948 switch (mBlockScreenType) { 949 case BLOCK_SCREEN_TYPE_NO_UI: 950 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: 951 return ""; 952 case BLOCK_SCREEN_TYPE_NORMAL: 953 if (mCanModifyParentalControls) { 954 return getResources().getString(R.string.tvview_channel_locked); 955 } else { 956 return getResources().getString( 957 R.string.tvview_channel_locked_no_permission); 958 } 959 } 960 } else if (mBlockedContentRating != null) { 961 String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating); 962 switch (mBlockScreenType) { 963 case BLOCK_SCREEN_TYPE_NO_UI: 964 return ""; 965 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: 966 if (TextUtils.isEmpty(name)) { 967 return getResources().getString(R.string.shrunken_tvview_content_locked); 968 } else { 969 return getContext().getString( 970 R.string.shrunken_tvview_content_locked_format, name); 971 } 972 case BLOCK_SCREEN_TYPE_NORMAL: 973 if (TextUtils.isEmpty(name)) { 974 if (mCanModifyParentalControls) { 975 return getResources().getString(R.string.tvview_content_locked); 976 } else { 977 return getResources().getString( 978 R.string.tvview_content_locked_no_permission); 979 } 980 } else { 981 if (mCanModifyParentalControls) { 982 return getContext().getString( 983 R.string.tvview_content_locked_format, name); 984 } else { 985 return getContext().getString( 986 R.string.tvview_content_locked_format_no_permission, name); 987 } 988 } 989 } 990 } 991 return null; 992 } 993 checkBlockScreenAndMuteNeeded()994 private void checkBlockScreenAndMuteNeeded() { 995 updateBlockScreenUI(false); 996 if (mScreenBlocked || mBlockedContentRating != null) { 997 mute(); 998 if (mIsPip) { 999 // If we don't make mTvView invisible, some frames are leaked when a user changes 1000 // PIP layout in options. 1001 // Note: When video is unavailable, we keep the mTvView's visibility, because 1002 // TIS implementation may not send video available with no surface. 1003 mTvView.setVisibility(View.INVISIBLE); 1004 } 1005 } else { 1006 unmuteIfPossible(); 1007 if (mIsPip) { 1008 mTvView.setVisibility(View.VISIBLE); 1009 } 1010 } 1011 } 1012 unblockScreen()1013 public void unblockScreen() { 1014 mScreenBlocked = false; 1015 checkBlockScreenAndMuteNeeded(); 1016 if (mOnScreenBlockedListener != null) { 1017 mOnScreenBlockedListener.onScreenBlockingChanged(false); 1018 } 1019 } 1020 unblockScreenByContentRating()1021 private void unblockScreenByContentRating() { 1022 mBlockedContentRating = null; 1023 checkBlockScreenAndMuteNeeded(); 1024 } 1025 1026 @UiThread hideScreenByVideoAvailability(int reason)1027 private void hideScreenByVideoAvailability(int reason) { 1028 mVideoAvailable = false; 1029 mVideoUnavailableReason = reason; 1030 if (mInternetCheckTask != null) { 1031 mInternetCheckTask.cancel(true); 1032 mInternetCheckTask = null; 1033 } 1034 switch (reason) { 1035 case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: 1036 mHideScreenView.setVisibility(VISIBLE); 1037 mHideScreenView.setImageVisibility(false); 1038 mHideScreenView.setText(R.string.tvview_msg_audio_only); 1039 mBufferingSpinnerView.setVisibility(GONE); 1040 unmuteIfPossible(); 1041 break; 1042 case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: 1043 mBufferingSpinnerView.setVisibility(VISIBLE); 1044 mute(); 1045 break; 1046 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: 1047 mHideScreenView.setVisibility(VISIBLE); 1048 mHideScreenView.setText(R.string.tvview_msg_weak_signal); 1049 mBufferingSpinnerView.setVisibility(GONE); 1050 mute(); 1051 break; 1052 case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: 1053 case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: 1054 mHideScreenView.setVisibility(VISIBLE); 1055 mHideScreenView.setImageVisibility(false); 1056 mHideScreenView.setText(null); 1057 mBufferingSpinnerView.setVisibility(GONE); 1058 mute(); 1059 break; 1060 case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: 1061 default: 1062 mHideScreenView.setVisibility(VISIBLE); 1063 mHideScreenView.setImageVisibility(false); 1064 mHideScreenView.setText(null); 1065 mBufferingSpinnerView.setVisibility(GONE); 1066 mute(); 1067 if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) { 1068 mInternetCheckTask = new InternetCheckTask(); 1069 mInternetCheckTask.execute(); 1070 } 1071 break; 1072 } 1073 } 1074 unhideScreenByVideoAvailability()1075 private void unhideScreenByVideoAvailability() { 1076 mVideoAvailable = true; 1077 mHideScreenView.setVisibility(GONE); 1078 mBufferingSpinnerView.setVisibility(GONE); 1079 unmuteIfPossible(); 1080 } 1081 unmuteIfPossible()1082 private void unmuteIfPossible() { 1083 if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) { 1084 unmute(); 1085 } 1086 } 1087 mute()1088 private void mute() { 1089 mIsMuted = true; 1090 mTvView.setStreamVolume(0); 1091 } 1092 unmute()1093 private void unmute() { 1094 mIsMuted = false; 1095 mTvView.setStreamVolume(mVolume); 1096 } 1097 1098 /** Returns true if this view is faded out. */ isFadedOut()1099 public boolean isFadedOut() { 1100 return mFadeState == FADED_OUT; 1101 } 1102 1103 /** Fade out this TunableTvView. Fade out by increasing the dimming. */ fadeOut(int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1104 public void fadeOut(int durationMillis, TimeInterpolator interpolator, 1105 final Runnable actionAfterFade) { 1106 mDimScreenView.setAlpha(0f); 1107 mDimScreenView.setVisibility(View.VISIBLE); 1108 mDimScreenView.animate() 1109 .alpha(1f) 1110 .setDuration(durationMillis) 1111 .setInterpolator(interpolator) 1112 .withStartAction(new Runnable() { 1113 @Override 1114 public void run() { 1115 mFadeState = FADING_OUT; 1116 mActionAfterFade = actionAfterFade; 1117 } 1118 }) 1119 .withEndAction(new Runnable() { 1120 @Override 1121 public void run() { 1122 mFadeState = FADED_OUT; 1123 } 1124 }); 1125 } 1126 1127 /** Fade in this TunableTvView. Fade in by decreasing the dimming. */ fadeIn(int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1128 public void fadeIn(int durationMillis, TimeInterpolator interpolator, 1129 final Runnable actionAfterFade) { 1130 mDimScreenView.setAlpha(1f); 1131 mDimScreenView.setVisibility(View.VISIBLE); 1132 mDimScreenView.animate() 1133 .alpha(0f) 1134 .setDuration(durationMillis) 1135 .setInterpolator(interpolator) 1136 .withStartAction(new Runnable() { 1137 @Override 1138 public void run() { 1139 mFadeState = FADING_IN; 1140 mActionAfterFade = actionAfterFade; 1141 } 1142 }) 1143 .withEndAction(new Runnable() { 1144 @Override 1145 public void run() { 1146 mFadeState = FADED_IN; 1147 mDimScreenView.setVisibility(View.GONE); 1148 } 1149 }); 1150 } 1151 1152 /** Remove the fade effect. */ removeFadeEffect()1153 public void removeFadeEffect() { 1154 mDimScreenView.animate().cancel(); 1155 mDimScreenView.setVisibility(View.GONE); 1156 mFadeState = FADED_IN; 1157 } 1158 1159 /** 1160 * Sets the TimeShiftListener 1161 * 1162 * @param listener The instance of {@link TimeShiftListener}. 1163 */ setTimeShiftListener(TimeShiftListener listener)1164 public void setTimeShiftListener(TimeShiftListener listener) { 1165 mTimeShiftListener = listener; 1166 } 1167 setTimeShiftAvailable(boolean isTimeShiftAvailable)1168 private void setTimeShiftAvailable(boolean isTimeShiftAvailable) { 1169 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || mTimeShiftAvailable == isTimeShiftAvailable) { 1170 return; 1171 } 1172 mTimeShiftAvailable = isTimeShiftAvailable; 1173 if (isTimeShiftAvailable) { 1174 mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { 1175 @Override 1176 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { 1177 if (mTimeShiftListener != null && mCurrentChannel != null 1178 && mCurrentChannel.getInputId().equals(inputId)) { 1179 mTimeShiftListener.onRecordStartTimeChanged(timeMs); 1180 } 1181 } 1182 1183 @Override 1184 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { 1185 mTimeShiftCurrentPositionMs = timeMs; 1186 } 1187 }); 1188 } else { 1189 mTvView.setTimeShiftPositionCallback(null); 1190 } 1191 if (mTimeShiftListener != null) { 1192 mTimeShiftListener.onAvailabilityChanged(); 1193 } 1194 } 1195 1196 /** 1197 * Returns if the time shift is available for the current channel. 1198 */ isTimeShiftAvailable()1199 public boolean isTimeShiftAvailable() { 1200 return mTimeShiftAvailable; 1201 } 1202 1203 /** 1204 * Returns the current time-shift state. It returns one of {@link #TIME_SHIFT_STATE_NONE}, 1205 * {@link #TIME_SHIFT_STATE_PLAY}, {@link #TIME_SHIFT_STATE_PAUSE}, 1206 * {@link #TIME_SHIFT_STATE_REWIND}, {@link #TIME_SHIFT_STATE_FAST_FORWARD} 1207 * or {@link #TIME_SHIFT_STATE_PAUSE}. 1208 */ getTimeShiftState()1209 @TimeShiftState public int getTimeShiftState() { 1210 return mTimeShiftState; 1211 } 1212 1213 /** 1214 * Plays the media, if the current input supports time-shifting. 1215 */ timeshiftPlay()1216 public void timeshiftPlay() { 1217 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1218 Log.w(TAG, "Time shifting is not supported in this platform."); 1219 return; 1220 } 1221 if (!isTimeShiftAvailable()) { 1222 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1223 } 1224 if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) { 1225 return; 1226 } 1227 mTvView.timeShiftResume(); 1228 } 1229 1230 /** 1231 * Pauses the media, if the current input supports time-shifting. 1232 */ timeshiftPause()1233 public void timeshiftPause() { 1234 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1235 Log.w(TAG, "Time shifting is not supported in this platform."); 1236 return; 1237 } 1238 if (!isTimeShiftAvailable()) { 1239 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1240 } 1241 if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) { 1242 return; 1243 } 1244 mTvView.timeShiftPause(); 1245 } 1246 1247 /** 1248 * Rewinds the media with the given speed, if the current input supports time-shifting. 1249 * 1250 * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. 1251 */ timeshiftRewind(int speed)1252 public void timeshiftRewind(int speed) { 1253 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1254 Log.w(TAG, "Time shifting is not supported in this platform."); 1255 } else if (!isTimeShiftAvailable()) { 1256 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1257 } else { 1258 if (speed <= 0) { 1259 throw new IllegalArgumentException("The speed should be a positive integer."); 1260 } 1261 mTimeShiftState = TIME_SHIFT_STATE_REWIND; 1262 PlaybackParams params = new PlaybackParams(); 1263 params.setSpeed(speed * -1); 1264 mTvView.timeShiftSetPlaybackParams(params); 1265 } 1266 } 1267 1268 /** 1269 * Fast-forwards the media with the given speed, if the current input supports time-shifting. 1270 * 1271 * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. 1272 */ timeshiftFastForward(int speed)1273 public void timeshiftFastForward(int speed) { 1274 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1275 Log.w(TAG, "Time shifting is not supported in this platform."); 1276 } else if (!isTimeShiftAvailable()) { 1277 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1278 } else { 1279 if (speed <= 0) { 1280 throw new IllegalArgumentException("The speed should be a positive integer."); 1281 } 1282 mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD; 1283 PlaybackParams params = new PlaybackParams(); 1284 params.setSpeed(speed); 1285 mTvView.timeShiftSetPlaybackParams(params); 1286 } 1287 } 1288 1289 /** 1290 * Seek to the given time position. 1291 * 1292 * @param timeMs The time in milliseconds to seek to. 1293 */ timeshiftSeekTo(long timeMs)1294 public void timeshiftSeekTo(long timeMs) { 1295 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1296 Log.w(TAG, "Time shifting is not supported in this platform."); 1297 return; 1298 } 1299 if (!isTimeShiftAvailable()) { 1300 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1301 } 1302 mTvView.timeShiftSeekTo(timeMs); 1303 } 1304 1305 /** 1306 * Returns the current playback position in milliseconds. 1307 */ timeshiftGetCurrentPositionMs()1308 public long timeshiftGetCurrentPositionMs() { 1309 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1310 Log.w(TAG, "Time shifting is not supported in this platform."); 1311 return INVALID_TIME; 1312 } 1313 if (!isTimeShiftAvailable()) { 1314 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1315 } 1316 if (DEBUG) { 1317 Log.d(TAG, "timeshiftGetCurrentPositionMs: current position =" 1318 + Utils.toTimeString(mTimeShiftCurrentPositionMs)); 1319 } 1320 return mTimeShiftCurrentPositionMs; 1321 } 1322 1323 /** 1324 * Used to receive the time-shift events. 1325 */ 1326 public static abstract class TimeShiftListener { 1327 /** 1328 * Called when the availability of the time-shift for the current channel has been changed. 1329 * It should be guaranteed that this is called only when the availability is really changed. 1330 */ onAvailabilityChanged()1331 public abstract void onAvailabilityChanged(); 1332 1333 /** 1334 * Called when the record start time has been changed. 1335 */ onRecordStartTimeChanged(long recordStartTimeMs)1336 public abstract void onRecordStartTimeChanged(long recordStartTimeMs); 1337 } 1338 1339 /** 1340 * A listener which receives the notification when the screen is blocked/unblocked. 1341 */ 1342 public static abstract class OnScreenBlockingChangedListener { 1343 /** 1344 * Called when the screen is blocked/unblocked. 1345 */ onScreenBlockingChanged(boolean blocked)1346 public abstract void onScreenBlockingChanged(boolean blocked); 1347 } 1348 1349 public class InternetCheckTask extends AsyncTask<Void, Void, Boolean> { 1350 @Override doInBackground(Void... params)1351 protected Boolean doInBackground(Void... params) { 1352 return NetworkUtils.isNetworkAvailable(mConnectivityManager); 1353 } 1354 1355 @Override onPostExecute(Boolean networkAvailable)1356 protected void onPostExecute(Boolean networkAvailable) { 1357 mInternetCheckTask = null; 1358 if (!mVideoAvailable && !networkAvailable && isAttachedToWindow() 1359 && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) { 1360 mHideScreenView.setImageVisibility(true); 1361 mHideScreenView.setImage(R.drawable.ic_sad_cloud); 1362 mHideScreenView.setText(R.string.tvview_msg_no_internet_connection); 1363 } 1364 } 1365 } 1366 } 1367