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