Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.TimeInterpolator;
     22 import android.annotation.SuppressLint;
     23 import android.content.Context;
     24 import android.content.pm.PackageManager;
     25 import android.media.PlaybackParams;
     26 import android.media.tv.TvContentRating;
     27 import android.media.tv.TvInputInfo;
     28 import android.media.tv.TvInputManager;
     29 import android.media.tv.TvTrackInfo;
     30 import android.media.tv.TvView;
     31 import android.media.tv.TvView.OnUnhandledInputEventListener;
     32 import android.media.tv.TvView.TvInputCallback;
     33 import android.net.ConnectivityManager;
     34 import android.net.Uri;
     35 import android.os.AsyncTask;
     36 import android.os.Bundle;
     37 import android.support.annotation.IntDef;
     38 import android.support.annotation.NonNull;
     39 import android.support.annotation.Nullable;
     40 import android.support.annotation.UiThread;
     41 import android.support.v4.os.BuildCompat;
     42 import android.text.TextUtils;
     43 import android.text.format.DateUtils;
     44 import android.util.AttributeSet;
     45 import android.util.Log;
     46 import android.view.KeyEvent;
     47 import android.view.MotionEvent;
     48 import android.view.SurfaceView;
     49 import android.view.View;
     50 import android.view.ViewGroup;
     51 import android.widget.FrameLayout;
     52 import android.widget.ImageView;
     53 
     54 import com.android.tv.ApplicationSingletons;
     55 import com.android.tv.InputSessionManager;
     56 import com.android.tv.InputSessionManager.TvViewSession;
     57 import com.android.tv.R;
     58 import com.android.tv.TvApplication;
     59 import com.android.tv.analytics.DurationTimer;
     60 import com.android.tv.analytics.Tracker;
     61 import com.android.tv.common.feature.CommonFeatures;
     62 import com.android.tv.data.Channel;
     63 import com.android.tv.data.StreamInfo;
     64 import com.android.tv.data.WatchedHistoryManager;
     65 import com.android.tv.parental.ContentRatingsManager;
     66 import com.android.tv.recommendation.NotificationService;
     67 import com.android.tv.util.NetworkUtils;
     68 import com.android.tv.util.PermissionUtils;
     69 import com.android.tv.util.TvInputManagerHelper;
     70 import com.android.tv.util.Utils;
     71 
     72 import java.lang.annotation.Retention;
     73 import java.lang.annotation.RetentionPolicy;
     74 import java.util.List;
     75 
     76 public class TunableTvView extends FrameLayout implements StreamInfo {
     77     private static final boolean DEBUG = false;
     78     private static final String TAG = "TunableTvView";
     79 
     80     public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1;
     81     public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2;
     82 
     83     @Retention(RetentionPolicy.SOURCE)
     84     @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
     85     public @interface BlockScreenType {}
     86     public static final int BLOCK_SCREEN_TYPE_NO_UI = 0;
     87     public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1;
     88     public static final int BLOCK_SCREEN_TYPE_NORMAL = 2;
     89 
     90     private static final String PERMISSION_RECEIVE_INPUT_EVENT =
     91             "com.android.tv.permission.RECEIVE_INPUT_EVENT";
     92 
     93     @Retention(RetentionPolicy.SOURCE)
     94     @IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE,
     95             TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD })
     96     private @interface TimeShiftState {}
     97     private static final int TIME_SHIFT_STATE_NONE = 0;
     98     private static final int TIME_SHIFT_STATE_PLAY = 1;
     99     private static final int TIME_SHIFT_STATE_PAUSE = 2;
    100     private static final int TIME_SHIFT_STATE_REWIND = 3;
    101     private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4;
    102 
    103     private static final int FADED_IN = 0;
    104     private static final int FADED_OUT = 1;
    105     private static final int FADING_IN = 2;
    106     private static final int FADING_OUT = 3;
    107 
    108     // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR.
    109     private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f;
    110 
    111     private AppLayerTvView mTvView;
    112     private TvViewSession mTvViewSession;
    113     private Channel mCurrentChannel;
    114     private TvInputManagerHelper mInputManagerHelper;
    115     private ContentRatingsManager mContentRatingsManager;
    116     @Nullable
    117     private WatchedHistoryManager mWatchedHistoryManager;
    118     private boolean mStarted;
    119     private TvInputInfo mInputInfo;
    120     private OnTuneListener mOnTuneListener;
    121     private int mVideoWidth;
    122     private int mVideoHeight;
    123     private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
    124     private float mVideoFrameRate;
    125     private float mVideoDisplayAspectRatio;
    126     private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
    127     private boolean mHasClosedCaption = false;
    128     private boolean mVideoAvailable;
    129     private boolean mScreenBlocked;
    130     private OnScreenBlockingChangedListener mOnScreenBlockedListener;
    131     private TvContentRating mBlockedContentRating;
    132     private int mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED;
    133     private boolean mCanReceiveInputEvent;
    134     private boolean mIsMuted;
    135     private float mVolume;
    136     private boolean mParentControlEnabled;
    137     private int mFixedSurfaceWidth;
    138     private int mFixedSurfaceHeight;
    139     private boolean mIsPip;
    140     private int mScreenHeight;
    141     private int mShrunkenTvViewHeight;
    142     private final boolean mCanModifyParentalControls;
    143 
    144     @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE;
    145     private TimeShiftListener mTimeShiftListener;
    146     private boolean mTimeShiftAvailable;
    147     private long mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
    148 
    149     private final Tracker mTracker;
    150     private final DurationTimer mChannelViewTimer = new DurationTimer();
    151     private InternetCheckTask mInternetCheckTask;
    152 
    153     // A block screen view which has lock icon with black background.
    154     // This indicates that user's action is needed to play video.
    155     private final BlockScreenView mBlockScreenView;
    156 
    157     // A View to hide screen when there's problem in video playback.
    158     private final BlockScreenView mHideScreenView;
    159 
    160     // A View to block screen until onContentAllowed is received if parental control is on.
    161     private final View mBlockScreenForTuneView;
    162 
    163     // A spinner view to show buffering status.
    164     private final View mBufferingSpinnerView;
    165 
    166     // A View for fade-in/out animation
    167     private final View mDimScreenView;
    168     private int mFadeState = FADED_IN;
    169     private Runnable mActionAfterFade;
    170 
    171     @BlockScreenType private int mBlockScreenType;
    172 
    173     private final TvInputManagerHelper mInputManager;
    174     private final ConnectivityManager mConnectivityManager;
    175     private final InputSessionManager mInputSessionManager;
    176 
    177     private final TvInputCallback mCallback = new TvInputCallback() {
    178         @Override
    179         public void onConnectionFailed(String inputId) {
    180             Log.w(TAG, "Failed to bind an input");
    181             mTracker.sendInputConnectionFailure(inputId);
    182             Channel channel = mCurrentChannel;
    183             mCurrentChannel = null;
    184             mInputInfo = null;
    185             mCanReceiveInputEvent = false;
    186             if (mOnTuneListener != null) {
    187                 // If tune is called inside onTuneFailed, mOnTuneListener will be set to
    188                 // a new instance. In order to avoid to clear the new mOnTuneListener,
    189                 // we copy mOnTuneListener to l and clear mOnTuneListener before
    190                 // calling onTuneFailed.
    191                 OnTuneListener listener = mOnTuneListener;
    192                 mOnTuneListener = null;
    193                 listener.onTuneFailed(channel);
    194             }
    195         }
    196 
    197         @Override
    198         public void onDisconnected(String inputId) {
    199             Log.w(TAG, "Session is released by crash");
    200             mTracker.sendInputDisconnected(inputId);
    201             Channel channel = mCurrentChannel;
    202             mCurrentChannel = null;
    203             mInputInfo = null;
    204             mCanReceiveInputEvent = false;
    205             if (mOnTuneListener != null) {
    206                 OnTuneListener listener = mOnTuneListener;
    207                 mOnTuneListener = null;
    208                 listener.onUnexpectedStop(channel);
    209             }
    210         }
    211 
    212         @Override
    213         public void onChannelRetuned(String inputId, Uri channelUri) {
    214             if (DEBUG) {
    215                 Log.d(TAG, "onChannelRetuned(inputId=" + inputId + ", channelUri="
    216                         + channelUri + ")");
    217             }
    218             if (mOnTuneListener != null) {
    219                 mOnTuneListener.onChannelRetuned(channelUri);
    220             }
    221         }
    222 
    223         @Override
    224         public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
    225             mHasClosedCaption = false;
    226             for (TvTrackInfo track : tracks) {
    227                 if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
    228                     mHasClosedCaption = true;
    229                     break;
    230                 }
    231             }
    232             if (mOnTuneListener != null) {
    233                 mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
    234             }
    235         }
    236 
    237         @Override
    238         public void onTrackSelected(String inputId, int type, String trackId) {
    239             if (trackId == null) {
    240                 // A track is unselected.
    241                 if (type == TvTrackInfo.TYPE_VIDEO) {
    242                     mVideoWidth = 0;
    243                     mVideoHeight = 0;
    244                     mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
    245                     mVideoFrameRate = 0f;
    246                     mVideoDisplayAspectRatio = 0f;
    247                 } else if (type == TvTrackInfo.TYPE_AUDIO) {
    248                     mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
    249                 }
    250             } else {
    251                 List<TvTrackInfo> tracks = getTracks(type);
    252                 boolean trackFound = false;
    253                 if (tracks != null) {
    254                     for (TvTrackInfo track : tracks) {
    255                         if (track.getId().equals(trackId)) {
    256                             if (type == TvTrackInfo.TYPE_VIDEO) {
    257                                 mVideoWidth = track.getVideoWidth();
    258                                 mVideoHeight = track.getVideoHeight();
    259                                 mVideoFormat = Utils.getVideoDefinitionLevelFromSize(
    260                                         mVideoWidth, mVideoHeight);
    261                                 mVideoFrameRate = track.getVideoFrameRate();
    262                                 if (mVideoWidth <= 0 || mVideoHeight <= 0) {
    263                                     mVideoDisplayAspectRatio = 0.0f;
    264                                 } else {
    265                                     float VideoPixelAspectRatio =
    266                                             track.getVideoPixelAspectRatio();
    267                                     mVideoDisplayAspectRatio = VideoPixelAspectRatio
    268                                             * mVideoWidth / mVideoHeight;
    269                                 }
    270                             } else if (type == TvTrackInfo.TYPE_AUDIO) {
    271                                 mAudioChannelCount = track.getAudioChannelCount();
    272                             }
    273                             trackFound = true;
    274                             break;
    275                         }
    276                     }
    277                 }
    278                 if (!trackFound) {
    279                     Log.w(TAG, "Invalid track ID: " + trackId);
    280                 }
    281             }
    282             if (mOnTuneListener != null) {
    283                 mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
    284             }
    285         }
    286 
    287         @Override
    288         public void onVideoAvailable(String inputId) {
    289             unhideScreenByVideoAvailability();
    290             if (mOnTuneListener != null) {
    291                 mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
    292             }
    293         }
    294 
    295         @Override
    296         public void onVideoUnavailable(String inputId, int reason) {
    297             hideScreenByVideoAvailability(inputId, reason);
    298             if (mOnTuneListener != null) {
    299                 mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
    300             }
    301             switch (reason) {
    302                 case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
    303                 case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
    304                 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
    305                     mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
    306                 default:
    307                     // do nothing
    308             }
    309         }
    310 
    311         @Override
    312         public void onContentAllowed(String inputId) {
    313             mBlockScreenForTuneView.setVisibility(View.GONE);
    314             unblockScreenByContentRating();
    315             if (mOnTuneListener != null) {
    316                 mOnTuneListener.onContentAllowed();
    317             }
    318         }
    319 
    320         @Override
    321         public void onContentBlocked(String inputId, TvContentRating rating) {
    322             blockScreenByContentRating(rating);
    323             if (mOnTuneListener != null) {
    324                 mOnTuneListener.onContentBlocked();
    325             }
    326         }
    327 
    328         @Override
    329         public void onTimeShiftStatusChanged(String inputId, int status) {
    330             boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
    331             setTimeShiftAvailable(available);
    332         }
    333     };
    334 
    335     public TunableTvView(Context context) {
    336         this(context, null);
    337     }
    338 
    339     public TunableTvView(Context context, AttributeSet attrs) {
    340         this(context, attrs, 0);
    341     }
    342 
    343     public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) {
    344         this(context, attrs, defStyleAttr, 0);
    345     }
    346 
    347     public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    348         super(context, attrs, defStyleAttr, defStyleRes);
    349         inflate(getContext(), R.layout.tunable_tv_view, this);
    350 
    351         ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
    352         if (CommonFeatures.DVR.isEnabled(context)) {
    353             mInputSessionManager = appSingletons.getInputSessionManager();
    354         } else {
    355             mInputSessionManager = null;
    356         }
    357         mInputManager = appSingletons.getTvInputManagerHelper();
    358         mConnectivityManager = (ConnectivityManager) context
    359                 .getSystemService(Context.CONNECTIVITY_SERVICE);
    360         mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
    361         mTracker = appSingletons.getTracker();
    362         mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
    363         mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
    364         if (!mCanModifyParentalControls) {
    365             mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission);
    366             mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER);
    367         } else {
    368             mBlockScreenView.setImage(R.drawable.ic_message_lock);
    369         }
    370         mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview);
    371         mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() {
    372             @Override
    373             public void onAnimationEnd(Animator animation) {
    374                 adjustBlockScreenSpacingAndText();
    375             }
    376         });
    377 
    378         mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen);
    379         mHideScreenView.setImageVisibility(false);
    380         mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
    381         mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune);
    382         mDimScreenView = findViewById(R.id.dim);
    383         mDimScreenView.animate().setListener(new AnimatorListenerAdapter() {
    384             @Override
    385             public void onAnimationEnd(Animator animation) {
    386                 if (mActionAfterFade != null) {
    387                     mActionAfterFade.run();
    388                 }
    389             }
    390 
    391             @Override
    392             public void onAnimationCancel(Animator animation) {
    393                 if (mActionAfterFade != null) {
    394                     mActionAfterFade.run();
    395                 }
    396             }
    397         });
    398     }
    399 
    400     public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight,
    401             int shrunkenTvViewHeight) {
    402         mTvView = tvView;
    403         if (mInputSessionManager != null) {
    404             mTvViewSession = mInputSessionManager.createTvViewSession(tvView, this, mCallback);
    405         } else {
    406             mTvView.setCallback(mCallback);
    407         }
    408         mIsPip = isPip;
    409         mScreenHeight = screenHeight;
    410         mShrunkenTvViewHeight = shrunkenTvViewHeight;
    411         mTvView.setZOrderOnTop(isPip);
    412         copyLayoutParamsToTvView();
    413     }
    414 
    415     public void start(TvInputManagerHelper tvInputManagerHelper) {
    416         mInputManagerHelper = tvInputManagerHelper;
    417         mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
    418         if (mStarted) {
    419             return;
    420         }
    421         mStarted = true;
    422     }
    423 
    424     /**
    425      * Warms up the input to reduce the start time.
    426      */
    427     public void warmUpInput(String inputId, Uri channelUri) {
    428         if (!mStarted && inputId != null && channelUri != null) {
    429             if (mTvViewSession != null) {
    430                 mTvViewSession.tune(inputId, channelUri);
    431             } else {
    432                 mTvView.tune(inputId, channelUri);
    433             }
    434             hideScreenByVideoAvailability(inputId, TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
    435         }
    436     }
    437 
    438     public void stop() {
    439         if (!mStarted) {
    440             return;
    441         }
    442         mStarted = false;
    443         if (mCurrentChannel != null) {
    444             long duration = mChannelViewTimer.reset();
    445             mTracker.sendChannelViewStop(mCurrentChannel, duration);
    446             if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
    447                 mWatchedHistoryManager.logChannelViewStop(mCurrentChannel,
    448                         System.currentTimeMillis(), duration);
    449             }
    450         }
    451         reset();
    452     }
    453 
    454     /**
    455      * Releases the resources.
    456      */
    457     public void release() {
    458         if (mInputSessionManager != null) {
    459             mInputSessionManager.releaseTvViewSession(mTvViewSession);
    460             mTvViewSession = null;
    461         }
    462     }
    463 
    464     /**
    465      * Reset TV view.
    466      */
    467     public void reset() {
    468         resetInternal();
    469         hideScreenByVideoAvailability(null, VIDEO_UNAVAILABLE_REASON_NOT_TUNED);
    470     }
    471 
    472     /**
    473      * Reset TV view to acquire the recording session.
    474      */
    475     public void resetByRecording() {
    476         resetInternal();
    477     }
    478 
    479     private void resetInternal() {
    480         if (mTvViewSession != null) {
    481             mTvViewSession.reset();
    482         } else {
    483             mTvView.reset();
    484         }
    485         mCurrentChannel = null;
    486         mInputInfo = null;
    487         mCanReceiveInputEvent = false;
    488         mOnTuneListener = null;
    489         setTimeShiftAvailable(false);
    490     }
    491 
    492     public void setMain() {
    493         mTvView.setMain();
    494     }
    495 
    496     public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
    497         mWatchedHistoryManager = watchedHistoryManager;
    498     }
    499 
    500     public boolean isPlaying() {
    501         return mStarted;
    502     }
    503 
    504     /**
    505      * Called when parental control is changed.
    506      */
    507     public void onParentalControlChanged(boolean enabled) {
    508         mParentControlEnabled = enabled;
    509         if (!mParentControlEnabled) {
    510             mBlockScreenForTuneView.setVisibility(View.GONE);
    511         }
    512     }
    513 
    514     /**
    515      * Tunes to a channel with the {@code channelId}.
    516      *
    517      * @param params extra data to send it to TIS and store the data in TIMS.
    518      * @return false, if the TV input is not a proper state to tune to a channel. For example,
    519      *         if the state is disconnected or channelId doesn't exist, it returns false.
    520      */
    521     public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) {
    522         if (!mStarted) {
    523             throw new IllegalStateException("TvView isn't started");
    524         }
    525         if (DEBUG) Log.d(TAG, "tuneTo " + channel);
    526         TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(channel.getInputId());
    527         if (inputInfo == null) {
    528             return false;
    529         }
    530         if (mCurrentChannel != null) {
    531             long duration = mChannelViewTimer.reset();
    532             mTracker.sendChannelViewStop(mCurrentChannel, duration);
    533             if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
    534                 mWatchedHistoryManager.logChannelViewStop(mCurrentChannel,
    535                         System.currentTimeMillis(), duration);
    536             }
    537         }
    538         mOnTuneListener = listener;
    539         mCurrentChannel = channel;
    540         boolean tunedByRecommendation = params != null
    541                 && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null;
    542         boolean needSurfaceSizeUpdate = false;
    543         if (!inputInfo.equals(mInputInfo)) {
    544             mInputInfo = inputInfo;
    545             mCanReceiveInputEvent = getContext().getPackageManager().checkPermission(
    546                     PERMISSION_RECEIVE_INPUT_EVENT, mInputInfo.getServiceInfo().packageName)
    547                             == PackageManager.PERMISSION_GRANTED;
    548             if (DEBUG) {
    549                 Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: "
    550                         + mCanReceiveInputEvent);
    551             }
    552             needSurfaceSizeUpdate = true;
    553         }
    554         mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation);
    555         mChannelViewTimer.start();
    556         mVideoWidth = 0;
    557         mVideoHeight = 0;
    558         mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
    559         mVideoFrameRate = 0f;
    560         mVideoDisplayAspectRatio = 0f;
    561         mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
    562         mHasClosedCaption = false;
    563         mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
    564         // To reduce the IPCs, unregister the callback here and register it when necessary.
    565         mTvView.setTimeShiftPositionCallback(null);
    566         setTimeShiftAvailable(false);
    567         if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
    568             // When the input is changed, TvView recreates its SurfaceView internally.
    569             // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
    570             getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
    571         }
    572         hideScreenByVideoAvailability(mInputInfo.getId(),
    573                 TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
    574         if (mTvViewSession != null) {
    575             mTvViewSession.tune(channel, params, listener);
    576         } else {
    577             mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);
    578         }
    579         unblockScreenByContentRating();
    580         if (channel.isPassthrough()) {
    581             mBlockScreenForTuneView.setVisibility(View.GONE);
    582         } else if (mParentControlEnabled) {
    583             mBlockScreenForTuneView.setVisibility(View.VISIBLE);
    584         }
    585         if (mOnTuneListener != null) {
    586             mOnTuneListener.onStreamInfoChanged(this);
    587         }
    588         return true;
    589     }
    590 
    591     @Override
    592     public Channel getCurrentChannel() {
    593         return mCurrentChannel;
    594     }
    595 
    596     /**
    597      * Sets the current channel. Call this method only when setting the current channel without
    598      * actually tuning to it.
    599      *
    600      * @param currentChannel The new current channel to set to.
    601      */
    602     public void setCurrentChannel(Channel currentChannel) {
    603         mCurrentChannel = currentChannel;
    604     }
    605 
    606     public void setStreamVolume(float volume) {
    607         if (!mStarted) {
    608             throw new IllegalStateException("TvView isn't started");
    609         }
    610         if (DEBUG) Log.d(TAG, "setStreamVolume " + volume);
    611         mVolume = volume;
    612         if (!mIsMuted) {
    613             mTvView.setStreamVolume(volume);
    614         }
    615     }
    616 
    617     /**
    618      * Sets fixed size for the internal {@link android.view.Surface} of
    619      * {@link android.media.tv.TvView}. If either {@code width} or {@code height} is non positive,
    620      * the {@link android.view.Surface}'s size will be matched to the layout.
    621      *
    622      * Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called,
    623      * {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size
    624      * of {@link android.view.SurfaceView} is changed without changing either left position or top
    625      * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow().
    626      */
    627     public void setFixedSurfaceSize(int width, int height) {
    628         mFixedSurfaceWidth = width;
    629         mFixedSurfaceHeight = height;
    630         if (mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
    631             // When the input is changed, TvView recreates its SurfaceView internally.
    632             // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
    633             SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0);
    634             surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
    635         } else {
    636             SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0);
    637             surfaceView.getHolder().setSizeFromLayout();
    638         }
    639     }
    640 
    641     @Override
    642     public boolean dispatchKeyEvent(KeyEvent event) {
    643         return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event);
    644     }
    645 
    646     @Override
    647     public boolean dispatchTouchEvent(MotionEvent event) {
    648         return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event);
    649     }
    650 
    651     @Override
    652     public boolean dispatchTrackballEvent(MotionEvent event) {
    653         return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event);
    654     }
    655 
    656     @Override
    657     public boolean dispatchGenericMotionEvent(MotionEvent event) {
    658         return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event);
    659     }
    660 
    661     public interface OnTuneListener {
    662         void onTuneFailed(Channel channel);
    663         void onUnexpectedStop(Channel channel);
    664         void onStreamInfoChanged(StreamInfo info);
    665         void onChannelRetuned(Uri channel);
    666         void onContentBlocked();
    667         void onContentAllowed();
    668     }
    669 
    670     public void unblockContent(TvContentRating rating) {
    671         mTvView.unblockContent(rating);
    672     }
    673 
    674     @Override
    675     public int getVideoWidth() {
    676         return mVideoWidth;
    677     }
    678 
    679     @Override
    680     public int getVideoHeight() {
    681         return mVideoHeight;
    682     }
    683 
    684     @Override
    685     public int getVideoDefinitionLevel() {
    686         return mVideoFormat;
    687     }
    688 
    689     @Override
    690     public float getVideoFrameRate() {
    691         return mVideoFrameRate;
    692     }
    693 
    694     /**
    695      * Returns displayed aspect ratio (video width / video height * pixel ratio).
    696      */
    697     @Override
    698     public float getVideoDisplayAspectRatio() {
    699         return mVideoDisplayAspectRatio;
    700     }
    701 
    702     @Override
    703     public int getAudioChannelCount() {
    704         return mAudioChannelCount;
    705     }
    706 
    707     @Override
    708     public boolean hasClosedCaption() {
    709         return mHasClosedCaption;
    710     }
    711 
    712     @Override
    713     public boolean isVideoAvailable() {
    714         return mVideoAvailable;
    715     }
    716 
    717     @Override
    718     public int getVideoUnavailableReason() {
    719         return mVideoUnavailableReason;
    720     }
    721 
    722     /**
    723      * Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}.
    724      */
    725     private SurfaceView getSurfaceView() {
    726         return (SurfaceView) mTvView.getChildAt(0);
    727     }
    728 
    729     public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
    730         mTvView.setOnUnhandledInputEventListener(listener);
    731     }
    732 
    733     public void setClosedCaptionEnabled(boolean enabled) {
    734         mTvView.setCaptionEnabled(enabled);
    735     }
    736 
    737     public List<TvTrackInfo> getTracks(int type) {
    738         return mTvView.getTracks(type);
    739     }
    740 
    741     public String getSelectedTrack(int type) {
    742         return mTvView.getSelectedTrack(type);
    743     }
    744 
    745     public void selectTrack(int type, String trackId) {
    746         mTvView.selectTrack(type, trackId);
    747     }
    748 
    749     /**
    750      * Returns if the screen is blocked by {@link #blockScreen()}.
    751      */
    752     public boolean isScreenBlocked() {
    753         return mScreenBlocked;
    754     }
    755 
    756     public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) {
    757         mOnScreenBlockedListener = listener;
    758     }
    759 
    760     /**
    761      * Returns currently blocked content rating. {@code null} if it's not blocked.
    762      */
    763     @Override
    764     public TvContentRating getBlockedContentRating() {
    765         return mBlockedContentRating;
    766     }
    767 
    768     /**
    769      * Locks current TV screen and mutes.
    770      * There would be black screen with lock icon in order to show that
    771      * screen block is intended and not an error.
    772      * TODO: Accept parameter to show lock icon or not.
    773      */
    774     public void blockScreen() {
    775         mScreenBlocked = true;
    776         checkBlockScreenAndMuteNeeded();
    777         if (mOnScreenBlockedListener != null) {
    778             mOnScreenBlockedListener.onScreenBlockingChanged(true);
    779         }
    780     }
    781 
    782     private void blockScreenByContentRating(TvContentRating rating) {
    783         mBlockedContentRating = rating;
    784         checkBlockScreenAndMuteNeeded();
    785     }
    786 
    787     @Override
    788     @SuppressLint("RtlHardcoded")
    789     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    790         super.onLayout(changed, left, top, right, bottom);
    791         if (mIsPip) {
    792             int height = bottom - top;
    793             float scale;
    794             if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) {
    795                 scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight;
    796             } else {
    797                 scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight;
    798             }
    799             // TODO: need to get UX confirmation.
    800             mBlockScreenView.scaleContainerView(scale);
    801         }
    802     }
    803 
    804     @Override
    805     public void setLayoutParams(ViewGroup.LayoutParams params) {
    806         super.setLayoutParams(params);
    807         if (mTvView != null) {
    808             copyLayoutParamsToTvView();
    809         }
    810     }
    811 
    812     private void copyLayoutParamsToTvView() {
    813         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
    814         FrameLayout.LayoutParams tvViewLp = (FrameLayout.LayoutParams) mTvView.getLayoutParams();
    815         if (tvViewLp.bottomMargin != lp.bottomMargin
    816                 || tvViewLp.topMargin != lp.topMargin
    817                 || tvViewLp.leftMargin != lp.leftMargin
    818                 || tvViewLp.rightMargin != lp.rightMargin
    819                 || tvViewLp.gravity != lp.gravity
    820                 || tvViewLp.height != lp.height
    821                 || tvViewLp.width != lp.width) {
    822             if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin
    823                     && !BuildCompat.isAtLeastN()) {
    824                 // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is
    825                 // used, SurfaceView doesn't catch the width and height change. It causes a bug that
    826                 // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for
    827                 // small size PIP as a workaround.
    828                 // Note: This framework issue has been fixed from NYC.
    829                 tvViewLp.leftMargin = lp.leftMargin + 1;
    830             } else {
    831                 tvViewLp.leftMargin = lp.leftMargin;
    832             }
    833             tvViewLp.topMargin = lp.topMargin;
    834             tvViewLp.bottomMargin = lp.bottomMargin;
    835             tvViewLp.rightMargin = lp.rightMargin;
    836             tvViewLp.gravity = lp.gravity;
    837             tvViewLp.height = lp.height;
    838             tvViewLp.width = lp.width;
    839             mTvView.setLayoutParams(tvViewLp);
    840         }
    841     }
    842 
    843     @Override
    844     protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
    845         super.onVisibilityChanged(changedView, visibility);
    846         if (mTvView != null) {
    847             mTvView.setVisibility(visibility);
    848         }
    849     }
    850 
    851     /**
    852      * Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the
    853      * block screen will not show any description such as a lock icon and a text for the blocked
    854      * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block screen
    855      * will show the description for shrunken tv view (Small icon and short text), and if
    856      * {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the
    857      * description for normal tv view (Big icon and long text).
    858      *
    859      * @param type The type of block screen to set.
    860      */
    861     public void setBlockScreenType(@BlockScreenType int type) {
    862         // TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse.
    863         if (mBlockScreenType != type) {
    864             mBlockScreenType = type;
    865             updateBlockScreenUI(true);
    866         }
    867     }
    868 
    869     private void updateBlockScreenUI(boolean animation) {
    870         mBlockScreenView.endAnimations();
    871 
    872         if (!mScreenBlocked && mBlockedContentRating == null) {
    873             mBlockScreenView.setVisibility(GONE);
    874             return;
    875         }
    876 
    877         mBlockScreenView.setVisibility(VISIBLE);
    878         if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) {
    879             adjustBlockScreenSpacingAndText();
    880         }
    881         mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
    882     }
    883 
    884     private void adjustBlockScreenSpacingAndText() {
    885         // TODO: need to add animation for padding change when the block screen type is changed
    886         // NORMAL to SHRUNKEN and vice verse.
    887         mBlockScreenView.setSpacing(mBlockScreenType);
    888         String text = getBlockScreenText();
    889         if (text != null) {
    890             mBlockScreenView.setText(text);
    891         }
    892     }
    893 
    894     /**
    895      * Returns the block screen text corresponding to the current status.
    896      * Note that returning {@code null} value means that the current text should not be changed.
    897      */
    898     private String getBlockScreenText() {
    899         if (mScreenBlocked) {
    900             switch (mBlockScreenType) {
    901                 case BLOCK_SCREEN_TYPE_NO_UI:
    902                 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
    903                     return "";
    904                 case BLOCK_SCREEN_TYPE_NORMAL:
    905                     if (mCanModifyParentalControls) {
    906                         return getResources().getString(R.string.tvview_channel_locked);
    907                     } else {
    908                         return getResources().getString(
    909                                 R.string.tvview_channel_locked_no_permission);
    910                     }
    911             }
    912         } else if (mBlockedContentRating != null) {
    913             String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating);
    914             switch (mBlockScreenType) {
    915                 case BLOCK_SCREEN_TYPE_NO_UI:
    916                     return "";
    917                 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
    918                     if (TextUtils.isEmpty(name)) {
    919                         return getResources().getString(R.string.shrunken_tvview_content_locked);
    920                     } else {
    921                         return getContext().getString(
    922                                 R.string.shrunken_tvview_content_locked_format, name);
    923                     }
    924                 case BLOCK_SCREEN_TYPE_NORMAL:
    925                     if (TextUtils.isEmpty(name)) {
    926                         if (mCanModifyParentalControls) {
    927                             return getResources().getString(R.string.tvview_content_locked);
    928                         } else {
    929                             return getResources().getString(
    930                                     R.string.tvview_content_locked_no_permission);
    931                         }
    932                     } else {
    933                         if (mCanModifyParentalControls) {
    934                             return getContext().getString(
    935                                     R.string.tvview_content_locked_format, name);
    936                         } else {
    937                             return getContext().getString(
    938                                     R.string.tvview_content_locked_format_no_permission, name);
    939                         }
    940                     }
    941             }
    942         }
    943         return null;
    944     }
    945 
    946     private void checkBlockScreenAndMuteNeeded() {
    947         updateBlockScreenUI(false);
    948         if (mScreenBlocked || mBlockedContentRating != null) {
    949             mute();
    950             if (mIsPip) {
    951                 // If we don't make mTvView invisible, some frames are leaked when a user changes
    952                 // PIP layout in options.
    953                 // Note: When video is unavailable, we keep the mTvView's visibility, because
    954                 // TIS implementation may not send video available with no surface.
    955                 mTvView.setVisibility(View.INVISIBLE);
    956             }
    957         } else {
    958             unmuteIfPossible();
    959             if (mIsPip) {
    960                 mTvView.setVisibility(View.VISIBLE);
    961             }
    962         }
    963     }
    964 
    965     public void unblockScreen() {
    966         mScreenBlocked = false;
    967         checkBlockScreenAndMuteNeeded();
    968         if (mOnScreenBlockedListener != null) {
    969             mOnScreenBlockedListener.onScreenBlockingChanged(false);
    970         }
    971     }
    972 
    973     private void unblockScreenByContentRating() {
    974         mBlockedContentRating = null;
    975         checkBlockScreenAndMuteNeeded();
    976     }
    977 
    978     @UiThread
    979     private void hideScreenByVideoAvailability(String inputId, int reason) {
    980         mVideoAvailable = false;
    981         mVideoUnavailableReason = reason;
    982         if (mInternetCheckTask != null) {
    983             mInternetCheckTask.cancel(true);
    984             mInternetCheckTask = null;
    985         }
    986         switch (reason) {
    987             case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
    988                 mHideScreenView.setVisibility(VISIBLE);
    989                 mHideScreenView.setImageVisibility(false);
    990                 mHideScreenView.setText(R.string.tvview_msg_audio_only);
    991                 mBufferingSpinnerView.setVisibility(GONE);
    992                 unmuteIfPossible();
    993                 break;
    994             case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
    995                 mBufferingSpinnerView.setVisibility(VISIBLE);
    996                 mute();
    997                 break;
    998             case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
    999                 mHideScreenView.setVisibility(VISIBLE);
   1000                 mHideScreenView.setText(R.string.tvview_msg_weak_signal);
   1001                 mBufferingSpinnerView.setVisibility(GONE);
   1002                 mute();
   1003                 break;
   1004             case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
   1005                 mHideScreenView.setVisibility(VISIBLE);
   1006                 mHideScreenView.setImageVisibility(false);
   1007                 mHideScreenView.setText(null);
   1008                 mBufferingSpinnerView.setVisibility(VISIBLE);
   1009                 mute();
   1010                 break;
   1011             case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
   1012                 mHideScreenView.setVisibility(VISIBLE);
   1013                 mHideScreenView.setImageVisibility(false);
   1014                 mHideScreenView.setText(null);
   1015                 mBufferingSpinnerView.setVisibility(GONE);
   1016                 mute();
   1017                 break;
   1018             case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
   1019                 mHideScreenView.setVisibility(VISIBLE);
   1020                 mHideScreenView.setImageVisibility(false);
   1021                 mHideScreenView.setText(getTuneConflictMessage(inputId));
   1022                 mBufferingSpinnerView.setVisibility(GONE);
   1023                 mute();
   1024                 break;
   1025             case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
   1026             default:
   1027                 mHideScreenView.setVisibility(VISIBLE);
   1028                 mHideScreenView.setImageVisibility(false);
   1029                 mHideScreenView.setText(null);
   1030                 mBufferingSpinnerView.setVisibility(GONE);
   1031                 mute();
   1032                 if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) {
   1033                     mInternetCheckTask = new InternetCheckTask();
   1034                     mInternetCheckTask.execute();
   1035                 }
   1036                 break;
   1037         }
   1038     }
   1039 
   1040     private String getTuneConflictMessage(String inputId) {
   1041         if (inputId != null) {
   1042             TvInputInfo input = mInputManager.getTvInputInfo(inputId);
   1043             Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(inputId);
   1044             if (timeMs != null) {
   1045                 return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource,
   1046                         input.getTunerCount(),
   1047                         DateUtils.formatDateTime(getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME));
   1048             }
   1049         }
   1050         return null;
   1051     }
   1052 
   1053     private void unhideScreenByVideoAvailability() {
   1054         mVideoAvailable = true;
   1055         mHideScreenView.setVisibility(GONE);
   1056         mBufferingSpinnerView.setVisibility(GONE);
   1057         unmuteIfPossible();
   1058     }
   1059 
   1060     private void unmuteIfPossible() {
   1061         if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) {
   1062             unmute();
   1063         }
   1064     }
   1065 
   1066     private void mute() {
   1067         mIsMuted = true;
   1068         mTvView.setStreamVolume(0);
   1069     }
   1070 
   1071     private void unmute() {
   1072         mIsMuted = false;
   1073         mTvView.setStreamVolume(mVolume);
   1074     }
   1075 
   1076     /** Returns true if this view is faded out. */
   1077     public boolean isFadedOut() {
   1078         return mFadeState == FADED_OUT;
   1079     }
   1080 
   1081     /** Fade out this TunableTvView. Fade out by increasing the dimming. */
   1082     public void fadeOut(int durationMillis, TimeInterpolator interpolator,
   1083             final Runnable actionAfterFade) {
   1084         mDimScreenView.setAlpha(0f);
   1085         mDimScreenView.setVisibility(View.VISIBLE);
   1086         mDimScreenView.animate()
   1087                 .alpha(1f)
   1088                 .setDuration(durationMillis)
   1089                 .setInterpolator(interpolator)
   1090                 .withStartAction(new Runnable() {
   1091                     @Override
   1092                     public void run() {
   1093                         mFadeState = FADING_OUT;
   1094                         mActionAfterFade = actionAfterFade;
   1095                     }
   1096                 })
   1097                 .withEndAction(new Runnable() {
   1098                     @Override
   1099                     public void run() {
   1100                         mFadeState = FADED_OUT;
   1101                     }
   1102                 });
   1103     }
   1104 
   1105     /** Fade in this TunableTvView. Fade in by decreasing the dimming. */
   1106     public void fadeIn(int durationMillis, TimeInterpolator interpolator,
   1107             final Runnable actionAfterFade) {
   1108         mDimScreenView.setAlpha(1f);
   1109         mDimScreenView.setVisibility(View.VISIBLE);
   1110         mDimScreenView.animate()
   1111                 .alpha(0f)
   1112                 .setDuration(durationMillis)
   1113                 .setInterpolator(interpolator)
   1114                 .withStartAction(new Runnable() {
   1115                     @Override
   1116                     public void run() {
   1117                         mFadeState = FADING_IN;
   1118                         mActionAfterFade = actionAfterFade;
   1119                     }
   1120                 })
   1121                 .withEndAction(new Runnable() {
   1122                     @Override
   1123                     public void run() {
   1124                         mFadeState = FADED_IN;
   1125                         mDimScreenView.setVisibility(View.GONE);
   1126                     }
   1127                 });
   1128     }
   1129 
   1130     /** Remove the fade effect. */
   1131     public void removeFadeEffect() {
   1132         mDimScreenView.animate().cancel();
   1133         mDimScreenView.setVisibility(View.GONE);
   1134         mFadeState = FADED_IN;
   1135     }
   1136 
   1137     /**
   1138      * Sets the TimeShiftListener
   1139      *
   1140      * @param listener The instance of {@link TimeShiftListener}.
   1141      */
   1142     public void setTimeShiftListener(TimeShiftListener listener) {
   1143         mTimeShiftListener = listener;
   1144     }
   1145 
   1146     private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
   1147         if (mTimeShiftAvailable == isTimeShiftAvailable) {
   1148             return;
   1149         }
   1150         mTimeShiftAvailable = isTimeShiftAvailable;
   1151         if (isTimeShiftAvailable) {
   1152             mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() {
   1153                 @Override
   1154                 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
   1155                     if (mTimeShiftListener != null && mCurrentChannel != null
   1156                             && mCurrentChannel.getInputId().equals(inputId)) {
   1157                         mTimeShiftListener.onRecordStartTimeChanged(timeMs);
   1158                     }
   1159                 }
   1160 
   1161                 @Override
   1162                 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
   1163                     mTimeShiftCurrentPositionMs = timeMs;
   1164                 }
   1165             });
   1166         } else {
   1167             mTvView.setTimeShiftPositionCallback(null);
   1168         }
   1169         if (mTimeShiftListener != null) {
   1170             mTimeShiftListener.onAvailabilityChanged();
   1171         }
   1172     }
   1173 
   1174     /**
   1175      * Returns if the time shift is available for the current channel.
   1176      */
   1177     public boolean isTimeShiftAvailable() {
   1178         return mTimeShiftAvailable;
   1179     }
   1180 
   1181     /**
   1182      * Plays the media, if the current input supports time-shifting.
   1183      */
   1184     public void timeshiftPlay() {
   1185         if (!isTimeShiftAvailable()) {
   1186             throw new IllegalStateException("Time-shift is not supported for the current channel");
   1187         }
   1188         if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) {
   1189             return;
   1190         }
   1191         mTvView.timeShiftResume();
   1192     }
   1193 
   1194     /**
   1195      * Pauses the media, if the current input supports time-shifting.
   1196      */
   1197     public void timeshiftPause() {
   1198         if (!isTimeShiftAvailable()) {
   1199             throw new IllegalStateException("Time-shift is not supported for the current channel");
   1200         }
   1201         if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) {
   1202             return;
   1203         }
   1204         mTvView.timeShiftPause();
   1205     }
   1206 
   1207     /**
   1208      * Rewinds the media with the given speed, if the current input supports time-shifting.
   1209      *
   1210      * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
   1211      */
   1212     public void timeshiftRewind(int speed) {
   1213         if (!isTimeShiftAvailable()) {
   1214             throw new IllegalStateException("Time-shift is not supported for the current channel");
   1215         } else {
   1216             if (speed <= 0) {
   1217                 throw new IllegalArgumentException("The speed should be a positive integer.");
   1218             }
   1219             mTimeShiftState = TIME_SHIFT_STATE_REWIND;
   1220             PlaybackParams params = new PlaybackParams();
   1221             params.setSpeed(speed * -1);
   1222             mTvView.timeShiftSetPlaybackParams(params);
   1223         }
   1224     }
   1225 
   1226     /**
   1227      * Fast-forwards the media with the given speed, if the current input supports time-shifting.
   1228      *
   1229      * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
   1230      */
   1231     public void timeshiftFastForward(int speed) {
   1232         if (!isTimeShiftAvailable()) {
   1233             throw new IllegalStateException("Time-shift is not supported for the current channel");
   1234         } else {
   1235             if (speed <= 0) {
   1236                 throw new IllegalArgumentException("The speed should be a positive integer.");
   1237             }
   1238             mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD;
   1239             PlaybackParams params = new PlaybackParams();
   1240             params.setSpeed(speed);
   1241             mTvView.timeShiftSetPlaybackParams(params);
   1242         }
   1243     }
   1244 
   1245     /**
   1246      * Seek to the given time position.
   1247      *
   1248      * @param timeMs The time in milliseconds to seek to.
   1249      */
   1250     public void timeshiftSeekTo(long timeMs) {
   1251         if (!isTimeShiftAvailable()) {
   1252             throw new IllegalStateException("Time-shift is not supported for the current channel");
   1253         }
   1254         mTvView.timeShiftSeekTo(timeMs);
   1255     }
   1256 
   1257     /**
   1258      * Returns the current playback position in milliseconds.
   1259      */
   1260     public long timeshiftGetCurrentPositionMs() {
   1261         if (!isTimeShiftAvailable()) {
   1262             throw new IllegalStateException("Time-shift is not supported for the current channel");
   1263         }
   1264         if (DEBUG) {
   1265             Log.d(TAG, "timeshiftGetCurrentPositionMs: current position ="
   1266                     + Utils.toTimeString(mTimeShiftCurrentPositionMs));
   1267         }
   1268         return mTimeShiftCurrentPositionMs;
   1269     }
   1270 
   1271     /**
   1272      * Used to receive the time-shift events.
   1273      */
   1274     public static abstract class TimeShiftListener {
   1275         /**
   1276          * Called when the availability of the time-shift for the current channel has been changed.
   1277          * It should be guaranteed that this is called only when the availability is really changed.
   1278          */
   1279         public abstract void onAvailabilityChanged();
   1280 
   1281         /**
   1282          * Called when the record start time has been changed.
   1283          * This is not called when the recorded programs is played.
   1284          */
   1285         public abstract void onRecordStartTimeChanged(long recordStartTimeMs);
   1286     }
   1287 
   1288     /**
   1289      * A listener which receives the notification when the screen is blocked/unblocked.
   1290      */
   1291     public static abstract class OnScreenBlockingChangedListener {
   1292         /**
   1293          * Called when the screen is blocked/unblocked.
   1294          */
   1295         public abstract void onScreenBlockingChanged(boolean blocked);
   1296     }
   1297 
   1298     private class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
   1299         @Override
   1300         protected Boolean doInBackground(Void... params) {
   1301             return NetworkUtils.isNetworkAvailable(mConnectivityManager);
   1302         }
   1303 
   1304         @Override
   1305         protected void onPostExecute(Boolean networkAvailable) {
   1306             mInternetCheckTask = null;
   1307             if (!mVideoAvailable && !networkAvailable && isAttachedToWindow()
   1308                     && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
   1309                 mHideScreenView.setImageVisibility(true);
   1310                 mHideScreenView.setImage(R.drawable.ic_sad_cloud);
   1311                 mHideScreenView.setText(R.string.tvview_msg_no_internet_connection);
   1312             }
   1313         }
   1314     }
   1315 }
   1316