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