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 (shouldShowEmptyInputStatusBlock()){
1021                 mBlockScreenView.setEmptyInputStatusInputInfo(mInputInfo);
1022                 mBlockScreenView.setEmptyInputStatusBlockVisibility(true);
1023             } else {
1024                 mBlockScreenView.setEmptyInputStatusBlockVisibility(false);
1025             }
1026             if (mTvIAppView != null) {
1027                 mTvIAppView.setVisibility(INVISIBLE);
1028             }
1029             mBlockScreenView.setBackgroundImage(null);
1030             if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) {
1031                 mBlockScreenView.setIconVisibility(true);
1032                 if (!mCanModifyParentalControls) {
1033                     mBlockScreenView.setIconImage(R.drawable.ic_message_lock_no_permission);
1034                     mBlockScreenView.setIconScaleType(ImageView.ScaleType.CENTER);
1035                 } else {
1036                     mBlockScreenView.setIconImage(R.drawable.ic_message_lock);
1037                     mBlockScreenView.setIconScaleType(ImageView.ScaleType.FIT_CENTER);
1038                 }
1039             } else {
1040                 if (mInternetCheckTask != null) {
1041                     mInternetCheckTask.cancel(true);
1042                     mInternetCheckTask = null;
1043                 }
1044                 mBlockScreenView.setIconVisibility(false);
1045                 if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) {
1046                     showImageForTuningIfNeeded();
1047                 } else if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
1048                         && mCurrentChannel != null
1049                         && !mCurrentChannel.isPhysicalTunerChannel()) {
1050                     mInternetCheckTask = new InternetCheckTask();
1051                     mInternetCheckTask.execute();
1052                 }
1053             }
1054             mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
1055         } else {
1056             mBufferingSpinnerView.setVisibility(GONE);
1057             if (mBlockScreenView.getVisibility() == VISIBLE) {
1058                 mBlockScreenView.fadeOut();
1059             }
1060             if (mTvIAppView != null) {
1061                 mTvIAppView.setVisibility(VISIBLE);
1062             }
1063         }
1064     }
1065 
adjustBlockScreenSpacingAndText()1066     private void adjustBlockScreenSpacingAndText() {
1067         mBlockScreenView.setSpacing(mBlockScreenType);
1068         String text = getBlockScreenText();
1069         if (text != null) {
1070             mBlockScreenView.setInfoText(text);
1071         }
1072         mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled);
1073     }
1074 
1075     /**
1076      * Returns the block screen text corresponding to the current status. Note that returning {@code
1077      * null} value means that the current text should not be changed.
1078      */
getBlockScreenText()1079     private String getBlockScreenText() {
1080         // TODO: add a test for this method
1081         Resources res = getResources();
1082         boolean isA11y = mAccessibilityManager.isEnabled();
1083 
1084         if (mScreenBlocked && mParentControlEnabled) {
1085             switch (mBlockScreenType) {
1086                 case BLOCK_SCREEN_TYPE_NO_UI:
1087                 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
1088                     return "";
1089                 case BLOCK_SCREEN_TYPE_NORMAL:
1090                     if (mCanModifyParentalControls) {
1091                         return res.getString(
1092                                 isA11y
1093                                         ? R.string.tvview_channel_locked_talkback
1094                                         : R.string.tvview_channel_locked);
1095                     } else {
1096                         return res.getString(R.string.tvview_channel_locked_no_permission);
1097                     }
1098             }
1099         } else if (mBlockedContentRating != null && mParentControlEnabled) {
1100             String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating);
1101             switch (mBlockScreenType) {
1102                 case BLOCK_SCREEN_TYPE_NO_UI:
1103                     return "";
1104                 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
1105                     if (TextUtils.isEmpty(name)) {
1106                         return res.getString(R.string.shrunken_tvview_content_locked);
1107                     } else if (name.equals(res.getString(R.string.unrated_rating_name))) {
1108                         return res.getString(R.string.shrunken_tvview_content_locked_unrated);
1109                     } else {
1110                         return res.getString(R.string.shrunken_tvview_content_locked_format, name);
1111                     }
1112                 case BLOCK_SCREEN_TYPE_NORMAL:
1113                     if (TextUtils.isEmpty(name)) {
1114                         if (mCanModifyParentalControls) {
1115                             return res.getString(
1116                                     isA11y
1117                                             ? R.string.tvview_content_locked_talkback
1118                                             : R.string.tvview_content_locked);
1119                         } else {
1120                             return res.getString(R.string.tvview_content_locked_no_permission);
1121                         }
1122                     } else {
1123                         if (mCanModifyParentalControls) {
1124                             return name.equals(res.getString(R.string.unrated_rating_name))
1125                                     ? res.getString(
1126                                             isA11y
1127                                                     ? R.string
1128                                                             .tvview_content_locked_unrated_talkback
1129                                                     : R.string.tvview_content_locked_unrated)
1130                                     : res.getString(
1131                                             isA11y
1132                                                     ? R.string.tvview_content_locked_format_talkback
1133                                                     : R.string.tvview_content_locked_format,
1134                                             name);
1135                         } else {
1136                             return name.equals(res.getString(R.string.unrated_rating_name))
1137                                     ? res.getString(
1138                                             R.string.tvview_content_locked_unrated_no_permission)
1139                                     : res.getString(
1140                                             R.string.tvview_content_locked_format_no_permission,
1141                                             name);
1142                         }
1143                     }
1144             }
1145         } else if (mVideoUnavailableReason != VIDEO_UNAVAILABLE_REASON_NONE) {
1146             switch (mVideoUnavailableReason) {
1147                 case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
1148                     return res.getString(R.string.tvview_msg_audio_only);
1149                 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
1150                     return res.getString(R.string.tvview_msg_weak_signal);
1151                 case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
1152                     return res.getString(R.string.msg_channel_unavailable_not_connected);
1153                 case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
1154                     return getTuneConflictMessage();
1155                 default:
1156                     return "";
1157             }
1158         }
1159         return null;
1160     }
1161 
closePipIfNeeded()1162     private boolean closePipIfNeeded() {
1163         if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getContext())
1164                 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
1165                 && ((Activity) getContext()).isInPictureInPictureMode()
1166                 && (mScreenBlocked
1167                         || mBlockedContentRating != null
1168                         || mVideoUnavailableReason
1169                                 == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
1170                         || mVideoUnavailableReason
1171                                 == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) {
1172             ((Activity) getContext()).finish();
1173             return true;
1174         }
1175         return false;
1176     }
1177 
updateBlockScreenAndMuting()1178     private void updateBlockScreenAndMuting() {
1179         updateBlockScreen(false);
1180         updateMuteStatus();
1181     }
1182 
shouldShowImageForTuning()1183     private boolean shouldShowImageForTuning() {
1184         if (mVideoUnavailableReason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
1185                 || mScreenBlocked
1186                 || mBlockedContentRating != null
1187                 || mCurrentChannel == null
1188                 || mIsUnderShrunken
1189                 || getWidth() == 0
1190                 || getWidth() == 0
1191                 || !isBundledInput()) {
1192             return false;
1193         }
1194         Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
1195         if (currentProgram == null) {
1196             return false;
1197         }
1198         TvContentRating rating =
1199                 mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings());
1200         return !(mParentControlEnabled && rating != null);
1201     }
1202 
showImageForTuningIfNeeded()1203     private void showImageForTuningIfNeeded() {
1204         if (shouldShowImageForTuning()) {
1205             if (mCurrentChannel == null) {
1206                 return;
1207             }
1208             Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
1209             if (currentProgram != null) {
1210                 currentProgram.loadPosterArt(
1211                         getContext(),
1212                         getWidth(),
1213                         getHeight(),
1214                         createProgramPosterArtCallback(mCurrentChannel.getId()));
1215             }
1216         }
1217     }
1218 
getTuneConflictMessage()1219     private String getTuneConflictMessage() {
1220         if (mTagetInputId != null) {
1221             TvInputInfo input = mInputManager.getTvInputInfo(mTagetInputId);
1222             Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(mTagetInputId);
1223             if (timeMs != null) {
1224                 return getResources()
1225                         .getQuantityString(
1226                                 R.plurals.tvview_msg_input_no_resource,
1227                                 input.getTunerCount(),
1228                                 DateUtils.formatDateTime(
1229                                         getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME));
1230             }
1231         }
1232         return null;
1233     }
1234 
updateMuteStatus()1235     private void updateMuteStatus() {
1236         // Workaround: BaseTunerTvInputService uses AC3 pass-through implementation, which disables
1237         // audio tracks to enforce the mute request. We don't want to send mute request if we are
1238         // not going to block the screen to prevent the video jankiness resulted by disabling audio
1239         // track before the playback is started. In other way, we should send unmute request before
1240         // the playback is started, because TunerTvInput will remember the muted state and mute
1241         // itself right way when the playback is going to be started, which results the initial
1242         // jankiness, too.
1243         boolean isBundledInput = isBundledInput();
1244         if ((isBundledInput || isVideoOrAudioAvailable())
1245                 && !mScreenBlocked
1246                 && mBlockedContentRating == null) {
1247             if (mIsMuted) {
1248                 mIsMuted = false;
1249                 mTvView.setStreamVolume(mVolume);
1250             }
1251         } else {
1252             if (!mIsMuted) {
1253                 if ((mInputInfo == null || isBundledInput)
1254                         && !mScreenBlocked
1255                         && mBlockedContentRating == null) {
1256                     return;
1257                 }
1258                 mIsMuted = true;
1259                 mTvView.setStreamVolume(0);
1260             }
1261         }
1262     }
1263 
shouldShowEmptyInputStatusBlock()1264     private boolean shouldShowEmptyInputStatusBlock() {
1265         return TvFeatures.USE_GTV_LIVETV_V2.isEnabled(getContext()) &&
1266                 (mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL ||
1267                         mVideoUnavailableReason ==
1268                                 CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED);
1269     }
1270 
isBundledInput()1271     private boolean isBundledInput() {
1272         return mInputInfo != null
1273                 && mInputInfo.getType() == TvInputInfo.TYPE_TUNER
1274                 && CommonUtils.isBundledInput(mInputInfo.getId());
1275     }
1276 
1277     /** Returns true if this view is faded out. */
isFadedOut()1278     public boolean isFadedOut() {
1279         return mFadeState == FADED_OUT;
1280     }
1281 
1282     /** Fade out this TunableTvView. Fade out by increasing the dimming. */
fadeOut( int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1283     public void fadeOut(
1284             int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) {
1285         mDimScreenView.setAlpha(0f);
1286         mDimScreenView.setVisibility(View.VISIBLE);
1287         mDimScreenView
1288                 .animate()
1289                 .alpha(1f)
1290                 .setDuration(durationMillis)
1291                 .setInterpolator(interpolator)
1292                 .withStartAction(
1293                         () -> {
1294                             mFadeState = FADING_OUT;
1295                             mActionAfterFade = actionAfterFade;
1296                         })
1297                 .withEndAction(() -> mFadeState = FADED_OUT);
1298     }
1299 
1300     /** Fade in this TunableTvView. Fade in by decreasing the dimming. */
fadeIn( int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1301     public void fadeIn(
1302             int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) {
1303         mDimScreenView.setAlpha(1f);
1304         mDimScreenView.setVisibility(View.VISIBLE);
1305         mDimScreenView
1306                 .animate()
1307                 .alpha(0f)
1308                 .setDuration(durationMillis)
1309                 .setInterpolator(interpolator)
1310                 .withStartAction(
1311                         () -> {
1312                             mFadeState = FADING_IN;
1313                             mActionAfterFade = actionAfterFade;
1314                         })
1315                 .withEndAction(
1316                         () -> {
1317                             mFadeState = FADED_IN;
1318                             mDimScreenView.setVisibility(View.GONE);
1319                         });
1320     }
1321 
1322     /** Remove the fade effect. */
removeFadeEffect()1323     public void removeFadeEffect() {
1324         mDimScreenView.animate().cancel();
1325         mDimScreenView.setVisibility(View.GONE);
1326         mFadeState = FADED_IN;
1327     }
1328 
1329     /**
1330      * Sets the TimeShiftListener
1331      *
1332      * @param listener The instance of {@link TimeShiftListener}.
1333      */
1334     @Override
setTimeShiftListener(TimeShiftListener listener)1335     public void setTimeShiftListener(TimeShiftListener listener) {
1336         mTimeShiftListener = listener;
1337     }
1338 
setBlockedInfoOnClickListener(@ullable OnClickListener onClickListener)1339     public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) {
1340         mBlockScreenView.setInfoTextOnClickListener(onClickListener);
1341     }
1342 
setTimeShiftAvailable(boolean isTimeShiftAvailable)1343     private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
1344         if (mTimeShiftAvailable == isTimeShiftAvailable) {
1345             return;
1346         }
1347         mTimeShiftAvailable = isTimeShiftAvailable;
1348         if (isTimeShiftAvailable) {
1349             mTvView.setTimeShiftPositionCallback(
1350                     new TvView.TimeShiftPositionCallback() {
1351                         @Override
1352                         public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
1353                             if (mTimeShiftListener != null
1354                                     && mCurrentChannel != null
1355                                     && mCurrentChannel.getInputId().equals(inputId)) {
1356                                 mTimeShiftListener.onRecordStartTimeChanged(timeMs);
1357                             }
1358                         }
1359 
1360                         @Override
1361                         public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
1362                             mTimeShiftCurrentPositionMs = timeMs;
1363                         }
1364                     });
1365         } else {
1366             mTvView.setTimeShiftPositionCallback(null);
1367         }
1368         if (mTimeShiftListener != null) {
1369             mTimeShiftListener.onAvailabilityChanged();
1370         }
1371     }
1372 
1373     /** Returns if the time shift is available for the current channel. */
1374     @Override
isTimeShiftAvailable()1375     public boolean isTimeShiftAvailable() {
1376         return mTimeShiftAvailable;
1377     }
1378 
1379     /** Plays the media, if the current input supports time-shifting. */
1380     @Override
timeShiftPlay()1381     public void timeShiftPlay() {
1382         if (!isTimeShiftAvailable()) {
1383             throw new IllegalStateException("Time-shift is not supported for the current channel");
1384         }
1385         if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) {
1386             return;
1387         }
1388         mTvView.timeShiftResume();
1389     }
1390 
1391     /** Pauses the media, if the current input supports time-shifting. */
1392     @Override
timeShiftPause()1393     public void timeShiftPause() {
1394         if (!isTimeShiftAvailable()) {
1395             throw new IllegalStateException("Time-shift is not supported for the current channel");
1396         }
1397         if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) {
1398             return;
1399         }
1400         mTvView.timeShiftPause();
1401     }
1402 
1403     /**
1404      * Rewinds the media with the given speed, if the current input supports time-shifting.
1405      *
1406      * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
1407      */
1408     @Override
timeShiftRewind(int speed)1409     public void timeShiftRewind(int speed) {
1410         if (!isTimeShiftAvailable()) {
1411             throw new IllegalStateException("Time-shift is not supported for the current channel");
1412         } else {
1413             if (speed <= 0) {
1414                 throw new IllegalArgumentException("The speed should be a positive integer.");
1415             }
1416             mTimeShiftState = TIME_SHIFT_STATE_REWIND;
1417             PlaybackParams params = new PlaybackParams();
1418             params.setSpeed(speed * -1);
1419             mTvView.timeShiftSetPlaybackParams(params);
1420         }
1421     }
1422 
1423     /**
1424      * Fast-forwards the media with the given speed, if the current input supports time-shifting.
1425      *
1426      * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
1427      */
1428     @Override
timeShiftFastForward(int speed)1429     public void timeShiftFastForward(int speed) {
1430         if (!isTimeShiftAvailable()) {
1431             throw new IllegalStateException("Time-shift is not supported for the current channel");
1432         } else {
1433             if (speed <= 0) {
1434                 throw new IllegalArgumentException("The speed should be a positive integer.");
1435             }
1436             mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD;
1437             PlaybackParams params = new PlaybackParams();
1438             params.setSpeed(speed);
1439             mTvView.timeShiftSetPlaybackParams(params);
1440         }
1441     }
1442 
1443     /**
1444      * Seek to the given time position.
1445      *
1446      * @param timeMs The time in milliseconds to seek to.
1447      */
1448     @Override
timeShiftSeekTo(long timeMs)1449     public void timeShiftSeekTo(long timeMs) {
1450         if (!isTimeShiftAvailable()) {
1451             throw new IllegalStateException("Time-shift is not supported for the current channel");
1452         }
1453         mTvView.timeShiftSeekTo(timeMs);
1454     }
1455 
1456     /** Returns the current playback position in milliseconds. */
1457     @Override
timeShiftGetCurrentPositionMs()1458     public long timeShiftGetCurrentPositionMs() {
1459         if (!isTimeShiftAvailable()) {
1460             throw new IllegalStateException("Time-shift is not supported for the current channel");
1461         }
1462         if (DEBUG) {
1463             Log.d(
1464                     TAG,
1465                     "timeShiftGetCurrentPositionMs: current position ="
1466                             + Utils.toTimeString(mTimeShiftCurrentPositionMs));
1467         }
1468         return mTimeShiftCurrentPositionMs;
1469     }
1470 
createProgramPosterArtCallback( final long channelId)1471     private ImageLoader.ImageLoaderCallback<BlockScreenView> createProgramPosterArtCallback(
1472             final long channelId) {
1473         return new ImageLoader.ImageLoaderCallback<BlockScreenView>(mBlockScreenView) {
1474             @Override
1475             public void onBitmapLoaded(BlockScreenView view, @Nullable Bitmap posterArt) {
1476                 if (posterArt == null
1477                         || getCurrentChannel() == null
1478                         || channelId != getCurrentChannel().getId()
1479                         || !shouldShowImageForTuning()) {
1480                     return;
1481                 }
1482                 Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt);
1483                 drawablePosterArt
1484                         .mutate()
1485                         .setColorFilter(mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER);
1486                 view.setBackgroundImage(drawablePosterArt);
1487             }
1488         };
1489     }
1490 
1491     /** A listener which receives the notification when the screen is blocked/unblocked. */
1492     public abstract static class OnScreenBlockingChangedListener {
1493         /** Called when the screen is blocked/unblocked. */
1494         public abstract void onScreenBlockingChanged(boolean blocked);
1495     }
1496 
1497     private class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
1498         @Override
1499         protected Boolean doInBackground(Void... params) {
1500             return NetworkUtils.isNetworkAvailable(mConnectivityManager);
1501         }
1502 
1503         @Override
1504         protected void onPostExecute(Boolean networkAvailable) {
1505             mInternetCheckTask = null;
1506             if (!networkAvailable
1507                     && isAttachedToWindow()
1508                     && !mScreenBlocked
1509                     && mBlockedContentRating == null
1510                     && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
1511                 mBlockScreenView.setIconVisibility(true);
1512                 mBlockScreenView.setIconImage(R.drawable.ic_sad_cloud);
1513                 mBlockScreenView.setInfoText(R.string.tvview_msg_no_internet_connection);
1514             }
1515         }
1516     }
1517 }
1518