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