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