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