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