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