/*
 * 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.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.PlaybackParams;
import android.media.tv.AitInfo;
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.interactive.TvInteractiveAppView;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
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.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.TvViewSession;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.CommonConstants;
import com.android.tv.common.compat.TvInputConstantCompat;
import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
import com.android.tv.common.util.DurationTimer;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.features.TvFeatures;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.ui.api.TunableTvViewPlayingApi;
import com.android.tv.util.NetworkUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageLoader;

import com.android.tv.common.flags.LegacyFlags;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/** Includes the real {@link AppLayerTvView} handling tuning, block and other display events. */
public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvViewPlayingApi {
    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;
    public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
    public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
    private final AccessibilityManager mAccessibilityManager;

    @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 =
            CommonConstants.BASE_PACKAGE + ".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;

    private AppLayerTvView mTvView;
    private TvViewSession mTvViewSession;
    @Nullable private Channel mCurrentChannel;
    private TvInputManagerHelper mInputManagerHelper;
    private ContentRatingsManager mContentRatingsManager;
    private ParentalControlSettings mParentalControlSettings;
    private ProgramDataManager mProgramDataManager;
    @Nullable private WatchedHistoryManager mWatchedHistoryManager;
    private boolean mStarted;
    private String mTagetInputId;
    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 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 final boolean mCanModifyParentalControls;
    private boolean mIsUnderShrunken;

    @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 to hide the real TV view underlying. It may be used to enforce parental
    // control, or hide screen when there's no video available and show appropriate information.
    private final BlockScreenView mBlockScreenView;
    private final int mTuningImageColorFilter;

    // A spinner view to show buffering status.
    private final View mBufferingSpinnerView;

    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 int mChannelSignalStrength;
    private TvInteractiveAppView mTvIAppView;

    private final TvInputCallbackCompat mCallback =
            new TvInputCallbackCompat() {
                @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<TvTrackInfo> tracks) {
                    mHasClosedCaption = false;
                    for (TvTrackInfo track : tracks) {
                        if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
                            mHasClosedCaption = true;
                            break;
                        }
                    }
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
                    }
                }

                @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<TvTrackInfo> 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 = (float) mVideoWidth
                                                    / mVideoHeight;
                                            mVideoDisplayAspectRatio *= videoPixelAspectRatio > 0 ?
                                                    videoPixelAspectRatio : 1;
                                        }
                                    } else if (type == TvTrackInfo.TYPE_AUDIO) {
                                        mAudioChannelCount = track.getAudioChannelCount();
                                    }
                                    trackFound = true;
                                    break;
                                }
                            }
                        }
                        if (!trackFound) {
                            Log.w(TAG, "Invalid track ID: " + trackId);
                        }
                    }
                    if (mOnTuneListener != null) {
                        // should not change audio track automatically when an audio track or a
                        // subtitle track is selected
                        mOnTuneListener.onStreamInfoChanged(
                                TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO);
                    }
                }

                @Override
                public void onVideoAvailable(String inputId) {
                    if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}");
                    Debug.getTimer(Debug.TAG_START_UP_TIMER)
                            .log(
                                    "Start up of TV app ends,"
                                            + " TunableTvView.onVideoAvailable resets timer");
                    Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
                    Debug.removeTimer(Debug.TAG_START_UP_TIMER);
                    mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE;
                    updateBlockScreenAndMuting();
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
                    }
                }

                @Override
                public void onVideoUnavailable(String inputId, int reason) {
                    if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
                            && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
                        Debug.getTimer(Debug.TAG_START_UP_TIMER)
                                .log(
                                        "TunableTvView.onVideoUnAvailable reason = ("
                                                + reason
                                                + ") and removes timer");
                        Debug.removeTimer(Debug.TAG_START_UP_TIMER);
                    } else {
                        Debug.getTimer(Debug.TAG_START_UP_TIMER)
                                .log("TunableTvView.onVideoUnAvailable reason = (" + reason + ")");
                    }
                    mVideoUnavailableReason = reason;
                    if (closePipIfNeeded()) {
                        return;
                    }
                    updateBlockScreenAndMuting();
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
                    }
                    switch (reason) {
                        case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
                        case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
                        case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
                        case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
                            mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
                            break;
                        default:
                            // do nothing
                    }
                }

                @Override
                public void onContentAllowed(String inputId) {
                    mBlockedContentRating = null;
                    updateBlockScreenAndMuting();
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onContentAllowed();
                    }
                }

                @Override
                public void onContentBlocked(String inputId, TvContentRating rating) {
                    if (rating != null && rating.equals(mBlockedContentRating)) {
                        return;
                    }
                    mBlockedContentRating = rating;
                    if (closePipIfNeeded()) {
                        return;
                    }
                    updateBlockScreenAndMuting();
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onContentBlocked();
                    }
                }

                @Override
                public void onTimeShiftStatusChanged(String inputId, int status) {
                    if (DEBUG) {
                        Log.d(
                                TAG,
                                "onTimeShiftStatusChanged: {inputId="
                                        + inputId
                                        + ", status="
                                        + status
                                        + "}");
                    }
                    boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
                    setTimeShiftAvailable(available);
                }

                @Override
                public void onSignalStrength(String inputId, int value) {
                    mChannelSignalStrength = value;
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onChannelSignalStrength();
                    }
                }

                @TargetApi(Build.VERSION_CODES.TIRAMISU)
                @Override
                public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {
                    if (!TvFeatures.HAS_TIAF.isEnabled(getContext())) {
                        return;
                    }
                    if (DEBUG) {
                        Log.d(TAG,
                                "onAitInfoUpdated: {inputId="
                                + inputId
                                + ", AitInfo=("
                                + aitInfo
                                +")}");
                    }
                    if (mOnTuneListener != null) {
                        mOnTuneListener.onAitInfoUpdated(inputId, aitInfo);
                    }
                }
            };

    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);

        TvSingletons tvSingletons = TvSingletons.getSingletons(context);
        if (CommonFeatures.DVR.isEnabled(context)) {
            mInputSessionManager = tvSingletons.getInputSessionManager();
        } else {
            mInputSessionManager = null;
        }
        mInputManager = tvSingletons.getTvInputManagerHelper();
        mConnectivityManager =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
        mTracker = tvSingletons.getTracker();
        mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
        mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
        mBlockScreenView.addInfoFadeInAnimationListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        adjustBlockScreenSpacingAndText();
                    }
                });

        mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
        mTuningImageColorFilter =
                getResources().getColor(R.color.tvview_block_image_color_filter, null);
        mDimScreenView = findViewById(R.id.dim_screen);
        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();
                                }
                            }
                        });
        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
    }
    public void initialize(
            ProgramDataManager programDataManager,
            TvInputManagerHelper tvInputManagerHelper,
            LegacyFlags legacyFlags) {
        initialize(programDataManager, tvInputManagerHelper, legacyFlags, null);
    }

    public void initialize(
            ProgramDataManager programDataManager,
            TvInputManagerHelper tvInputManagerHelper,
            LegacyFlags legacyFlags,
            TvInteractiveAppView tvIAppView) {
        mTvView = findViewById(R.id.tv_view);
        mTvView.setUseSecureSurface(!BuildConfig.ENG && !legacyFlags.enableDeveloperFeatures());

        mProgramDataManager = programDataManager;
        mInputManagerHelper = tvInputManagerHelper;
        mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
        mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings();
        mTvIAppView = tvIAppView;
        if (mInputSessionManager != null) {
            mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, this, mCallback);
        } else {
            mTvView.setCallback(mCallback);
        }
    }

    public void start() {
        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);
            }
            mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
            updateBlockScreenAndMuting();
        }
    }

    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;
        }
    }

    /** Resets TV view. */
    public void reset() {
        resetInternal();
        mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED;
        updateBlockScreenAndMuting();
    }

    /** Resets 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() {
        if (PermissionUtils.hasChangeHdmiCecActiveSource(getContext())) {
            mTvView.setMain();
        }
    }

    public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
        mWatchedHistoryManager = watchedHistoryManager;
    }

    /** Sets if the TunableTvView is under shrunken. */
    public void setIsUnderShrunken(boolean isUnderShrunken) {
        mIsUnderShrunken = isUnderShrunken;
    }

    public int getChannelSignalStrength() {
        return mChannelSignalStrength;
    }

    public void resetChannelSignalStrength() {
        mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
    }

    @Override
    public boolean isPlaying() {
        return mStarted;
    }

    /** Called when parental control is changed. */
    public void onParentalControlChanged(boolean enabled) {
        mParentControlEnabled = enabled;
        if (!enabled) {
            // Unblock screen immediately if parental control is turned off
            updateBlockScreenAndMuting();
        }
    }

    /**
     * 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) {
        Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo");
        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)) {
            mTagetInputId = inputInfo.getId();
            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;
        mBlockedContentRating = null;
        mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
        // To reduce the IPCs, unregister the callback here and register it when necessary.
        mTvView.setTimeShiftPositionCallback(null);
        setTimeShiftAvailable(false);
        mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
        if (mTvViewSession != null) {
            mTvViewSession.tune(channel, params, listener);
        } else {
            mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);
        }
        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.
            SurfaceView surfaceView = getSurfaceView();
            if (surfaceView != null) {
                surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
            } else {
                Log.w(TAG, "Failed to set fixed size for surface view: Null surface view");
            }
        }
        updateBlockScreenAndMuting();
        if (mOnTuneListener != null) {
            mOnTuneListener.onStreamInfoChanged(this, true);
        }
        return true;
    }

    @Override
    @Nullable
    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;
    }

    @Override
    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);
        }
    }

    @Override
    public float getStreamVolume() {
        return mIsMuted
                ? 0
                : mVolume;
    }

    /**
     * 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.
     *
     * <p>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, boolean allowAutoSelectionOfTrack);

        void onChannelRetuned(Uri channel);

        void onContentBlocked();

        void onContentAllowed();

        void onChannelSignalStrength();

        @TargetApi(Build.VERSION_CODES.TIRAMISU)
        void onAitInfoUpdated(String inputId, AitInfo aitInfo);
    }

    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 mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE;
    }

    @Override
    public boolean isVideoOrAudioAvailable() {
        return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE
                || mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY;
    }

    @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);
    }

    @VisibleForTesting
    public void setOnTuneListener(OnTuneListener listener) {
        mOnTuneListener = listener;
    }

    public List<TvTrackInfo> 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);
    }

    /**
     * Gets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView},
     * which is the actual view to play live TV videos.
     */
    public MarginLayoutParams getTvViewLayoutParams() {
        return (MarginLayoutParams) mTvView.getLayoutParams();
    }

    /**
     * Sets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView},
     * which is the actual view to play live TV videos.
     */
    public void setTvViewLayoutParams(MarginLayoutParams layoutParams) {
        mTvView.setLayoutParams(layoutParams);
    }

    /**
     * Gets the underlying {@link AppLayerTvView}, which is the actual view to play live TV videos.
     */
    public TvView getTvView() {
        return mTvView;
    }

    /**
     * Returns if the screen is blocked, either by {@link #blockOrUnblockScreen(boolean)} or because
     * the content is blocked.
     */
    public boolean isBlocked() {
        return isScreenBlocked() || isContentBlocked();
    }

    /** Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. */
    public boolean isScreenBlocked() {
        return mScreenBlocked;
    }

    /** Returns {@code true} if the content is blocked, otherwise {@code false}. */
    public boolean isContentBlocked() {
        return mBlockedContentRating != null;
    }

    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;
    }

    /**
     * Blocks/unblocks 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.
     *
     * @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock.
     */
    public void blockOrUnblockScreen(boolean blockOrUnblock) {
        if (mScreenBlocked == blockOrUnblock) {
            return;
        }
        mScreenBlocked = blockOrUnblock;
        if (closePipIfNeeded()) {
            return;
        }
        updateBlockScreenAndMuting();
        if (mOnScreenBlockedListener != null) {
            mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock);
        }
    }

    @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) {
        if (mBlockScreenType != type) {
            mBlockScreenType = type;
            updateBlockScreen(true);
        }
    }

    private void updateBlockScreen(boolean animation) {
        mBlockScreenView.endAnimations();
        int blockReason =
                (mScreenBlocked || mBlockedContentRating != null) && mParentControlEnabled
                        ? VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED
                        : mVideoUnavailableReason;
        if (blockReason != VIDEO_UNAVAILABLE_REASON_NONE) {
            mBufferingSpinnerView.setVisibility(
                    blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
                                    || blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
                            ? VISIBLE
                            : GONE);
            if (!animation) {
                adjustBlockScreenSpacingAndText();
            }
            if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
                return;
            }
            mBlockScreenView.setVisibility(VISIBLE);
            if (shouldShowEmptyInputStatusBlock()){
                mBlockScreenView.setEmptyInputStatusInputInfo(mInputInfo);
                mBlockScreenView.setEmptyInputStatusBlockVisibility(true);
            } else {
                mBlockScreenView.setEmptyInputStatusBlockVisibility(false);
            }
            if (mTvIAppView != null) {
                mTvIAppView.setVisibility(INVISIBLE);
            }
            mBlockScreenView.setBackgroundImage(null);
            if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) {
                mBlockScreenView.setIconVisibility(true);
                if (!mCanModifyParentalControls) {
                    mBlockScreenView.setIconImage(R.drawable.ic_message_lock_no_permission);
                    mBlockScreenView.setIconScaleType(ImageView.ScaleType.CENTER);
                } else {
                    mBlockScreenView.setIconImage(R.drawable.ic_message_lock);
                    mBlockScreenView.setIconScaleType(ImageView.ScaleType.FIT_CENTER);
                }
            } else {
                if (mInternetCheckTask != null) {
                    mInternetCheckTask.cancel(true);
                    mInternetCheckTask = null;
                }
                mBlockScreenView.setIconVisibility(false);
                if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) {
                    showImageForTuningIfNeeded();
                } else if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
                        && mCurrentChannel != null
                        && !mCurrentChannel.isPhysicalTunerChannel()) {
                    mInternetCheckTask = new InternetCheckTask();
                    mInternetCheckTask.execute();
                }
            }
            mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
        } else {
            mBufferingSpinnerView.setVisibility(GONE);
            if (mBlockScreenView.getVisibility() == VISIBLE) {
                mBlockScreenView.fadeOut();
            }
            if (mTvIAppView != null) {
                mTvIAppView.setVisibility(VISIBLE);
            }
        }
    }

    private void adjustBlockScreenSpacingAndText() {
        mBlockScreenView.setSpacing(mBlockScreenType);
        String text = getBlockScreenText();
        if (text != null) {
            mBlockScreenView.setInfoText(text);
        }
        mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled);
    }

    /**
     * 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() {
        // TODO: add a test for this method
        Resources res = getResources();
        boolean isA11y = mAccessibilityManager.isEnabled();

        if (mScreenBlocked && mParentControlEnabled) {
            switch (mBlockScreenType) {
                case BLOCK_SCREEN_TYPE_NO_UI:
                case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
                    return "";
                case BLOCK_SCREEN_TYPE_NORMAL:
                    if (mCanModifyParentalControls) {
                        return res.getString(
                                isA11y
                                        ? R.string.tvview_channel_locked_talkback
                                        : R.string.tvview_channel_locked);
                    } else {
                        return res.getString(R.string.tvview_channel_locked_no_permission);
                    }
            }
        } else if (mBlockedContentRating != null && mParentControlEnabled) {
            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 res.getString(R.string.shrunken_tvview_content_locked);
                    } else if (name.equals(res.getString(R.string.unrated_rating_name))) {
                        return res.getString(R.string.shrunken_tvview_content_locked_unrated);
                    } else {
                        return res.getString(R.string.shrunken_tvview_content_locked_format, name);
                    }
                case BLOCK_SCREEN_TYPE_NORMAL:
                    if (TextUtils.isEmpty(name)) {
                        if (mCanModifyParentalControls) {
                            return res.getString(
                                    isA11y
                                            ? R.string.tvview_content_locked_talkback
                                            : R.string.tvview_content_locked);
                        } else {
                            return res.getString(R.string.tvview_content_locked_no_permission);
                        }
                    } else {
                        if (mCanModifyParentalControls) {
                            return name.equals(res.getString(R.string.unrated_rating_name))
                                    ? res.getString(
                                            isA11y
                                                    ? R.string
                                                            .tvview_content_locked_unrated_talkback
                                                    : R.string.tvview_content_locked_unrated)
                                    : res.getString(
                                            isA11y
                                                    ? R.string.tvview_content_locked_format_talkback
                                                    : R.string.tvview_content_locked_format,
                                            name);
                        } else {
                            return name.equals(res.getString(R.string.unrated_rating_name))
                                    ? res.getString(
                                            R.string.tvview_content_locked_unrated_no_permission)
                                    : res.getString(
                                            R.string.tvview_content_locked_format_no_permission,
                                            name);
                        }
                    }
            }
        } else if (mVideoUnavailableReason != VIDEO_UNAVAILABLE_REASON_NONE) {
            switch (mVideoUnavailableReason) {
                case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
                    return res.getString(R.string.tvview_msg_audio_only);
                case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
                    return res.getString(R.string.tvview_msg_weak_signal);
                case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
                    return res.getString(R.string.msg_channel_unavailable_not_connected);
                case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
                    return getTuneConflictMessage();
                default:
                    return "";
            }
        }
        return null;
    }

    private boolean closePipIfNeeded() {
        if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getContext())
                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
                && ((Activity) getContext()).isInPictureInPictureMode()
                && (mScreenBlocked
                        || mBlockedContentRating != null
                        || mVideoUnavailableReason
                                == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
                        || mVideoUnavailableReason
                                == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) {
            ((Activity) getContext()).finish();
            return true;
        }
        return false;
    }

    private void updateBlockScreenAndMuting() {
        updateBlockScreen(false);
        updateMuteStatus();
    }

    private boolean shouldShowImageForTuning() {
        if (mVideoUnavailableReason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
                || mScreenBlocked
                || mBlockedContentRating != null
                || mCurrentChannel == null
                || mIsUnderShrunken
                || getWidth() == 0
                || getWidth() == 0
                || !isBundledInput()) {
            return false;
        }
        Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
        if (currentProgram == null) {
            return false;
        }
        TvContentRating rating =
                mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings());
        return !(mParentControlEnabled && rating != null);
    }

    private void showImageForTuningIfNeeded() {
        if (shouldShowImageForTuning()) {
            if (mCurrentChannel == null) {
                return;
            }
            Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
            if (currentProgram != null) {
                currentProgram.loadPosterArt(
                        getContext(),
                        getWidth(),
                        getHeight(),
                        createProgramPosterArtCallback(mCurrentChannel.getId()));
            }
        }
    }

    private String getTuneConflictMessage() {
        if (mTagetInputId != null) {
            TvInputInfo input = mInputManager.getTvInputInfo(mTagetInputId);
            Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(mTagetInputId);
            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 updateMuteStatus() {
        // Workaround: BaseTunerTvInputService uses AC3 pass-through implementation, which disables
        // audio tracks to enforce the mute request. We don't want to send mute request if we are
        // not going to block the screen to prevent the video jankiness resulted by disabling audio
        // track before the playback is started. In other way, we should send unmute request before
        // the playback is started, because TunerTvInput will remember the muted state and mute
        // itself right way when the playback is going to be started, which results the initial
        // jankiness, too.
        boolean isBundledInput = isBundledInput();
        if ((isBundledInput || isVideoOrAudioAvailable())
                && !mScreenBlocked
                && mBlockedContentRating == null) {
            if (mIsMuted) {
                mIsMuted = false;
                mTvView.setStreamVolume(mVolume);
            }
        } else {
            if (!mIsMuted) {
                if ((mInputInfo == null || isBundledInput)
                        && !mScreenBlocked
                        && mBlockedContentRating == null) {
                    return;
                }
                mIsMuted = true;
                mTvView.setStreamVolume(0);
            }
        }
    }

    private boolean shouldShowEmptyInputStatusBlock() {
        return TvFeatures.USE_GTV_LIVETV_V2.isEnabled(getContext()) &&
                (mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL ||
                        mVideoUnavailableReason ==
                                CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED);
    }

    private boolean isBundledInput() {
        return mInputInfo != null
                && mInputInfo.getType() == TvInputInfo.TYPE_TUNER
                && CommonUtils.isBundledInput(mInputInfo.getId());
    }

    /** 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(
                        () -> {
                            mFadeState = FADING_OUT;
                            mActionAfterFade = actionAfterFade;
                        })
                .withEndAction(() -> 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(
                        () -> {
                            mFadeState = FADING_IN;
                            mActionAfterFade = actionAfterFade;
                        })
                .withEndAction(
                        () -> {
                            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}.
     */
    @Override
    public void setTimeShiftListener(TimeShiftListener listener) {
        mTimeShiftListener = listener;
    }

    public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) {
        mBlockScreenView.setInfoTextOnClickListener(onClickListener);
    }

    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. */
    @Override
    public boolean isTimeShiftAvailable() {
        return mTimeShiftAvailable;
    }

    /** Plays the media, if the current input supports time-shifting. */
    @Override
    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. */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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. */
    @Override
    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;
    }

    private ImageLoader.ImageLoaderCallback<BlockScreenView> createProgramPosterArtCallback(
            final long channelId) {
        return new ImageLoader.ImageLoaderCallback<BlockScreenView>(mBlockScreenView) {
            @Override
            public void onBitmapLoaded(BlockScreenView view, @Nullable Bitmap posterArt) {
                if (posterArt == null
                        || getCurrentChannel() == null
                        || channelId != getCurrentChannel().getId()
                        || !shouldShowImageForTuning()) {
                    return;
                }
                Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt);
                drawablePosterArt
                        .mutate()
                        .setColorFilter(mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER);
                view.setBackgroundImage(drawablePosterArt);
            }
        };
    }

    /** A listener which receives the notification when the screen is blocked/unblocked. */
    public abstract static class OnScreenBlockingChangedListener {
        /** Called when the screen is blocked/unblocked. */
        public abstract void onScreenBlockingChanged(boolean blocked);
    }

    private class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
        @Override
        protected Boolean doInBackground(Void... params) {
            return NetworkUtils.isNetworkAvailable(mConnectivityManager);
        }

        @Override
        protected void onPostExecute(Boolean networkAvailable) {
            mInternetCheckTask = null;
            if (!networkAvailable
                    && isAttachedToWindow()
                    && !mScreenBlocked
                    && mBlockedContentRating == null
                    && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
                mBlockScreenView.setIconVisibility(true);
                mBlockScreenView.setIconImage(R.drawable.ic_sad_cloud);
                mBlockScreenView.setInfoText(R.string.tvview_msg_no_internet_connection);
            }
        }
    }
}
