/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.media.PlaybackParams; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.TvView.OnUnhandledInputEventListener; import android.media.tv.TvView.TvInputCallback; import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.tv.ApplicationSingletons; import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.TvViewSession; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.recommendation.NotificationService; import com.android.tv.util.NetworkUtils; import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; public class TunableTvView extends FrameLayout implements StreamInfo { private static final boolean DEBUG = false; private static final String TAG = "TunableTvView"; public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1; public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2; @Retention(RetentionPolicy.SOURCE) @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL}) public @interface BlockScreenType {} public static final int BLOCK_SCREEN_TYPE_NO_UI = 0; public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1; public static final int BLOCK_SCREEN_TYPE_NORMAL = 2; private static final String PERMISSION_RECEIVE_INPUT_EVENT = "com.android.tv.permission.RECEIVE_INPUT_EVENT"; @Retention(RetentionPolicy.SOURCE) @IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE, TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD }) private @interface TimeShiftState {} private static final int TIME_SHIFT_STATE_NONE = 0; private static final int TIME_SHIFT_STATE_PLAY = 1; private static final int TIME_SHIFT_STATE_PAUSE = 2; private static final int TIME_SHIFT_STATE_REWIND = 3; private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4; private static final int FADED_IN = 0; private static final int FADED_OUT = 1; private static final int FADING_IN = 2; private static final int FADING_OUT = 3; // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR. private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f; private AppLayerTvView mTvView; private TvViewSession mTvViewSession; private Channel mCurrentChannel; private TvInputManagerHelper mInputManagerHelper; private ContentRatingsManager mContentRatingsManager; @Nullable private WatchedHistoryManager mWatchedHistoryManager; private boolean mStarted; private TvInputInfo mInputInfo; private OnTuneListener mOnTuneListener; private int mVideoWidth; private int mVideoHeight; private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; private float mVideoFrameRate; private float mVideoDisplayAspectRatio; private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; private boolean mHasClosedCaption = false; private boolean mVideoAvailable; private boolean mScreenBlocked; private OnScreenBlockingChangedListener mOnScreenBlockedListener; private TvContentRating mBlockedContentRating; private int mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED; private boolean mCanReceiveInputEvent; private boolean mIsMuted; private float mVolume; private boolean mParentControlEnabled; private int mFixedSurfaceWidth; private int mFixedSurfaceHeight; private boolean mIsPip; private int mScreenHeight; private int mShrunkenTvViewHeight; private final boolean mCanModifyParentalControls; @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; private TimeShiftListener mTimeShiftListener; private boolean mTimeShiftAvailable; private long mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; private final Tracker mTracker; private final DurationTimer mChannelViewTimer = new DurationTimer(); private InternetCheckTask mInternetCheckTask; // A block screen view which has lock icon with black background. // This indicates that user's action is needed to play video. private final BlockScreenView mBlockScreenView; // A View to hide screen when there's problem in video playback. private final BlockScreenView mHideScreenView; // A View to block screen until onContentAllowed is received if parental control is on. private final View mBlockScreenForTuneView; // A spinner view to show buffering status. private final View mBufferingSpinnerView; // A View for fade-in/out animation private final View mDimScreenView; private int mFadeState = FADED_IN; private Runnable mActionAfterFade; @BlockScreenType private int mBlockScreenType; private final TvInputManagerHelper mInputManager; private final ConnectivityManager mConnectivityManager; private final InputSessionManager mInputSessionManager; private final TvInputCallback mCallback = new TvInputCallback() { @Override public void onConnectionFailed(String inputId) { Log.w(TAG, "Failed to bind an input"); mTracker.sendInputConnectionFailure(inputId); Channel channel = mCurrentChannel; mCurrentChannel = null; mInputInfo = null; mCanReceiveInputEvent = false; if (mOnTuneListener != null) { // If tune is called inside onTuneFailed, mOnTuneListener will be set to // a new instance. In order to avoid to clear the new mOnTuneListener, // we copy mOnTuneListener to l and clear mOnTuneListener before // calling onTuneFailed. OnTuneListener listener = mOnTuneListener; mOnTuneListener = null; listener.onTuneFailed(channel); } } @Override public void onDisconnected(String inputId) { Log.w(TAG, "Session is released by crash"); mTracker.sendInputDisconnected(inputId); Channel channel = mCurrentChannel; mCurrentChannel = null; mInputInfo = null; mCanReceiveInputEvent = false; if (mOnTuneListener != null) { OnTuneListener listener = mOnTuneListener; mOnTuneListener = null; listener.onUnexpectedStop(channel); } } @Override public void onChannelRetuned(String inputId, Uri channelUri) { if (DEBUG) { Log.d(TAG, "onChannelRetuned(inputId=" + inputId + ", channelUri=" + channelUri + ")"); } if (mOnTuneListener != null) { mOnTuneListener.onChannelRetuned(channelUri); } } @Override public void onTracksChanged(String inputId, List tracks) { mHasClosedCaption = false; for (TvTrackInfo track : tracks) { if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) { mHasClosedCaption = true; break; } } if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } } @Override public void onTrackSelected(String inputId, int type, String trackId) { if (trackId == null) { // A track is unselected. if (type == TvTrackInfo.TYPE_VIDEO) { mVideoWidth = 0; mVideoHeight = 0; mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; mVideoFrameRate = 0f; mVideoDisplayAspectRatio = 0f; } else if (type == TvTrackInfo.TYPE_AUDIO) { mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; } } else { List tracks = getTracks(type); boolean trackFound = false; if (tracks != null) { for (TvTrackInfo track : tracks) { if (track.getId().equals(trackId)) { if (type == TvTrackInfo.TYPE_VIDEO) { mVideoWidth = track.getVideoWidth(); mVideoHeight = track.getVideoHeight(); mVideoFormat = Utils.getVideoDefinitionLevelFromSize( mVideoWidth, mVideoHeight); mVideoFrameRate = track.getVideoFrameRate(); if (mVideoWidth <= 0 || mVideoHeight <= 0) { mVideoDisplayAspectRatio = 0.0f; } else { float VideoPixelAspectRatio = track.getVideoPixelAspectRatio(); mVideoDisplayAspectRatio = VideoPixelAspectRatio * mVideoWidth / mVideoHeight; } } else if (type == TvTrackInfo.TYPE_AUDIO) { mAudioChannelCount = track.getAudioChannelCount(); } trackFound = true; break; } } } if (!trackFound) { Log.w(TAG, "Invalid track ID: " + trackId); } } if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } } @Override public void onVideoAvailable(String inputId) { unhideScreenByVideoAvailability(); if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } } @Override public void onVideoUnavailable(String inputId, int reason) { hideScreenByVideoAvailability(inputId, reason); if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } switch (reason) { case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); default: // do nothing } } @Override public void onContentAllowed(String inputId) { mBlockScreenForTuneView.setVisibility(View.GONE); unblockScreenByContentRating(); if (mOnTuneListener != null) { mOnTuneListener.onContentAllowed(); } } @Override public void onContentBlocked(String inputId, TvContentRating rating) { blockScreenByContentRating(rating); if (mOnTuneListener != null) { mOnTuneListener.onContentBlocked(); } } @Override public void onTimeShiftStatusChanged(String inputId, int status) { boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; setTimeShiftAvailable(available); } }; public TunableTvView(Context context) { this(context, null); } public TunableTvView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); inflate(getContext(), R.layout.tunable_tv_view, this); ApplicationSingletons appSingletons = TvApplication.getSingletons(context); if (CommonFeatures.DVR.isEnabled(context)) { mInputSessionManager = appSingletons.getInputSessionManager(); } else { mInputSessionManager = null; } mInputManager = appSingletons.getTvInputManagerHelper(); mConnectivityManager = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); mTracker = appSingletons.getTracker(); mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen); if (!mCanModifyParentalControls) { mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission); mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER); } else { mBlockScreenView.setImage(R.drawable.ic_message_lock); } mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview); mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { adjustBlockScreenSpacingAndText(); } }); mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen); mHideScreenView.setImageVisibility(false); mBufferingSpinnerView = findViewById(R.id.buffering_spinner); mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune); mDimScreenView = findViewById(R.id.dim); mDimScreenView.animate().setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mActionAfterFade != null) { mActionAfterFade.run(); } } @Override public void onAnimationCancel(Animator animation) { if (mActionAfterFade != null) { mActionAfterFade.run(); } } }); } public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight, int shrunkenTvViewHeight) { mTvView = tvView; if (mInputSessionManager != null) { mTvViewSession = mInputSessionManager.createTvViewSession(tvView, this, mCallback); } else { mTvView.setCallback(mCallback); } mIsPip = isPip; mScreenHeight = screenHeight; mShrunkenTvViewHeight = shrunkenTvViewHeight; mTvView.setZOrderOnTop(isPip); copyLayoutParamsToTvView(); } public void start(TvInputManagerHelper tvInputManagerHelper) { mInputManagerHelper = tvInputManagerHelper; mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); if (mStarted) { return; } mStarted = true; } /** * Warms up the input to reduce the start time. */ public void warmUpInput(String inputId, Uri channelUri) { if (!mStarted && inputId != null && channelUri != null) { if (mTvViewSession != null) { mTvViewSession.tune(inputId, channelUri); } else { mTvView.tune(inputId, channelUri); } hideScreenByVideoAvailability(inputId, TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); } } public void stop() { if (!mStarted) { return; } mStarted = false; if (mCurrentChannel != null) { long duration = mChannelViewTimer.reset(); mTracker.sendChannelViewStop(mCurrentChannel, duration); if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, System.currentTimeMillis(), duration); } } reset(); } /** * Releases the resources. */ public void release() { if (mInputSessionManager != null) { mInputSessionManager.releaseTvViewSession(mTvViewSession); mTvViewSession = null; } } /** * Reset TV view. */ public void reset() { resetInternal(); hideScreenByVideoAvailability(null, VIDEO_UNAVAILABLE_REASON_NOT_TUNED); } /** * Reset TV view to acquire the recording session. */ public void resetByRecording() { resetInternal(); } private void resetInternal() { if (mTvViewSession != null) { mTvViewSession.reset(); } else { mTvView.reset(); } mCurrentChannel = null; mInputInfo = null; mCanReceiveInputEvent = false; mOnTuneListener = null; setTimeShiftAvailable(false); } public void setMain() { mTvView.setMain(); } public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) { mWatchedHistoryManager = watchedHistoryManager; } public boolean isPlaying() { return mStarted; } /** * Called when parental control is changed. */ public void onParentalControlChanged(boolean enabled) { mParentControlEnabled = enabled; if (!mParentControlEnabled) { mBlockScreenForTuneView.setVisibility(View.GONE); } } /** * Tunes to a channel with the {@code channelId}. * * @param params extra data to send it to TIS and store the data in TIMS. * @return false, if the TV input is not a proper state to tune to a channel. For example, * if the state is disconnected or channelId doesn't exist, it returns false. */ public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) { if (!mStarted) { throw new IllegalStateException("TvView isn't started"); } if (DEBUG) Log.d(TAG, "tuneTo " + channel); TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(channel.getInputId()); if (inputInfo == null) { return false; } if (mCurrentChannel != null) { long duration = mChannelViewTimer.reset(); mTracker.sendChannelViewStop(mCurrentChannel, duration); if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, System.currentTimeMillis(), duration); } } mOnTuneListener = listener; mCurrentChannel = channel; boolean tunedByRecommendation = params != null && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null; boolean needSurfaceSizeUpdate = false; if (!inputInfo.equals(mInputInfo)) { mInputInfo = inputInfo; mCanReceiveInputEvent = getContext().getPackageManager().checkPermission( PERMISSION_RECEIVE_INPUT_EVENT, mInputInfo.getServiceInfo().packageName) == PackageManager.PERMISSION_GRANTED; if (DEBUG) { Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: " + mCanReceiveInputEvent); } needSurfaceSizeUpdate = true; } mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation); mChannelViewTimer.start(); mVideoWidth = 0; mVideoHeight = 0; mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; mVideoFrameRate = 0f; mVideoDisplayAspectRatio = 0f; mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; mHasClosedCaption = false; mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; // To reduce the IPCs, unregister the callback here and register it when necessary. mTvView.setTimeShiftPositionCallback(null); setTimeShiftAvailable(false); if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { // When the input is changed, TvView recreates its SurfaceView internally. // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); } hideScreenByVideoAvailability(mInputInfo.getId(), TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); if (mTvViewSession != null) { mTvViewSession.tune(channel, params, listener); } else { mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params); } unblockScreenByContentRating(); if (channel.isPassthrough()) { mBlockScreenForTuneView.setVisibility(View.GONE); } else if (mParentControlEnabled) { mBlockScreenForTuneView.setVisibility(View.VISIBLE); } if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(this); } return true; } @Override public Channel getCurrentChannel() { return mCurrentChannel; } /** * Sets the current channel. Call this method only when setting the current channel without * actually tuning to it. * * @param currentChannel The new current channel to set to. */ public void setCurrentChannel(Channel currentChannel) { mCurrentChannel = currentChannel; } public void setStreamVolume(float volume) { if (!mStarted) { throw new IllegalStateException("TvView isn't started"); } if (DEBUG) Log.d(TAG, "setStreamVolume " + volume); mVolume = volume; if (!mIsMuted) { mTvView.setStreamVolume(volume); } } /** * Sets fixed size for the internal {@link android.view.Surface} of * {@link android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, * the {@link android.view.Surface}'s size will be matched to the layout. * * Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, * {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size * of {@link android.view.SurfaceView} is changed without changing either left position or top * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow(). */ public void setFixedSurfaceSize(int width, int height) { mFixedSurfaceWidth = width; mFixedSurfaceHeight = height; if (mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) { // When the input is changed, TvView recreates its SurfaceView internally. // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView. SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0); surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight); } else { SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0); surfaceView.getHolder().setSizeFromLayout(); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent event) { return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event); } @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event); } public interface OnTuneListener { void onTuneFailed(Channel channel); void onUnexpectedStop(Channel channel); void onStreamInfoChanged(StreamInfo info); void onChannelRetuned(Uri channel); void onContentBlocked(); void onContentAllowed(); } public void unblockContent(TvContentRating rating) { mTvView.unblockContent(rating); } @Override public int getVideoWidth() { return mVideoWidth; } @Override public int getVideoHeight() { return mVideoHeight; } @Override public int getVideoDefinitionLevel() { return mVideoFormat; } @Override public float getVideoFrameRate() { return mVideoFrameRate; } /** * Returns displayed aspect ratio (video width / video height * pixel ratio). */ @Override public float getVideoDisplayAspectRatio() { return mVideoDisplayAspectRatio; } @Override public int getAudioChannelCount() { return mAudioChannelCount; } @Override public boolean hasClosedCaption() { return mHasClosedCaption; } @Override public boolean isVideoAvailable() { return mVideoAvailable; } @Override public int getVideoUnavailableReason() { return mVideoUnavailableReason; } /** * Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. */ private SurfaceView getSurfaceView() { return (SurfaceView) mTvView.getChildAt(0); } public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { mTvView.setOnUnhandledInputEventListener(listener); } public void setClosedCaptionEnabled(boolean enabled) { mTvView.setCaptionEnabled(enabled); } public List getTracks(int type) { return mTvView.getTracks(type); } public String getSelectedTrack(int type) { return mTvView.getSelectedTrack(type); } public void selectTrack(int type, String trackId) { mTvView.selectTrack(type, trackId); } /** * Returns if the screen is blocked by {@link #blockScreen()}. */ public boolean isScreenBlocked() { return mScreenBlocked; } public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) { mOnScreenBlockedListener = listener; } /** * Returns currently blocked content rating. {@code null} if it's not blocked. */ @Override public TvContentRating getBlockedContentRating() { return mBlockedContentRating; } /** * Locks current TV screen and mutes. * There would be black screen with lock icon in order to show that * screen block is intended and not an error. * TODO: Accept parameter to show lock icon or not. */ public void blockScreen() { mScreenBlocked = true; checkBlockScreenAndMuteNeeded(); if (mOnScreenBlockedListener != null) { mOnScreenBlockedListener.onScreenBlockingChanged(true); } } private void blockScreenByContentRating(TvContentRating rating) { mBlockedContentRating = rating; checkBlockScreenAndMuteNeeded(); } @Override @SuppressLint("RtlHardcoded") protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mIsPip) { int height = bottom - top; float scale; if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) { scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight; } else { scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight; } // TODO: need to get UX confirmation. mBlockScreenView.scaleContainerView(scale); } } @Override public void setLayoutParams(ViewGroup.LayoutParams params) { super.setLayoutParams(params); if (mTvView != null) { copyLayoutParamsToTvView(); } } private void copyLayoutParamsToTvView() { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); FrameLayout.LayoutParams tvViewLp = (FrameLayout.LayoutParams) mTvView.getLayoutParams(); if (tvViewLp.bottomMargin != lp.bottomMargin || tvViewLp.topMargin != lp.topMargin || tvViewLp.leftMargin != lp.leftMargin || tvViewLp.rightMargin != lp.rightMargin || tvViewLp.gravity != lp.gravity || tvViewLp.height != lp.height || tvViewLp.width != lp.width) { if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin && !BuildCompat.isAtLeastN()) { // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is // used, SurfaceView doesn't catch the width and height change. It causes a bug that // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for // small size PIP as a workaround. // Note: This framework issue has been fixed from NYC. tvViewLp.leftMargin = lp.leftMargin + 1; } else { tvViewLp.leftMargin = lp.leftMargin; } tvViewLp.topMargin = lp.topMargin; tvViewLp.bottomMargin = lp.bottomMargin; tvViewLp.rightMargin = lp.rightMargin; tvViewLp.gravity = lp.gravity; tvViewLp.height = lp.height; tvViewLp.width = lp.width; mTvView.setLayoutParams(tvViewLp); } } @Override protected void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (mTvView != null) { mTvView.setVisibility(visibility); } } /** * Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the * block screen will not show any description such as a lock icon and a text for the blocked * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block screen * will show the description for shrunken tv view (Small icon and short text), and if * {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the * description for normal tv view (Big icon and long text). * * @param type The type of block screen to set. */ public void setBlockScreenType(@BlockScreenType int type) { // TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse. if (mBlockScreenType != type) { mBlockScreenType = type; updateBlockScreenUI(true); } } private void updateBlockScreenUI(boolean animation) { mBlockScreenView.endAnimations(); if (!mScreenBlocked && mBlockedContentRating == null) { mBlockScreenView.setVisibility(GONE); return; } mBlockScreenView.setVisibility(VISIBLE); if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) { adjustBlockScreenSpacingAndText(); } mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation); } private void adjustBlockScreenSpacingAndText() { // TODO: need to add animation for padding change when the block screen type is changed // NORMAL to SHRUNKEN and vice verse. mBlockScreenView.setSpacing(mBlockScreenType); String text = getBlockScreenText(); if (text != null) { mBlockScreenView.setText(text); } } /** * Returns the block screen text corresponding to the current status. * Note that returning {@code null} value means that the current text should not be changed. */ private String getBlockScreenText() { if (mScreenBlocked) { switch (mBlockScreenType) { case BLOCK_SCREEN_TYPE_NO_UI: case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: return ""; case BLOCK_SCREEN_TYPE_NORMAL: if (mCanModifyParentalControls) { return getResources().getString(R.string.tvview_channel_locked); } else { return getResources().getString( R.string.tvview_channel_locked_no_permission); } } } else if (mBlockedContentRating != null) { String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating); switch (mBlockScreenType) { case BLOCK_SCREEN_TYPE_NO_UI: return ""; case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW: if (TextUtils.isEmpty(name)) { return getResources().getString(R.string.shrunken_tvview_content_locked); } else { return getContext().getString( R.string.shrunken_tvview_content_locked_format, name); } case BLOCK_SCREEN_TYPE_NORMAL: if (TextUtils.isEmpty(name)) { if (mCanModifyParentalControls) { return getResources().getString(R.string.tvview_content_locked); } else { return getResources().getString( R.string.tvview_content_locked_no_permission); } } else { if (mCanModifyParentalControls) { return getContext().getString( R.string.tvview_content_locked_format, name); } else { return getContext().getString( R.string.tvview_content_locked_format_no_permission, name); } } } } return null; } private void checkBlockScreenAndMuteNeeded() { updateBlockScreenUI(false); if (mScreenBlocked || mBlockedContentRating != null) { mute(); if (mIsPip) { // If we don't make mTvView invisible, some frames are leaked when a user changes // PIP layout in options. // Note: When video is unavailable, we keep the mTvView's visibility, because // TIS implementation may not send video available with no surface. mTvView.setVisibility(View.INVISIBLE); } } else { unmuteIfPossible(); if (mIsPip) { mTvView.setVisibility(View.VISIBLE); } } } public void unblockScreen() { mScreenBlocked = false; checkBlockScreenAndMuteNeeded(); if (mOnScreenBlockedListener != null) { mOnScreenBlockedListener.onScreenBlockingChanged(false); } } private void unblockScreenByContentRating() { mBlockedContentRating = null; checkBlockScreenAndMuteNeeded(); } @UiThread private void hideScreenByVideoAvailability(String inputId, int reason) { mVideoAvailable = false; mVideoUnavailableReason = reason; if (mInternetCheckTask != null) { mInternetCheckTask.cancel(true); mInternetCheckTask = null; } switch (reason) { case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(R.string.tvview_msg_audio_only); mBufferingSpinnerView.setVisibility(GONE); unmuteIfPossible(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: mBufferingSpinnerView.setVisibility(VISIBLE); mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setText(R.string.tvview_msg_weak_signal); mBufferingSpinnerView.setVisibility(GONE); mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(VISIBLE); mute(); break; case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(GONE); mute(); break; case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(getTuneConflictMessage(inputId)); mBufferingSpinnerView.setVisibility(GONE); mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(GONE); mute(); if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) { mInternetCheckTask = new InternetCheckTask(); mInternetCheckTask.execute(); } break; } } private String getTuneConflictMessage(String inputId) { if (inputId != null) { TvInputInfo input = mInputManager.getTvInputInfo(inputId); Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(inputId); if (timeMs != null) { return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource, input.getTunerCount(), DateUtils.formatDateTime(getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME)); } } return null; } private void unhideScreenByVideoAvailability() { mVideoAvailable = true; mHideScreenView.setVisibility(GONE); mBufferingSpinnerView.setVisibility(GONE); unmuteIfPossible(); } private void unmuteIfPossible() { if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) { unmute(); } } private void mute() { mIsMuted = true; mTvView.setStreamVolume(0); } private void unmute() { mIsMuted = false; mTvView.setStreamVolume(mVolume); } /** Returns true if this view is faded out. */ public boolean isFadedOut() { return mFadeState == FADED_OUT; } /** Fade out this TunableTvView. Fade out by increasing the dimming. */ public void fadeOut(int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) { mDimScreenView.setAlpha(0f); mDimScreenView.setVisibility(View.VISIBLE); mDimScreenView.animate() .alpha(1f) .setDuration(durationMillis) .setInterpolator(interpolator) .withStartAction(new Runnable() { @Override public void run() { mFadeState = FADING_OUT; mActionAfterFade = actionAfterFade; } }) .withEndAction(new Runnable() { @Override public void run() { mFadeState = FADED_OUT; } }); } /** Fade in this TunableTvView. Fade in by decreasing the dimming. */ public void fadeIn(int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) { mDimScreenView.setAlpha(1f); mDimScreenView.setVisibility(View.VISIBLE); mDimScreenView.animate() .alpha(0f) .setDuration(durationMillis) .setInterpolator(interpolator) .withStartAction(new Runnable() { @Override public void run() { mFadeState = FADING_IN; mActionAfterFade = actionAfterFade; } }) .withEndAction(new Runnable() { @Override public void run() { mFadeState = FADED_IN; mDimScreenView.setVisibility(View.GONE); } }); } /** Remove the fade effect. */ public void removeFadeEffect() { mDimScreenView.animate().cancel(); mDimScreenView.setVisibility(View.GONE); mFadeState = FADED_IN; } /** * Sets the TimeShiftListener * * @param listener The instance of {@link TimeShiftListener}. */ public void setTimeShiftListener(TimeShiftListener listener) { mTimeShiftListener = listener; } private void setTimeShiftAvailable(boolean isTimeShiftAvailable) { if (mTimeShiftAvailable == isTimeShiftAvailable) { return; } mTimeShiftAvailable = isTimeShiftAvailable; if (isTimeShiftAvailable) { mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { @Override public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { if (mTimeShiftListener != null && mCurrentChannel != null && mCurrentChannel.getInputId().equals(inputId)) { mTimeShiftListener.onRecordStartTimeChanged(timeMs); } } @Override public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { mTimeShiftCurrentPositionMs = timeMs; } }); } else { mTvView.setTimeShiftPositionCallback(null); } if (mTimeShiftListener != null) { mTimeShiftListener.onAvailabilityChanged(); } } /** * Returns if the time shift is available for the current channel. */ public boolean isTimeShiftAvailable() { return mTimeShiftAvailable; } /** * Plays the media, if the current input supports time-shifting. */ public void timeshiftPlay() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) { return; } mTvView.timeShiftResume(); } /** * Pauses the media, if the current input supports time-shifting. */ public void timeshiftPause() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) { return; } mTvView.timeShiftPause(); } /** * Rewinds the media with the given speed, if the current input supports time-shifting. * * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. */ public void timeshiftRewind(int speed) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } else { if (speed <= 0) { throw new IllegalArgumentException("The speed should be a positive integer."); } mTimeShiftState = TIME_SHIFT_STATE_REWIND; PlaybackParams params = new PlaybackParams(); params.setSpeed(speed * -1); mTvView.timeShiftSetPlaybackParams(params); } } /** * Fast-forwards the media with the given speed, if the current input supports time-shifting. * * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. */ public void timeshiftFastForward(int speed) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } else { if (speed <= 0) { throw new IllegalArgumentException("The speed should be a positive integer."); } mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD; PlaybackParams params = new PlaybackParams(); params.setSpeed(speed); mTvView.timeShiftSetPlaybackParams(params); } } /** * Seek to the given time position. * * @param timeMs The time in milliseconds to seek to. */ public void timeshiftSeekTo(long timeMs) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } mTvView.timeShiftSeekTo(timeMs); } /** * Returns the current playback position in milliseconds. */ public long timeshiftGetCurrentPositionMs() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } if (DEBUG) { Log.d(TAG, "timeshiftGetCurrentPositionMs: current position =" + Utils.toTimeString(mTimeShiftCurrentPositionMs)); } return mTimeShiftCurrentPositionMs; } /** * Used to receive the time-shift events. */ public static abstract class TimeShiftListener { /** * Called when the availability of the time-shift for the current channel has been changed. * It should be guaranteed that this is called only when the availability is really changed. */ public abstract void onAvailabilityChanged(); /** * Called when the record start time has been changed. * This is not called when the recorded programs is played. */ public abstract void onRecordStartTimeChanged(long recordStartTimeMs); } /** * A listener which receives the notification when the screen is blocked/unblocked. */ public static abstract class OnScreenBlockingChangedListener { /** * Called when the screen is blocked/unblocked. */ public abstract void onScreenBlockingChanged(boolean blocked); } private class InternetCheckTask extends AsyncTask { @Override protected Boolean doInBackground(Void... params) { return NetworkUtils.isNetworkAvailable(mConnectivityManager); } @Override protected void onPostExecute(Boolean networkAvailable) { mInternetCheckTask = null; if (!mVideoAvailable && !networkAvailable && isAttachedToWindow() && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) { mHideScreenView.setImageVisibility(true); mHideScreenView.setImage(R.drawable.ic_sad_cloud); mHideScreenView.setText(R.string.tvview_msg_no_internet_connection); } } } }