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.TargetApi; 23 import android.app.Activity; 24 import android.content.Context; 25 import android.content.pm.PackageManager; 26 import android.content.res.Resources; 27 import android.graphics.Bitmap; 28 import android.graphics.PorterDuff; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.media.PlaybackParams; 32 import android.media.tv.AitInfo; 33 import android.media.tv.TvContentRating; 34 import android.media.tv.TvInputInfo; 35 import android.media.tv.TvInputManager; 36 import android.media.tv.TvTrackInfo; 37 import android.media.tv.TvView; 38 import android.media.tv.TvView.OnUnhandledInputEventListener; 39 import android.media.tv.interactive.TvInteractiveAppView; 40 import android.net.ConnectivityManager; 41 import android.net.Uri; 42 import android.os.AsyncTask; 43 import android.os.Build; 44 import android.os.Bundle; 45 import android.support.annotation.IntDef; 46 import android.support.annotation.NonNull; 47 import android.support.annotation.Nullable; 48 import android.support.annotation.VisibleForTesting; 49 import android.text.TextUtils; 50 import android.text.format.DateUtils; 51 import android.util.AttributeSet; 52 import android.util.Log; 53 import android.view.KeyEvent; 54 import android.view.MotionEvent; 55 import android.view.SurfaceView; 56 import android.view.View; 57 import android.view.accessibility.AccessibilityManager; 58 import android.widget.FrameLayout; 59 import android.widget.ImageView; 60 61 import com.android.tv.InputSessionManager; 62 import com.android.tv.InputSessionManager.TvViewSession; 63 import com.android.tv.R; 64 import com.android.tv.TvSingletons; 65 import com.android.tv.analytics.Tracker; 66 import com.android.tv.common.BuildConfig; 67 import com.android.tv.common.CommonConstants; 68 import com.android.tv.common.compat.TvInputConstantCompat; 69 import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; 70 import com.android.tv.common.feature.CommonFeatures; 71 import com.android.tv.common.util.CommonUtils; 72 import com.android.tv.common.util.Debug; 73 import com.android.tv.common.util.DurationTimer; 74 import com.android.tv.common.util.PermissionUtils; 75 import com.android.tv.data.ProgramDataManager; 76 import com.android.tv.data.StreamInfo; 77 import com.android.tv.data.WatchedHistoryManager; 78 import com.android.tv.data.api.Channel; 79 import com.android.tv.data.api.Program; 80 import com.android.tv.features.TvFeatures; 81 import com.android.tv.parental.ContentRatingsManager; 82 import com.android.tv.parental.ParentalControlSettings; 83 import com.android.tv.recommendation.NotificationService; 84 import com.android.tv.ui.api.TunableTvViewPlayingApi; 85 import com.android.tv.util.NetworkUtils; 86 import com.android.tv.util.TvInputManagerHelper; 87 import com.android.tv.util.Utils; 88 import com.android.tv.util.images.ImageLoader; 89 90 import com.android.tv.common.flags.LegacyFlags; 91 92 import java.lang.annotation.Retention; 93 import java.lang.annotation.RetentionPolicy; 94 import java.util.List; 95 96 /** Includes the real {@link AppLayerTvView} handling tuning, block and other display events. */ 97 public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvViewPlayingApi { 98 private static final boolean DEBUG = false; 99 private static final String TAG = "TunableTvView"; 100 101 public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1; 102 public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2; 103 public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3; 104 public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100; 105 private final AccessibilityManager mAccessibilityManager; 106 107 @Retention(RetentionPolicy.SOURCE) 108 @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL}) 109 public @interface BlockScreenType {} 110 111 public static final int BLOCK_SCREEN_TYPE_NO_UI = 0; 112 public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1; 113 public static final int BLOCK_SCREEN_TYPE_NORMAL = 2; 114 115 private static final String PERMISSION_RECEIVE_INPUT_EVENT = 116 CommonConstants.BASE_PACKAGE + ".permission.RECEIVE_INPUT_EVENT"; 117 118 @Retention(RetentionPolicy.SOURCE) 119 @IntDef({ 120 TIME_SHIFT_STATE_NONE, 121 TIME_SHIFT_STATE_PLAY, 122 TIME_SHIFT_STATE_PAUSE, 123 TIME_SHIFT_STATE_REWIND, 124 TIME_SHIFT_STATE_FAST_FORWARD 125 }) 126 private @interface TimeShiftState {} 127 128 private static final int TIME_SHIFT_STATE_NONE = 0; 129 private static final int TIME_SHIFT_STATE_PLAY = 1; 130 private static final int TIME_SHIFT_STATE_PAUSE = 2; 131 private static final int TIME_SHIFT_STATE_REWIND = 3; 132 private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4; 133 134 private static final int FADED_IN = 0; 135 private static final int FADED_OUT = 1; 136 private static final int FADING_IN = 2; 137 private static final int FADING_OUT = 3; 138 139 private AppLayerTvView mTvView; 140 private TvViewSession mTvViewSession; 141 @Nullable private Channel mCurrentChannel; 142 private TvInputManagerHelper mInputManagerHelper; 143 private ContentRatingsManager mContentRatingsManager; 144 private ParentalControlSettings mParentalControlSettings; 145 private ProgramDataManager mProgramDataManager; 146 @Nullable private WatchedHistoryManager mWatchedHistoryManager; 147 private boolean mStarted; 148 private String mTagetInputId; 149 private TvInputInfo mInputInfo; 150 private OnTuneListener mOnTuneListener; 151 private int mVideoWidth; 152 private int mVideoHeight; 153 private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 154 private float mVideoFrameRate; 155 private float mVideoDisplayAspectRatio; 156 private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 157 private boolean mHasClosedCaption = false; 158 private boolean mScreenBlocked; 159 private OnScreenBlockingChangedListener mOnScreenBlockedListener; 160 private TvContentRating mBlockedContentRating; 161 private int mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED; 162 private boolean mCanReceiveInputEvent; 163 private boolean mIsMuted; 164 private float mVolume; 165 private boolean mParentControlEnabled; 166 private int mFixedSurfaceWidth; 167 private int mFixedSurfaceHeight; 168 private final boolean mCanModifyParentalControls; 169 private boolean mIsUnderShrunken; 170 171 @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; 172 private TimeShiftListener mTimeShiftListener; 173 private boolean mTimeShiftAvailable; 174 private long mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 175 176 private final Tracker mTracker; 177 private final DurationTimer mChannelViewTimer = new DurationTimer(); 178 private InternetCheckTask mInternetCheckTask; 179 180 // A block screen view to hide the real TV view underlying. It may be used to enforce parental 181 // control, or hide screen when there's no video available and show appropriate information. 182 private final BlockScreenView mBlockScreenView; 183 private final int mTuningImageColorFilter; 184 185 // A spinner view to show buffering status. 186 private final View mBufferingSpinnerView; 187 188 private final View mDimScreenView; 189 190 private int mFadeState = FADED_IN; 191 private Runnable mActionAfterFade; 192 193 @BlockScreenType private int mBlockScreenType; 194 195 private final TvInputManagerHelper mInputManager; 196 private final ConnectivityManager mConnectivityManager; 197 private final InputSessionManager mInputSessionManager; 198 199 private int mChannelSignalStrength; 200 private TvInteractiveAppView mTvIAppView; 201 202 private final TvInputCallbackCompat mCallback = 203 new TvInputCallbackCompat() { 204 @Override 205 public void onConnectionFailed(String inputId) { 206 Log.w(TAG, "Failed to bind an input"); 207 mTracker.sendInputConnectionFailure(inputId); 208 Channel channel = mCurrentChannel; 209 mCurrentChannel = null; 210 mInputInfo = null; 211 mCanReceiveInputEvent = false; 212 if (mOnTuneListener != null) { 213 // If tune is called inside onTuneFailed, mOnTuneListener will be set to 214 // a new instance. In order to avoid to clear the new mOnTuneListener, 215 // we copy mOnTuneListener to l and clear mOnTuneListener before 216 // calling onTuneFailed. 217 OnTuneListener listener = mOnTuneListener; 218 mOnTuneListener = null; 219 listener.onTuneFailed(channel); 220 } 221 } 222 223 @Override 224 public void onDisconnected(String inputId) { 225 Log.w(TAG, "Session is released by crash"); 226 mTracker.sendInputDisconnected(inputId); 227 Channel channel = mCurrentChannel; 228 mCurrentChannel = null; 229 mInputInfo = null; 230 mCanReceiveInputEvent = false; 231 if (mOnTuneListener != null) { 232 OnTuneListener listener = mOnTuneListener; 233 mOnTuneListener = null; 234 listener.onUnexpectedStop(channel); 235 } 236 } 237 238 @Override 239 public void onChannelRetuned(String inputId, Uri channelUri) { 240 if (DEBUG) { 241 Log.d( 242 TAG, 243 "onChannelRetuned(inputId=" 244 + inputId 245 + ", channelUri=" 246 + channelUri 247 + ")"); 248 } 249 if (mOnTuneListener != null) { 250 mOnTuneListener.onChannelRetuned(channelUri); 251 } 252 } 253 254 @Override 255 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 256 mHasClosedCaption = false; 257 for (TvTrackInfo track : tracks) { 258 if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) { 259 mHasClosedCaption = true; 260 break; 261 } 262 } 263 if (mOnTuneListener != null) { 264 mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true); 265 } 266 } 267 268 @Override 269 public void onTrackSelected(String inputId, int type, String trackId) { 270 if (trackId == null) { 271 // A track is unselected. 272 if (type == TvTrackInfo.TYPE_VIDEO) { 273 mVideoWidth = 0; 274 mVideoHeight = 0; 275 mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 276 mVideoFrameRate = 0f; 277 mVideoDisplayAspectRatio = 0f; 278 } else if (type == TvTrackInfo.TYPE_AUDIO) { 279 mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 280 } 281 } else { 282 List<TvTrackInfo> tracks = getTracks(type); 283 boolean trackFound = false; 284 if (tracks != null) { 285 for (TvTrackInfo track : tracks) { 286 if (track.getId().equals(trackId)) { 287 if (type == TvTrackInfo.TYPE_VIDEO) { 288 mVideoWidth = track.getVideoWidth(); 289 mVideoHeight = track.getVideoHeight(); 290 mVideoFormat = 291 Utils.getVideoDefinitionLevelFromSize( 292 mVideoWidth, mVideoHeight); 293 mVideoFrameRate = track.getVideoFrameRate(); 294 if (mVideoWidth <= 0 || mVideoHeight <= 0) { 295 mVideoDisplayAspectRatio = 0.0f; 296 } else { 297 float videoPixelAspectRatio = 298 track.getVideoPixelAspectRatio(); 299 mVideoDisplayAspectRatio = (float) mVideoWidth 300 / mVideoHeight; 301 mVideoDisplayAspectRatio *= videoPixelAspectRatio > 0 ? 302 videoPixelAspectRatio : 1; 303 } 304 } else if (type == TvTrackInfo.TYPE_AUDIO) { 305 mAudioChannelCount = track.getAudioChannelCount(); 306 } 307 trackFound = true; 308 break; 309 } 310 } 311 } 312 if (!trackFound) { 313 Log.w(TAG, "Invalid track ID: " + trackId); 314 } 315 } 316 if (mOnTuneListener != null) { 317 // should not change audio track automatically when an audio track or a 318 // subtitle track is selected 319 mOnTuneListener.onStreamInfoChanged( 320 TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO); 321 } 322 } 323 324 @Override 325 public void onVideoAvailable(String inputId) { 326 if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}"); 327 Debug.getTimer(Debug.TAG_START_UP_TIMER) 328 .log( 329 "Start up of TV app ends," 330 + " TunableTvView.onVideoAvailable resets timer"); 331 Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); 332 Debug.removeTimer(Debug.TAG_START_UP_TIMER); 333 mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE; 334 updateBlockScreenAndMuting(); 335 if (mOnTuneListener != null) { 336 mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true); 337 } 338 } 339 340 @Override 341 public void onVideoUnavailable(String inputId, int reason) { 342 if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING 343 && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { 344 Debug.getTimer(Debug.TAG_START_UP_TIMER) 345 .log( 346 "TunableTvView.onVideoUnAvailable reason = (" 347 + reason 348 + ") and removes timer"); 349 Debug.removeTimer(Debug.TAG_START_UP_TIMER); 350 } else { 351 Debug.getTimer(Debug.TAG_START_UP_TIMER) 352 .log("TunableTvView.onVideoUnAvailable reason = (" + reason + ")"); 353 } 354 mVideoUnavailableReason = reason; 355 if (closePipIfNeeded()) { 356 return; 357 } 358 updateBlockScreenAndMuting(); 359 if (mOnTuneListener != null) { 360 mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true); 361 } 362 switch (reason) { 363 case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: 364 case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: 365 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: 366 case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED: 367 mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); 368 break; 369 default: 370 // do nothing 371 } 372 } 373 374 @Override 375 public void onContentAllowed(String inputId) { 376 mBlockedContentRating = null; 377 updateBlockScreenAndMuting(); 378 if (mOnTuneListener != null) { 379 mOnTuneListener.onContentAllowed(); 380 } 381 } 382 383 @Override 384 public void onContentBlocked(String inputId, TvContentRating rating) { 385 if (rating != null && rating.equals(mBlockedContentRating)) { 386 return; 387 } 388 mBlockedContentRating = rating; 389 if (closePipIfNeeded()) { 390 return; 391 } 392 updateBlockScreenAndMuting(); 393 if (mOnTuneListener != null) { 394 mOnTuneListener.onContentBlocked(); 395 } 396 } 397 398 @Override 399 public void onTimeShiftStatusChanged(String inputId, int status) { 400 if (DEBUG) { 401 Log.d( 402 TAG, 403 "onTimeShiftStatusChanged: {inputId=" 404 + inputId 405 + ", status=" 406 + status 407 + "}"); 408 } 409 boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; 410 setTimeShiftAvailable(available); 411 } 412 413 @Override 414 public void onSignalStrength(String inputId, int value) { 415 mChannelSignalStrength = value; 416 if (mOnTuneListener != null) { 417 mOnTuneListener.onChannelSignalStrength(); 418 } 419 } 420 421 @TargetApi(Build.VERSION_CODES.TIRAMISU) 422 @Override 423 public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { 424 if (!TvFeatures.HAS_TIAF.isEnabled(getContext())) { 425 return; 426 } 427 if (DEBUG) { 428 Log.d(TAG, 429 "onAitInfoUpdated: {inputId=" 430 + inputId 431 + ", AitInfo=(" 432 + aitInfo 433 +")}"); 434 } 435 if (mOnTuneListener != null) { 436 mOnTuneListener.onAitInfoUpdated(inputId, aitInfo); 437 } 438 } 439 }; 440 TunableTvView(Context context)441 public TunableTvView(Context context) { 442 this(context, null); 443 } 444 TunableTvView(Context context, AttributeSet attrs)445 public TunableTvView(Context context, AttributeSet attrs) { 446 this(context, attrs, 0); 447 } 448 TunableTvView(Context context, AttributeSet attrs, int defStyleAttr)449 public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) { 450 this(context, attrs, defStyleAttr, 0); 451 } 452 TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)453 public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 454 super(context, attrs, defStyleAttr, defStyleRes); 455 inflate(getContext(), R.layout.tunable_tv_view, this); 456 457 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 458 if (CommonFeatures.DVR.isEnabled(context)) { 459 mInputSessionManager = tvSingletons.getInputSessionManager(); 460 } else { 461 mInputSessionManager = null; 462 } 463 mInputManager = tvSingletons.getTvInputManagerHelper(); 464 mConnectivityManager = 465 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 466 mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); 467 mTracker = tvSingletons.getTracker(); 468 mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; 469 mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen); 470 mBlockScreenView.addInfoFadeInAnimationListener( 471 new AnimatorListenerAdapter() { 472 @Override 473 public void onAnimationStart(Animator animation) { 474 adjustBlockScreenSpacingAndText(); 475 } 476 }); 477 478 mBufferingSpinnerView = findViewById(R.id.buffering_spinner); 479 mTuningImageColorFilter = 480 getResources().getColor(R.color.tvview_block_image_color_filter, null); 481 mDimScreenView = findViewById(R.id.dim_screen); 482 mDimScreenView 483 .animate() 484 .setListener( 485 new AnimatorListenerAdapter() { 486 @Override 487 public void onAnimationEnd(Animator animation) { 488 if (mActionAfterFade != null) { 489 mActionAfterFade.run(); 490 } 491 } 492 493 @Override 494 public void onAnimationCancel(Animator animation) { 495 if (mActionAfterFade != null) { 496 mActionAfterFade.run(); 497 } 498 } 499 }); 500 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 501 } initialize( ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper, LegacyFlags legacyFlags)502 public void initialize( 503 ProgramDataManager programDataManager, 504 TvInputManagerHelper tvInputManagerHelper, 505 LegacyFlags legacyFlags) { 506 initialize(programDataManager, tvInputManagerHelper, legacyFlags, null); 507 } 508 initialize( ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper, LegacyFlags legacyFlags, TvInteractiveAppView tvIAppView)509 public void initialize( 510 ProgramDataManager programDataManager, 511 TvInputManagerHelper tvInputManagerHelper, 512 LegacyFlags legacyFlags, 513 TvInteractiveAppView tvIAppView) { 514 mTvView = findViewById(R.id.tv_view); 515 mTvView.setUseSecureSurface(!BuildConfig.ENG && !legacyFlags.enableDeveloperFeatures()); 516 517 mProgramDataManager = programDataManager; 518 mInputManagerHelper = tvInputManagerHelper; 519 mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); 520 mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings(); 521 mTvIAppView = tvIAppView; 522 if (mInputSessionManager != null) { 523 mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, this, mCallback); 524 } else { 525 mTvView.setCallback(mCallback); 526 } 527 } 528 start()529 public void start() { 530 mStarted = true; 531 } 532 533 /** Warms up the input to reduce the start time. */ warmUpInput(String inputId, Uri channelUri)534 public void warmUpInput(String inputId, Uri channelUri) { 535 if (!mStarted && inputId != null && channelUri != null) { 536 if (mTvViewSession != null) { 537 mTvViewSession.tune(inputId, channelUri); 538 } else { 539 mTvView.tune(inputId, channelUri); 540 } 541 mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING; 542 updateBlockScreenAndMuting(); 543 } 544 } 545 stop()546 public void stop() { 547 if (!mStarted) { 548 return; 549 } 550 mStarted = false; 551 if (mCurrentChannel != null) { 552 long duration = mChannelViewTimer.reset(); 553 mTracker.sendChannelViewStop(mCurrentChannel, duration); 554 if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { 555 mWatchedHistoryManager.logChannelViewStop( 556 mCurrentChannel, System.currentTimeMillis(), duration); 557 } 558 } 559 reset(); 560 } 561 562 /** Releases the resources. */ release()563 public void release() { 564 if (mInputSessionManager != null) { 565 mInputSessionManager.releaseTvViewSession(mTvViewSession); 566 mTvViewSession = null; 567 } 568 } 569 570 /** Resets TV view. */ reset()571 public void reset() { 572 resetInternal(); 573 mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED; 574 updateBlockScreenAndMuting(); 575 } 576 577 /** Resets TV view to acquire the recording session. */ resetByRecording()578 public void resetByRecording() { 579 resetInternal(); 580 } 581 resetInternal()582 private void resetInternal() { 583 if (mTvViewSession != null) { 584 mTvViewSession.reset(); 585 } else { 586 mTvView.reset(); 587 } 588 mCurrentChannel = null; 589 mInputInfo = null; 590 mCanReceiveInputEvent = false; 591 mOnTuneListener = null; 592 setTimeShiftAvailable(false); 593 } 594 setMain()595 public void setMain() { 596 if (PermissionUtils.hasChangeHdmiCecActiveSource(getContext())) { 597 mTvView.setMain(); 598 } 599 } 600 setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager)601 public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) { 602 mWatchedHistoryManager = watchedHistoryManager; 603 } 604 605 /** Sets if the TunableTvView is under shrunken. */ setIsUnderShrunken(boolean isUnderShrunken)606 public void setIsUnderShrunken(boolean isUnderShrunken) { 607 mIsUnderShrunken = isUnderShrunken; 608 } 609 getChannelSignalStrength()610 public int getChannelSignalStrength() { 611 return mChannelSignalStrength; 612 } 613 resetChannelSignalStrength()614 public void resetChannelSignalStrength() { 615 mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED; 616 } 617 618 @Override isPlaying()619 public boolean isPlaying() { 620 return mStarted; 621 } 622 623 /** Called when parental control is changed. */ onParentalControlChanged(boolean enabled)624 public void onParentalControlChanged(boolean enabled) { 625 mParentControlEnabled = enabled; 626 if (!enabled) { 627 // Unblock screen immediately if parental control is turned off 628 updateBlockScreenAndMuting(); 629 } 630 } 631 632 /** 633 * Tunes to a channel with the {@code channelId}. 634 * 635 * @param params extra data to send it to TIS and store the data in TIMS. 636 * @return false, if the TV input is not a proper state to tune to a channel. For example, if 637 * the state is disconnected or channelId doesn't exist, it returns false. 638 */ tuneTo(Channel channel, Bundle params, OnTuneListener listener)639 public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) { 640 Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo"); 641 if (!mStarted) { 642 throw new IllegalStateException("TvView isn't started"); 643 } 644 if (DEBUG) Log.d(TAG, "tuneTo " + channel); 645 TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(channel.getInputId()); 646 if (inputInfo == null) { 647 return false; 648 } 649 if (mCurrentChannel != null) { 650 long duration = mChannelViewTimer.reset(); 651 mTracker.sendChannelViewStop(mCurrentChannel, duration); 652 if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { 653 mWatchedHistoryManager.logChannelViewStop( 654 mCurrentChannel, System.currentTimeMillis(), duration); 655 } 656 } 657 mOnTuneListener = listener; 658 mCurrentChannel = channel; 659 boolean tunedByRecommendation = 660 params != null 661 && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) 662 != null; 663 boolean needSurfaceSizeUpdate = false; 664 if (!inputInfo.equals(mInputInfo)) { 665 mTagetInputId = inputInfo.getId(); 666 mInputInfo = inputInfo; 667 mCanReceiveInputEvent = 668 getContext() 669 .getPackageManager() 670 .checkPermission( 671 PERMISSION_RECEIVE_INPUT_EVENT, 672 mInputInfo.getServiceInfo().packageName) 673 == PackageManager.PERMISSION_GRANTED; 674 if (DEBUG) { 675 Log.d( 676 TAG, 677 "Input \'" 678 + mInputInfo.getId() 679 + "\' can receive input event: " 680 + mCanReceiveInputEvent); 681 } 682 needSurfaceSizeUpdate = true; 683 } 684 mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation); 685 mChannelViewTimer.start(); 686 mVideoWidth = 0; 687 mVideoHeight = 0; 688 mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 689 mVideoFrameRate = 0f; 690 mVideoDisplayAspectRatio = 0f; 691 mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; 692 mHasClosedCaption = false; 693 mBlockedContentRating = null; 694 mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 695 // To reduce the IPCs, unregister the callback here and register it when necessary. 696 mTvView.setTimeShiftPositionCallback(null); 697 setTimeShiftAvailable(false); 698 mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING; 699 if (mTvViewSession != null) { 700 mTvViewSession.tune(channel, params, listener); 701 } else { 702 mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params); 703 } 704 if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { 705 // When the input is changed, TvView recreates its SurfaceView internally. 706 // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. 707 SurfaceView surfaceView = getSurfaceView(); 708 if (surfaceView != null) { 709 surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); 710 } else { 711 Log.w(TAG, "Failed to set fixed size for surface view: Null surface view"); 712 } 713 } 714 updateBlockScreenAndMuting(); 715 if (mOnTuneListener != null) { 716 mOnTuneListener.onStreamInfoChanged(this, true); 717 } 718 return true; 719 } 720 721 @Override 722 @Nullable getCurrentChannel()723 public Channel getCurrentChannel() { 724 return mCurrentChannel; 725 } 726 727 /** 728 * Sets the current channel. Call this method only when setting the current channel without 729 * actually tuning to it. 730 * 731 * @param currentChannel The new current channel to set to. 732 */ setCurrentChannel(Channel currentChannel)733 public void setCurrentChannel(Channel currentChannel) { 734 mCurrentChannel = currentChannel; 735 } 736 737 @Override setStreamVolume(float volume)738 public void setStreamVolume(float volume) { 739 if (!mStarted) { 740 throw new IllegalStateException("TvView isn't started"); 741 } 742 if (DEBUG) Log.d(TAG, "setStreamVolume " + volume); 743 mVolume = volume; 744 if (!mIsMuted) { 745 mTvView.setStreamVolume(volume); 746 } 747 } 748 749 @Override getStreamVolume()750 public float getStreamVolume() { 751 return mIsMuted 752 ? 0 753 : mVolume; 754 } 755 756 /** 757 * Sets fixed size for the internal {@link android.view.Surface} of {@link 758 * android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, the 759 * {@link android.view.Surface}'s size will be matched to the layout. 760 * 761 * <p>Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, {@link 762 * android.view.SurfaceView} and its underlying window can be misaligned, when the size of 763 * {@link android.view.SurfaceView} is changed without changing either left position or top 764 * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow(). 765 */ setFixedSurfaceSize(int width, int height)766 public void setFixedSurfaceSize(int width, int height) { 767 mFixedSurfaceWidth = width; 768 mFixedSurfaceHeight = height; 769 if (mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { 770 // When the input is changed, TvView recreates its SurfaceView internally. 771 // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. 772 SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0); 773 surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); 774 } else { 775 SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0); 776 surfaceView.getHolder().setSizeFromLayout(); 777 } 778 } 779 780 @Override dispatchKeyEvent(KeyEvent event)781 public boolean dispatchKeyEvent(KeyEvent event) { 782 return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event); 783 } 784 785 @Override dispatchTouchEvent(MotionEvent event)786 public boolean dispatchTouchEvent(MotionEvent event) { 787 return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event); 788 } 789 790 @Override dispatchTrackballEvent(MotionEvent event)791 public boolean dispatchTrackballEvent(MotionEvent event) { 792 return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event); 793 } 794 795 @Override dispatchGenericMotionEvent(MotionEvent event)796 public boolean dispatchGenericMotionEvent(MotionEvent event) { 797 return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event); 798 } 799 800 public interface OnTuneListener { onTuneFailed(Channel channel)801 void onTuneFailed(Channel channel); 802 onUnexpectedStop(Channel channel)803 void onUnexpectedStop(Channel channel); 804 onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack)805 void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack); 806 onChannelRetuned(Uri channel)807 void onChannelRetuned(Uri channel); 808 onContentBlocked()809 void onContentBlocked(); 810 onContentAllowed()811 void onContentAllowed(); 812 onChannelSignalStrength()813 void onChannelSignalStrength(); 814 815 @TargetApi(Build.VERSION_CODES.TIRAMISU) onAitInfoUpdated(String inputId, AitInfo aitInfo)816 void onAitInfoUpdated(String inputId, AitInfo aitInfo); 817 } 818 unblockContent(TvContentRating rating)819 public void unblockContent(TvContentRating rating) { 820 mTvView.unblockContent(rating); 821 } 822 823 @Override getVideoWidth()824 public int getVideoWidth() { 825 return mVideoWidth; 826 } 827 828 @Override getVideoHeight()829 public int getVideoHeight() { 830 return mVideoHeight; 831 } 832 833 @Override getVideoDefinitionLevel()834 public int getVideoDefinitionLevel() { 835 return mVideoFormat; 836 } 837 838 @Override getVideoFrameRate()839 public float getVideoFrameRate() { 840 return mVideoFrameRate; 841 } 842 843 /** Returns displayed aspect ratio (video width / video height * pixel ratio). */ 844 @Override getVideoDisplayAspectRatio()845 public float getVideoDisplayAspectRatio() { 846 return mVideoDisplayAspectRatio; 847 } 848 849 @Override getAudioChannelCount()850 public int getAudioChannelCount() { 851 return mAudioChannelCount; 852 } 853 854 @Override hasClosedCaption()855 public boolean hasClosedCaption() { 856 return mHasClosedCaption; 857 } 858 859 @Override isVideoAvailable()860 public boolean isVideoAvailable() { 861 return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE; 862 } 863 864 @Override isVideoOrAudioAvailable()865 public boolean isVideoOrAudioAvailable() { 866 return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE 867 || mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY; 868 } 869 870 @Override getVideoUnavailableReason()871 public int getVideoUnavailableReason() { 872 return mVideoUnavailableReason; 873 } 874 875 /** Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. */ getSurfaceView()876 private SurfaceView getSurfaceView() { 877 return (SurfaceView) mTvView.getChildAt(0); 878 } 879 setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener)880 public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { 881 mTvView.setOnUnhandledInputEventListener(listener); 882 } 883 setClosedCaptionEnabled(boolean enabled)884 public void setClosedCaptionEnabled(boolean enabled) { 885 mTvView.setCaptionEnabled(enabled); 886 } 887 888 @VisibleForTesting setOnTuneListener(OnTuneListener listener)889 public void setOnTuneListener(OnTuneListener listener) { 890 mOnTuneListener = listener; 891 } 892 getTracks(int type)893 public List<TvTrackInfo> getTracks(int type) { 894 return mTvView.getTracks(type); 895 } 896 getSelectedTrack(int type)897 public String getSelectedTrack(int type) { 898 return mTvView.getSelectedTrack(type); 899 } 900 selectTrack(int type, String trackId)901 public void selectTrack(int type, String trackId) { 902 mTvView.selectTrack(type, trackId); 903 } 904 905 /** 906 * Gets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView}, 907 * which is the actual view to play live TV videos. 908 */ getTvViewLayoutParams()909 public MarginLayoutParams getTvViewLayoutParams() { 910 return (MarginLayoutParams) mTvView.getLayoutParams(); 911 } 912 913 /** 914 * Sets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView}, 915 * which is the actual view to play live TV videos. 916 */ setTvViewLayoutParams(MarginLayoutParams layoutParams)917 public void setTvViewLayoutParams(MarginLayoutParams layoutParams) { 918 mTvView.setLayoutParams(layoutParams); 919 } 920 921 /** 922 * Gets the underlying {@link AppLayerTvView}, which is the actual view to play live TV videos. 923 */ getTvView()924 public TvView getTvView() { 925 return mTvView; 926 } 927 928 /** 929 * Returns if the screen is blocked, either by {@link #blockOrUnblockScreen(boolean)} or because 930 * the content is blocked. 931 */ isBlocked()932 public boolean isBlocked() { 933 return isScreenBlocked() || isContentBlocked(); 934 } 935 936 /** Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. */ isScreenBlocked()937 public boolean isScreenBlocked() { 938 return mScreenBlocked; 939 } 940 941 /** Returns {@code true} if the content is blocked, otherwise {@code false}. */ isContentBlocked()942 public boolean isContentBlocked() { 943 return mBlockedContentRating != null; 944 } 945 setOnScreenBlockedListener(OnScreenBlockingChangedListener listener)946 public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) { 947 mOnScreenBlockedListener = listener; 948 } 949 950 /** Returns currently blocked content rating. {@code null} if it's not blocked. */ 951 @Override getBlockedContentRating()952 public TvContentRating getBlockedContentRating() { 953 return mBlockedContentRating; 954 } 955 956 /** 957 * Blocks/unblocks current TV screen and mutes. There would be black screen with lock icon in 958 * order to show that screen block is intended and not an error. 959 * 960 * @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock. 961 */ blockOrUnblockScreen(boolean blockOrUnblock)962 public void blockOrUnblockScreen(boolean blockOrUnblock) { 963 if (mScreenBlocked == blockOrUnblock) { 964 return; 965 } 966 mScreenBlocked = blockOrUnblock; 967 if (closePipIfNeeded()) { 968 return; 969 } 970 updateBlockScreenAndMuting(); 971 if (mOnScreenBlockedListener != null) { 972 mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock); 973 } 974 } 975 976 @Override onVisibilityChanged(@onNull View changedView, int visibility)977 protected void onVisibilityChanged(@NonNull View changedView, int visibility) { 978 super.onVisibilityChanged(changedView, visibility); 979 if (mTvView != null) { 980 mTvView.setVisibility(visibility); 981 } 982 } 983 984 /** 985 * Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the 986 * block screen will not show any description such as a lock icon and a text for the blocked 987 * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block 988 * screen will show the description for shrunken tv view (Small icon and short text), and if 989 * {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the 990 * description for normal tv view (Big icon and long text). 991 * 992 * @param type The type of block screen to set. 993 */ setBlockScreenType(@lockScreenType int type)994 public void setBlockScreenType(@BlockScreenType int type) { 995 if (mBlockScreenType != type) { 996 mBlockScreenType = type; 997 updateBlockScreen(true); 998 } 999 } 1000 updateBlockScreen(boolean animation)1001 private void updateBlockScreen(boolean animation) { 1002 mBlockScreenView.endAnimations(); 1003 int blockReason = 1004 (mScreenBlocked || mBlockedContentRating != null) && mParentControlEnabled 1005 ? VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED 1006 : mVideoUnavailableReason; 1007 if (blockReason != VIDEO_UNAVAILABLE_REASON_NONE) { 1008 mBufferingSpinnerView.setVisibility( 1009 blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING 1010 || blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING 1011 ? VISIBLE 1012 : GONE); 1013 if (!animation) { 1014 adjustBlockScreenSpacingAndText(); 1015 } 1016 if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { 1017 return; 1018 } 1019 mBlockScreenView.setVisibility(VISIBLE); 1020 if (mTvIAppView != null) { 1021 mTvIAppView.setVisibility(INVISIBLE); 1022 } 1023 mBlockScreenView.setBackgroundImage(null); 1024 if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) { 1025 mBlockScreenView.setIconVisibility(true); 1026 if (!mCanModifyParentalControls) { 1027 mBlockScreenView.setIconImage(R.drawable.ic_message_lock_no_permission); 1028 mBlockScreenView.setIconScaleType(ImageView.ScaleType.CENTER); 1029 } else { 1030 mBlockScreenView.setIconImage(R.drawable.ic_message_lock); 1031 mBlockScreenView.setIconScaleType(ImageView.ScaleType.FIT_CENTER); 1032 } 1033 } else { 1034 if (mInternetCheckTask != null) { 1035 mInternetCheckTask.cancel(true); 1036 mInternetCheckTask = null; 1037 } 1038 mBlockScreenView.setIconVisibility(false); 1039 if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) { 1040 showImageForTuningIfNeeded(); 1041 } else if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN 1042 && mCurrentChannel != null 1043 && !mCurrentChannel.isPhysicalTunerChannel()) { 1044 mInternetCheckTask = new InternetCheckTask(); 1045 mInternetCheckTask.execute(); 1046 } 1047 } 1048 mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation); 1049 } else { 1050 mBufferingSpinnerView.setVisibility(GONE); 1051 if (mBlockScreenView.getVisibility() == VISIBLE) { 1052 mBlockScreenView.fadeOut(); 1053 } 1054 if (mTvIAppView != null) { 1055 mTvIAppView.setVisibility(VISIBLE); 1056 } 1057 } 1058 } 1059 adjustBlockScreenSpacingAndText()1060 private void adjustBlockScreenSpacingAndText() { 1061 mBlockScreenView.setSpacing(mBlockScreenType); 1062 String text = getBlockScreenText(); 1063 if (text != null) { 1064 mBlockScreenView.setInfoText(text); 1065 } 1066 mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled); 1067 } 1068 1069 /** 1070 * Returns the block screen text corresponding to the current status. Note that returning {@code 1071 * null} value means that the current text should not be changed. 1072 */ getBlockScreenText()1073 private String getBlockScreenText() { 1074 // TODO: add a test for this method 1075 Resources res = getResources(); 1076 boolean isA11y = mAccessibilityManager.isEnabled(); 1077 1078 if (mScreenBlocked && mParentControlEnabled) { 1079 switch (mBlockScreenType) { 1080 case BLOCK_SCREEN_TYPE_NO_UI: 1081 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: 1082 return ""; 1083 case BLOCK_SCREEN_TYPE_NORMAL: 1084 if (mCanModifyParentalControls) { 1085 return res.getString( 1086 isA11y 1087 ? R.string.tvview_channel_locked_talkback 1088 : R.string.tvview_channel_locked); 1089 } else { 1090 return res.getString(R.string.tvview_channel_locked_no_permission); 1091 } 1092 } 1093 } else if (mBlockedContentRating != null && mParentControlEnabled) { 1094 String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating); 1095 switch (mBlockScreenType) { 1096 case BLOCK_SCREEN_TYPE_NO_UI: 1097 return ""; 1098 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: 1099 if (TextUtils.isEmpty(name)) { 1100 return res.getString(R.string.shrunken_tvview_content_locked); 1101 } else if (name.equals(res.getString(R.string.unrated_rating_name))) { 1102 return res.getString(R.string.shrunken_tvview_content_locked_unrated); 1103 } else { 1104 return res.getString(R.string.shrunken_tvview_content_locked_format, name); 1105 } 1106 case BLOCK_SCREEN_TYPE_NORMAL: 1107 if (TextUtils.isEmpty(name)) { 1108 if (mCanModifyParentalControls) { 1109 return res.getString( 1110 isA11y 1111 ? R.string.tvview_content_locked_talkback 1112 : R.string.tvview_content_locked); 1113 } else { 1114 return res.getString(R.string.tvview_content_locked_no_permission); 1115 } 1116 } else { 1117 if (mCanModifyParentalControls) { 1118 return name.equals(res.getString(R.string.unrated_rating_name)) 1119 ? res.getString( 1120 isA11y 1121 ? R.string 1122 .tvview_content_locked_unrated_talkback 1123 : R.string.tvview_content_locked_unrated) 1124 : res.getString( 1125 isA11y 1126 ? R.string.tvview_content_locked_format_talkback 1127 : R.string.tvview_content_locked_format, 1128 name); 1129 } else { 1130 return name.equals(res.getString(R.string.unrated_rating_name)) 1131 ? res.getString( 1132 R.string.tvview_content_locked_unrated_no_permission) 1133 : res.getString( 1134 R.string.tvview_content_locked_format_no_permission, 1135 name); 1136 } 1137 } 1138 } 1139 } else if (mVideoUnavailableReason != VIDEO_UNAVAILABLE_REASON_NONE) { 1140 switch (mVideoUnavailableReason) { 1141 case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: 1142 return res.getString(R.string.tvview_msg_audio_only); 1143 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: 1144 return res.getString(R.string.tvview_msg_weak_signal); 1145 case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED: 1146 return res.getString(R.string.msg_channel_unavailable_not_connected); 1147 case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE: 1148 return getTuneConflictMessage(); 1149 default: 1150 return ""; 1151 } 1152 } 1153 return null; 1154 } 1155 closePipIfNeeded()1156 private boolean closePipIfNeeded() { 1157 if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getContext()) 1158 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N 1159 && ((Activity) getContext()).isInPictureInPictureMode() 1160 && (mScreenBlocked 1161 || mBlockedContentRating != null 1162 || mVideoUnavailableReason 1163 == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN 1164 || mVideoUnavailableReason 1165 == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) { 1166 ((Activity) getContext()).finish(); 1167 return true; 1168 } 1169 return false; 1170 } 1171 updateBlockScreenAndMuting()1172 private void updateBlockScreenAndMuting() { 1173 updateBlockScreen(false); 1174 updateMuteStatus(); 1175 } 1176 shouldShowImageForTuning()1177 private boolean shouldShowImageForTuning() { 1178 if (mVideoUnavailableReason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING 1179 || mScreenBlocked 1180 || mBlockedContentRating != null 1181 || mCurrentChannel == null 1182 || mIsUnderShrunken 1183 || getWidth() == 0 1184 || getWidth() == 0 1185 || !isBundledInput()) { 1186 return false; 1187 } 1188 Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); 1189 if (currentProgram == null) { 1190 return false; 1191 } 1192 TvContentRating rating = 1193 mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings()); 1194 return !(mParentControlEnabled && rating != null); 1195 } 1196 showImageForTuningIfNeeded()1197 private void showImageForTuningIfNeeded() { 1198 if (shouldShowImageForTuning()) { 1199 if (mCurrentChannel == null) { 1200 return; 1201 } 1202 Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); 1203 if (currentProgram != null) { 1204 currentProgram.loadPosterArt( 1205 getContext(), 1206 getWidth(), 1207 getHeight(), 1208 createProgramPosterArtCallback(mCurrentChannel.getId())); 1209 } 1210 } 1211 } 1212 getTuneConflictMessage()1213 private String getTuneConflictMessage() { 1214 if (mTagetInputId != null) { 1215 TvInputInfo input = mInputManager.getTvInputInfo(mTagetInputId); 1216 Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(mTagetInputId); 1217 if (timeMs != null) { 1218 return getResources() 1219 .getQuantityString( 1220 R.plurals.tvview_msg_input_no_resource, 1221 input.getTunerCount(), 1222 DateUtils.formatDateTime( 1223 getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME)); 1224 } 1225 } 1226 return null; 1227 } 1228 updateMuteStatus()1229 private void updateMuteStatus() { 1230 // Workaround: BaseTunerTvInputService uses AC3 pass-through implementation, which disables 1231 // audio tracks to enforce the mute request. We don't want to send mute request if we are 1232 // not going to block the screen to prevent the video jankiness resulted by disabling audio 1233 // track before the playback is started. In other way, we should send unmute request before 1234 // the playback is started, because TunerTvInput will remember the muted state and mute 1235 // itself right way when the playback is going to be started, which results the initial 1236 // jankiness, too. 1237 boolean isBundledInput = isBundledInput(); 1238 if ((isBundledInput || isVideoOrAudioAvailable()) 1239 && !mScreenBlocked 1240 && mBlockedContentRating == null) { 1241 if (mIsMuted) { 1242 mIsMuted = false; 1243 mTvView.setStreamVolume(mVolume); 1244 } 1245 } else { 1246 if (!mIsMuted) { 1247 if ((mInputInfo == null || isBundledInput) 1248 && !mScreenBlocked 1249 && mBlockedContentRating == null) { 1250 return; 1251 } 1252 mIsMuted = true; 1253 mTvView.setStreamVolume(0); 1254 } 1255 } 1256 } 1257 isBundledInput()1258 private boolean isBundledInput() { 1259 return mInputInfo != null 1260 && mInputInfo.getType() == TvInputInfo.TYPE_TUNER 1261 && CommonUtils.isBundledInput(mInputInfo.getId()); 1262 } 1263 1264 /** Returns true if this view is faded out. */ isFadedOut()1265 public boolean isFadedOut() { 1266 return mFadeState == FADED_OUT; 1267 } 1268 1269 /** Fade out this TunableTvView. Fade out by increasing the dimming. */ fadeOut( int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1270 public void fadeOut( 1271 int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) { 1272 mDimScreenView.setAlpha(0f); 1273 mDimScreenView.setVisibility(View.VISIBLE); 1274 mDimScreenView 1275 .animate() 1276 .alpha(1f) 1277 .setDuration(durationMillis) 1278 .setInterpolator(interpolator) 1279 .withStartAction( 1280 () -> { 1281 mFadeState = FADING_OUT; 1282 mActionAfterFade = actionAfterFade; 1283 }) 1284 .withEndAction(() -> mFadeState = FADED_OUT); 1285 } 1286 1287 /** Fade in this TunableTvView. Fade in by decreasing the dimming. */ fadeIn( int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1288 public void fadeIn( 1289 int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) { 1290 mDimScreenView.setAlpha(1f); 1291 mDimScreenView.setVisibility(View.VISIBLE); 1292 mDimScreenView 1293 .animate() 1294 .alpha(0f) 1295 .setDuration(durationMillis) 1296 .setInterpolator(interpolator) 1297 .withStartAction( 1298 () -> { 1299 mFadeState = FADING_IN; 1300 mActionAfterFade = actionAfterFade; 1301 }) 1302 .withEndAction( 1303 () -> { 1304 mFadeState = FADED_IN; 1305 mDimScreenView.setVisibility(View.GONE); 1306 }); 1307 } 1308 1309 /** Remove the fade effect. */ removeFadeEffect()1310 public void removeFadeEffect() { 1311 mDimScreenView.animate().cancel(); 1312 mDimScreenView.setVisibility(View.GONE); 1313 mFadeState = FADED_IN; 1314 } 1315 1316 /** 1317 * Sets the TimeShiftListener 1318 * 1319 * @param listener The instance of {@link TimeShiftListener}. 1320 */ 1321 @Override setTimeShiftListener(TimeShiftListener listener)1322 public void setTimeShiftListener(TimeShiftListener listener) { 1323 mTimeShiftListener = listener; 1324 } 1325 setBlockedInfoOnClickListener(@ullable OnClickListener onClickListener)1326 public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) { 1327 mBlockScreenView.setInfoTextOnClickListener(onClickListener); 1328 } 1329 setTimeShiftAvailable(boolean isTimeShiftAvailable)1330 private void setTimeShiftAvailable(boolean isTimeShiftAvailable) { 1331 if (mTimeShiftAvailable == isTimeShiftAvailable) { 1332 return; 1333 } 1334 mTimeShiftAvailable = isTimeShiftAvailable; 1335 if (isTimeShiftAvailable) { 1336 mTvView.setTimeShiftPositionCallback( 1337 new TvView.TimeShiftPositionCallback() { 1338 @Override 1339 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { 1340 if (mTimeShiftListener != null 1341 && mCurrentChannel != null 1342 && mCurrentChannel.getInputId().equals(inputId)) { 1343 mTimeShiftListener.onRecordStartTimeChanged(timeMs); 1344 } 1345 } 1346 1347 @Override 1348 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { 1349 mTimeShiftCurrentPositionMs = timeMs; 1350 } 1351 }); 1352 } else { 1353 mTvView.setTimeShiftPositionCallback(null); 1354 } 1355 if (mTimeShiftListener != null) { 1356 mTimeShiftListener.onAvailabilityChanged(); 1357 } 1358 } 1359 1360 /** Returns if the time shift is available for the current channel. */ 1361 @Override isTimeShiftAvailable()1362 public boolean isTimeShiftAvailable() { 1363 return mTimeShiftAvailable; 1364 } 1365 1366 /** Plays the media, if the current input supports time-shifting. */ 1367 @Override timeShiftPlay()1368 public void timeShiftPlay() { 1369 if (!isTimeShiftAvailable()) { 1370 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1371 } 1372 if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) { 1373 return; 1374 } 1375 mTvView.timeShiftResume(); 1376 } 1377 1378 /** Pauses the media, if the current input supports time-shifting. */ 1379 @Override timeShiftPause()1380 public void timeShiftPause() { 1381 if (!isTimeShiftAvailable()) { 1382 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1383 } 1384 if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) { 1385 return; 1386 } 1387 mTvView.timeShiftPause(); 1388 } 1389 1390 /** 1391 * Rewinds the media with the given speed, if the current input supports time-shifting. 1392 * 1393 * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. 1394 */ 1395 @Override timeShiftRewind(int speed)1396 public void timeShiftRewind(int speed) { 1397 if (!isTimeShiftAvailable()) { 1398 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1399 } else { 1400 if (speed <= 0) { 1401 throw new IllegalArgumentException("The speed should be a positive integer."); 1402 } 1403 mTimeShiftState = TIME_SHIFT_STATE_REWIND; 1404 PlaybackParams params = new PlaybackParams(); 1405 params.setSpeed(speed * -1); 1406 mTvView.timeShiftSetPlaybackParams(params); 1407 } 1408 } 1409 1410 /** 1411 * Fast-forwards the media with the given speed, if the current input supports time-shifting. 1412 * 1413 * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. 1414 */ 1415 @Override timeShiftFastForward(int speed)1416 public void timeShiftFastForward(int speed) { 1417 if (!isTimeShiftAvailable()) { 1418 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1419 } else { 1420 if (speed <= 0) { 1421 throw new IllegalArgumentException("The speed should be a positive integer."); 1422 } 1423 mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD; 1424 PlaybackParams params = new PlaybackParams(); 1425 params.setSpeed(speed); 1426 mTvView.timeShiftSetPlaybackParams(params); 1427 } 1428 } 1429 1430 /** 1431 * Seek to the given time position. 1432 * 1433 * @param timeMs The time in milliseconds to seek to. 1434 */ 1435 @Override timeShiftSeekTo(long timeMs)1436 public void timeShiftSeekTo(long timeMs) { 1437 if (!isTimeShiftAvailable()) { 1438 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1439 } 1440 mTvView.timeShiftSeekTo(timeMs); 1441 } 1442 1443 /** Returns the current playback position in milliseconds. */ 1444 @Override timeShiftGetCurrentPositionMs()1445 public long timeShiftGetCurrentPositionMs() { 1446 if (!isTimeShiftAvailable()) { 1447 throw new IllegalStateException("Time-shift is not supported for the current channel"); 1448 } 1449 if (DEBUG) { 1450 Log.d( 1451 TAG, 1452 "timeShiftGetCurrentPositionMs: current position =" 1453 + Utils.toTimeString(mTimeShiftCurrentPositionMs)); 1454 } 1455 return mTimeShiftCurrentPositionMs; 1456 } 1457 createProgramPosterArtCallback( final long channelId)1458 private ImageLoader.ImageLoaderCallback<BlockScreenView> createProgramPosterArtCallback( 1459 final long channelId) { 1460 return new ImageLoader.ImageLoaderCallback<BlockScreenView>(mBlockScreenView) { 1461 @Override 1462 public void onBitmapLoaded(BlockScreenView view, @Nullable Bitmap posterArt) { 1463 if (posterArt == null 1464 || getCurrentChannel() == null 1465 || channelId != getCurrentChannel().getId() 1466 || !shouldShowImageForTuning()) { 1467 return; 1468 } 1469 Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt); 1470 drawablePosterArt 1471 .mutate() 1472 .setColorFilter(mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER); 1473 view.setBackgroundImage(drawablePosterArt); 1474 } 1475 }; 1476 } 1477 1478 /** A listener which receives the notification when the screen is blocked/unblocked. */ 1479 public abstract static class OnScreenBlockingChangedListener { 1480 /** Called when the screen is blocked/unblocked. */ 1481 public abstract void onScreenBlockingChanged(boolean blocked); 1482 } 1483 1484 private class InternetCheckTask extends AsyncTask<Void, Void, Boolean> { 1485 @Override 1486 protected Boolean doInBackground(Void... params) { 1487 return NetworkUtils.isNetworkAvailable(mConnectivityManager); 1488 } 1489 1490 @Override 1491 protected void onPostExecute(Boolean networkAvailable) { 1492 mInternetCheckTask = null; 1493 if (!networkAvailable 1494 && isAttachedToWindow() 1495 && !mScreenBlocked 1496 && mBlockedContentRating == null 1497 && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) { 1498 mBlockScreenView.setIconVisibility(true); 1499 mBlockScreenView.setIconImage(R.drawable.ic_sad_cloud); 1500 mBlockScreenView.setInfoText(R.string.tvview_msg_no_internet_connection); 1501 } 1502 } 1503 } 1504 } 1505