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