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