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