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