1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.ui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.TimeInterpolator;
22 import android.annotation.SuppressLint;
23 import android.annotation.TargetApi;
24 import android.content.ContentUris;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.media.PlaybackParams;
28 import android.media.tv.TvContentRating;
29 import android.media.tv.TvInputInfo;
30 import android.media.tv.TvInputManager;
31 import android.media.tv.TvTrackInfo;
32 import android.media.tv.TvView;
33 import android.media.tv.TvView.OnUnhandledInputEventListener;
34 import android.media.tv.TvView.TvInputCallback;
35 import android.net.ConnectivityManager;
36 import android.net.Uri;
37 import android.os.AsyncTask;
38 import android.os.Build;
39 import android.os.Bundle;
40 import android.support.annotation.IntDef;
41 import android.support.annotation.Nullable;
42 import android.support.annotation.UiThread;
43 import android.support.v4.os.BuildCompat;
44 import android.text.TextUtils;
45 import android.util.AttributeSet;
46 import android.util.Log;
47 import android.view.KeyEvent;
48 import android.view.MotionEvent;
49 import android.view.SurfaceView;
50 import android.view.View;
51 import android.view.ViewGroup;
52 import android.widget.FrameLayout;
53 import android.widget.ImageView;
54 
55 import com.android.tv.ApplicationSingletons;
56 import com.android.tv.R;
57 import com.android.tv.TvApplication;
58 import com.android.tv.analytics.DurationTimer;
59 import com.android.tv.analytics.Tracker;
60 import com.android.tv.common.feature.CommonFeatures;
61 import com.android.tv.common.recording.RecordedProgram;
62 import com.android.tv.data.Channel;
63 import com.android.tv.data.ChannelDataManager;
64 import com.android.tv.data.StreamInfo;
65 import com.android.tv.data.WatchedHistoryManager;
66 import com.android.tv.dvr.DvrDataManager;
67 import com.android.tv.parental.ContentRatingsManager;
68 import com.android.tv.recommendation.NotificationService;
69 import com.android.tv.util.NetworkUtils;
70 import com.android.tv.util.PermissionUtils;
71 import com.android.tv.util.TvInputManagerHelper;
72 import com.android.tv.util.Utils;
73 
74 import java.lang.annotation.Retention;
75 import java.lang.annotation.RetentionPolicy;
76 import java.lang.reflect.InvocationTargetException;
77 import java.lang.reflect.Method;
78 import java.util.List;
79 
80 public class TunableTvView extends FrameLayout implements StreamInfo {
81     private static final boolean DEBUG = false;
82     private static final String TAG = "TunableTvView";
83 
84     public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1;
85 
86     @Retention(RetentionPolicy.SOURCE)
87     @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
88     public @interface BlockScreenType {}
89     public static final int BLOCK_SCREEN_TYPE_NO_UI = 0;
90     public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1;
91     public static final int BLOCK_SCREEN_TYPE_NORMAL = 2;
92 
93     private static final String PERMISSION_RECEIVE_INPUT_EVENT =
94             "com.android.tv.permission.RECEIVE_INPUT_EVENT";
95 
96     @Retention(RetentionPolicy.SOURCE)
97     @IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE,
98             TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD })
99     private @interface TimeShiftState {}
100     private static final int TIME_SHIFT_STATE_NONE = 0;
101     private static final int TIME_SHIFT_STATE_PLAY = 1;
102     private static final int TIME_SHIFT_STATE_PAUSE = 2;
103     private static final int TIME_SHIFT_STATE_REWIND = 3;
104     private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4;
105 
106     private static final int FADED_IN = 0;
107     private static final int FADED_OUT = 1;
108     private static final int FADING_IN = 2;
109     private static final int FADING_OUT = 3;
110 
111     private static final long INVALID_TIME = -1;
112 
113     // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR.
114     private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f;
115 
116     private AppLayerTvView mTvView;
117     private Channel mCurrentChannel;
118     private RecordedProgram mRecordedProgram;
119     private TvInputManagerHelper mInputManagerHelper;
120     private ContentRatingsManager mContentRatingsManager;
121     @Nullable
122     private WatchedHistoryManager mWatchedHistoryManager;
123     private boolean mStarted;
124     private TvInputInfo mInputInfo;
125     private OnTuneListener mOnTuneListener;
126     private int mVideoWidth;
127     private int mVideoHeight;
128     private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
129     private float mVideoFrameRate;
130     private float mVideoDisplayAspectRatio;
131     private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
132     private boolean mHasClosedCaption = false;
133     private boolean mVideoAvailable;
134     private boolean mScreenBlocked;
135     private OnScreenBlockingChangedListener mOnScreenBlockedListener;
136     private TvContentRating mBlockedContentRating;
137     private int mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED;
138     private boolean mCanReceiveInputEvent;
139     private boolean mIsMuted;
140     private float mVolume;
141     private boolean mParentControlEnabled;
142     private int mFixedSurfaceWidth;
143     private int mFixedSurfaceHeight;
144     private boolean mIsPip;
145     private int mScreenHeight;
146     private int mShrunkenTvViewHeight;
147     private final boolean mCanModifyParentalControls;
148 
149     @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE;
150     private TimeShiftListener mTimeShiftListener;
151     private boolean mTimeShiftAvailable;
152     private long mTimeShiftCurrentPositionMs = INVALID_TIME;
153 
154     private final Tracker mTracker;
155     private final DurationTimer mChannelViewTimer = new DurationTimer();
156     private InternetCheckTask mInternetCheckTask;
157 
158     // A block screen view which has lock icon with black background.
159     // This indicates that user's action is needed to play video.
160     private final BlockScreenView mBlockScreenView;
161 
162     // A View to hide screen when there's problem in video playback.
163     private final BlockScreenView mHideScreenView;
164 
165     // A View to block screen until onContentAllowed is received if parental control is on.
166     private final View mBlockScreenForTuneView;
167 
168     // A spinner view to show buffering status.
169     private final View mBufferingSpinnerView;
170 
171     // A View for fade-in/out animation
172     private final View mDimScreenView;
173     private int mFadeState = FADED_IN;
174     private Runnable mActionAfterFade;
175 
176     @BlockScreenType private int mBlockScreenType;
177 
178     private final DvrDataManager mDvrDataManager;
179     private final ChannelDataManager mChannelDataManager;
180     private final ConnectivityManager mConnectivityManager;
181 
182     private final TvInputCallback mCallback =
183             new TvInputCallback() {
184                 @Override
185                 public void onConnectionFailed(String inputId) {
186                     Log.w(TAG, "Failed to bind an input");
187                     mTracker.sendInputConnectionFailure(inputId);
188                     Channel channel = mCurrentChannel;
189                     mCurrentChannel = null;
190                     mInputInfo = null;
191                     mCanReceiveInputEvent = false;
192                     if (mOnTuneListener != null) {
193                         // If tune is called inside onTuneFailed, mOnTuneListener will be set to
194                         // a new instance. In order to avoid to clear the new mOnTuneListener,
195                         // we copy mOnTuneListener to l and clear mOnTuneListener before
196                         // calling onTuneFailed.
197                         OnTuneListener listener = mOnTuneListener;
198                         mOnTuneListener = null;
199                         listener.onTuneFailed(channel);
200                     }
201                 }
202 
203                 @Override
204                 public void onDisconnected(String inputId) {
205                     Log.w(TAG, "Session is released by crash");
206                     mTracker.sendInputDisconnected(inputId);
207                     Channel channel = mCurrentChannel;
208                     mCurrentChannel = null;
209                     mInputInfo = null;
210                     mCanReceiveInputEvent = false;
211                     if (mOnTuneListener != null) {
212                         OnTuneListener listener = mOnTuneListener;
213                         mOnTuneListener = null;
214                         listener.onUnexpectedStop(channel);
215                     }
216                 }
217 
218                 @Override
219                 public void onChannelRetuned(String inputId, Uri channelUri) {
220                     if (DEBUG) {
221                         Log.d(TAG, "onChannelRetuned(inputId=" + inputId + ", channelUri="
222                                 + channelUri + ")");
223                     }
224                     if (mOnTuneListener != null) {
225                         mOnTuneListener.onChannelRetuned(channelUri);
226                     }
227                 }
228 
229                 @Override
230                 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
231                     mHasClosedCaption = false;
232                     for (TvTrackInfo track : tracks) {
233                         if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
234                             mHasClosedCaption = true;
235                             break;
236                         }
237                     }
238                     if (mOnTuneListener != null) {
239                         mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
240                     }
241                 }
242 
243                 @Override
244                 public void onTrackSelected(String inputId, int type, String trackId) {
245                     if (trackId == null) {
246                         // A track is unselected.
247                         if (type == TvTrackInfo.TYPE_VIDEO) {
248                             mVideoWidth = 0;
249                             mVideoHeight = 0;
250                             mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
251                             mVideoFrameRate = 0f;
252                             mVideoDisplayAspectRatio = 0f;
253                         } else if (type == TvTrackInfo.TYPE_AUDIO) {
254                             mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
255                         }
256                     } else {
257                         List<TvTrackInfo> tracks = getTracks(type);
258                         boolean trackFound = false;
259                         if (tracks != null) {
260                             for (TvTrackInfo track : tracks) {
261                                 if (track.getId().equals(trackId)) {
262                                     if (type == TvTrackInfo.TYPE_VIDEO) {
263                                         mVideoWidth = track.getVideoWidth();
264                                         mVideoHeight = track.getVideoHeight();
265                                         mVideoFormat = Utils.getVideoDefinitionLevelFromSize(
266                                                 mVideoWidth, mVideoHeight);
267                                         mVideoFrameRate = track.getVideoFrameRate();
268                                         if (mVideoWidth <= 0 || mVideoHeight <= 0) {
269                                             mVideoDisplayAspectRatio = 0.0f;
270                                         } else if (android.os.Build.VERSION.SDK_INT >=
271                                                 android.os.Build.VERSION_CODES.M) {
272                                             float VideoPixelAspectRatio =
273                                                     track.getVideoPixelAspectRatio();
274                                             mVideoDisplayAspectRatio = VideoPixelAspectRatio
275                                                     * mVideoWidth / mVideoHeight;
276                                         } else {
277                                             mVideoDisplayAspectRatio = mVideoWidth
278                                                     / (float) mVideoHeight;
279                                         }
280                                     } else if (type == TvTrackInfo.TYPE_AUDIO) {
281                                         mAudioChannelCount = track.getAudioChannelCount();
282                                     }
283                                     trackFound = true;
284                                     break;
285                                 }
286                             }
287                         }
288                         if (!trackFound) {
289                             Log.w(TAG, "Invalid track ID: " + trackId);
290                         }
291                     }
292                     if (mOnTuneListener != null) {
293                         mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
294                     }
295                 }
296 
297                 @Override
298                 public void onVideoAvailable(String inputId) {
299                     unhideScreenByVideoAvailability();
300                     if (mOnTuneListener != null) {
301                         mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
302                     }
303                 }
304 
305                 @Override
306                 public void onVideoUnavailable(String inputId, int reason) {
307                     hideScreenByVideoAvailability(reason);
308                     if (mOnTuneListener != null) {
309                         mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
310                     }
311                     switch (reason) {
312                         case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
313                         case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
314                         case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
315                             mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
316                         default:
317                             // do nothing
318                     }
319                 }
320 
321                 @Override
322                 public void onContentAllowed(String inputId) {
323                     mBlockScreenForTuneView.setVisibility(View.GONE);
324                     unblockScreenByContentRating();
325                     if (mOnTuneListener != null) {
326                         mOnTuneListener.onContentAllowed();
327                     }
328                 }
329 
330                 @Override
331                 public void onContentBlocked(String inputId, TvContentRating rating) {
332                     blockScreenByContentRating(rating);
333                     if (mOnTuneListener != null) {
334                         mOnTuneListener.onContentBlocked();
335                     }
336                 }
337 
338                 @Override
339                 @TargetApi(Build.VERSION_CODES.M)
340                 public void onTimeShiftStatusChanged(String inputId, int status) {
341                     boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
342                     setTimeShiftAvailable(available);
343                 }
344             };
345 
TunableTvView(Context context)346     public TunableTvView(Context context) {
347         this(context, null);
348     }
349 
TunableTvView(Context context, AttributeSet attrs)350     public TunableTvView(Context context, AttributeSet attrs) {
351         this(context, attrs, 0);
352     }
353 
TunableTvView(Context context, AttributeSet attrs, int defStyleAttr)354     public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) {
355         this(context, attrs, defStyleAttr, 0);
356     }
357 
TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)358     public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
359         super(context, attrs, defStyleAttr, defStyleRes);
360         inflate(getContext(), R.layout.tunable_tv_view, this);
361 
362         ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
363         mDvrDataManager = CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN()
364                 ? appSingletons.getDvrDataManager()
365                 : null;
366         mChannelDataManager = appSingletons.getChannelDataManager();
367         mConnectivityManager = (ConnectivityManager) context
368                 .getSystemService(Context.CONNECTIVITY_SERVICE);
369         mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
370         mTracker = appSingletons.getTracker();
371         mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
372         mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
373         if (!mCanModifyParentalControls) {
374             mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission);
375             mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER);
376         } else {
377             mBlockScreenView.setImage(R.drawable.ic_message_lock);
378         }
379         mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview);
380         mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() {
381             @Override
382             public void onAnimationEnd(Animator animation) {
383                 adjustBlockScreenSpacingAndText();
384             }
385         });
386 
387         mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen);
388         mHideScreenView.setImageVisibility(false);
389         mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
390         mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune);
391         mDimScreenView = findViewById(R.id.dim);
392         mDimScreenView.animate().setListener(new AnimatorListenerAdapter() {
393             @Override
394             public void onAnimationEnd(Animator animation) {
395                 if (mActionAfterFade != null) {
396                     mActionAfterFade.run();
397                 }
398             }
399 
400             @Override
401             public void onAnimationCancel(Animator animation) {
402                 if (mActionAfterFade != null) {
403                     mActionAfterFade.run();
404                 }
405             }
406         });
407     }
408 
initialize(AppLayerTvView tvView, boolean isPip, int screenHeight, int shrunkenTvViewHeight)409     public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight,
410             int shrunkenTvViewHeight) {
411         mTvView = tvView;
412         mIsPip = isPip;
413         mScreenHeight = screenHeight;
414         mShrunkenTvViewHeight = shrunkenTvViewHeight;
415         mTvView.setZOrderOnTop(isPip);
416         copyLayoutParamsToTvView();
417     }
418 
start(TvInputManagerHelper tvInputManagerHelper)419     public void start(TvInputManagerHelper tvInputManagerHelper) {
420         mInputManagerHelper = tvInputManagerHelper;
421         mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
422         if (mStarted) {
423             return;
424         }
425         mStarted = true;
426     }
427 
stop()428     public void stop() {
429         if (!mStarted) {
430             return;
431         }
432         mStarted = false;
433         if (mCurrentChannel != null) {
434             long duration = mChannelViewTimer.reset();
435             mTracker.sendChannelViewStop(mCurrentChannel, duration);
436             if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
437                 mWatchedHistoryManager.logChannelViewStop(mCurrentChannel,
438                         System.currentTimeMillis(), duration);
439             }
440         }
441         reset();
442     }
443 
reset()444     public void reset() {
445         mTvView.reset();
446         mCurrentChannel = null;
447         mRecordedProgram = null;
448         mInputInfo = null;
449         mCanReceiveInputEvent = false;
450         mOnTuneListener = null;
451         setTimeShiftAvailable(false);
452         hideScreenByVideoAvailability(VIDEO_UNAVAILABLE_REASON_NOT_TUNED);
453     }
454 
setMain()455     public void setMain() {
456         mTvView.setMain();
457     }
458 
setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager)459     public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
460         mWatchedHistoryManager = watchedHistoryManager;
461     }
462 
isPlaying()463     public boolean isPlaying() {
464         return mStarted;
465     }
466 
467     /**
468      * Called when parental control is changed.
469      */
onParentalControlChanged(boolean enabled)470     public void onParentalControlChanged(boolean enabled) {
471         mParentControlEnabled = enabled;
472         if (!mParentControlEnabled) {
473             mBlockScreenForTuneView.setVisibility(View.GONE);
474         }
475     }
476 
477     /**
478      * Returns {@code true}, if this view is the recording playback mode.
479      */
isRecordingPlayback()480     public boolean isRecordingPlayback() {
481         return mRecordedProgram != null;
482     }
483 
484     /**
485      * Returns the recording which is being played right now.
486      */
getPlayingRecordedProgram()487     public RecordedProgram getPlayingRecordedProgram() {
488         return mRecordedProgram;
489     }
490 
491     /**
492      * Plays a recording.
493      */
playRecording(Uri recordingUri, OnTuneListener listener)494     public boolean playRecording(Uri recordingUri, OnTuneListener listener) {
495         if (!mStarted) {
496             throw new IllegalStateException("TvView isn't started");
497         }
498         if (!CommonFeatures.DVR.isEnabled(getContext()) || !BuildCompat.isAtLeastN()) {
499             return false;
500         }
501         if (DEBUG) Log.d(TAG, "playRecording " + recordingUri);
502         long recordingId = ContentUris.parseId(recordingUri);
503         mRecordedProgram = mDvrDataManager.getRecordedProgram(recordingId);
504         if (mRecordedProgram == null) {
505             Log.w(TAG, "No recorded program (Uri=" + recordingUri + ")");
506             return false;
507         }
508         String inputId = mRecordedProgram.getInputId();
509         TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(inputId);
510         if (inputInfo == null) {
511             return false;
512         }
513         mOnTuneListener = listener;
514         // mCurrentChannel can be null.
515         mCurrentChannel = mChannelDataManager.getChannel(mRecordedProgram.getChannelId());
516         // For recording playback, input event should not be sent.
517         mCanReceiveInputEvent = false;
518         boolean needSurfaceSizeUpdate = false;
519         if (!inputInfo.equals(mInputInfo)) {
520             mInputInfo = inputInfo;
521             if (DEBUG) {
522                 Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: "
523                         + mCanReceiveInputEvent);
524             }
525             needSurfaceSizeUpdate = true;
526         }
527         mChannelViewTimer.start();
528         mVideoWidth = 0;
529         mVideoHeight = 0;
530         mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
531         mVideoFrameRate = 0f;
532         mVideoDisplayAspectRatio = 0f;
533         mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
534         mHasClosedCaption = false;
535         mTvView.setCallback(mCallback);
536         mTimeShiftCurrentPositionMs = INVALID_TIME;
537         mTvView.setTimeShiftPositionCallback(null);
538         setTimeShiftAvailable(false);
539         mTvView.timeShiftPlay(inputId, recordingUri);
540         if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
541             // When the input is changed, TvView recreates its SurfaceView internally.
542             // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
543             getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
544         }
545         hideScreenByVideoAvailability(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
546         unblockScreenByContentRating();
547         if (mParentControlEnabled) {
548             mBlockScreenForTuneView.setVisibility(View.VISIBLE);
549         }
550         if (mOnTuneListener != null) {
551             mOnTuneListener.onStreamInfoChanged(this);
552         }
553         return true;
554     }
555 
556     /**
557      * Tunes to a channel with the {@code channelId}.
558      *
559      * @param params extra data to send it to TIS and store the data in TIMS.
560      * @return false, if the TV input is not a proper state to tune to a channel. For example,
561      *         if the state is disconnected or channelId doesn't exist, it returns false.
562      */
tuneTo(Channel channel, Bundle params, OnTuneListener listener)563     public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) {
564         if (!mStarted) {
565             throw new IllegalStateException("TvView isn't started");
566         }
567         if (DEBUG) Log.d(TAG, "tuneTo " + channel);
568         TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(channel.getInputId());
569         if (inputInfo == null) {
570             return false;
571         }
572         if (mCurrentChannel != null) {
573             long duration = mChannelViewTimer.reset();
574             mTracker.sendChannelViewStop(mCurrentChannel, duration);
575             if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
576                 mWatchedHistoryManager.logChannelViewStop(mCurrentChannel,
577                         System.currentTimeMillis(), duration);
578             }
579         }
580         mOnTuneListener = listener;
581         mCurrentChannel = channel;
582         mRecordedProgram = null;
583         boolean tunedByRecommendation = params != null
584                 && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null;
585         boolean needSurfaceSizeUpdate = false;
586         if (!inputInfo.equals(mInputInfo)) {
587             mInputInfo = inputInfo;
588             mCanReceiveInputEvent = getContext().getPackageManager().checkPermission(
589                     PERMISSION_RECEIVE_INPUT_EVENT, mInputInfo.getServiceInfo().packageName)
590                             == PackageManager.PERMISSION_GRANTED;
591             if (DEBUG) {
592                 Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: "
593                         + mCanReceiveInputEvent);
594             }
595             needSurfaceSizeUpdate = true;
596         }
597         mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation);
598         mChannelViewTimer.start();
599         mVideoWidth = 0;
600         mVideoHeight = 0;
601         mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
602         mVideoFrameRate = 0f;
603         mVideoDisplayAspectRatio = 0f;
604         mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
605         mHasClosedCaption = false;
606         mTvView.setCallback(mCallback);
607         mTimeShiftCurrentPositionMs = INVALID_TIME;
608         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
609             // To reduce the IPCs, unregister the callback here and register it when necessary.
610             mTvView.setTimeShiftPositionCallback(null);
611         }
612         setTimeShiftAvailable(false);
613         mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);
614         if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
615             // When the input is changed, TvView recreates its SurfaceView internally.
616             // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
617             getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
618         }
619         hideScreenByVideoAvailability(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
620         unblockScreenByContentRating();
621         if (channel.isPassthrough()) {
622             mBlockScreenForTuneView.setVisibility(View.GONE);
623         } else if (mParentControlEnabled) {
624             mBlockScreenForTuneView.setVisibility(View.VISIBLE);
625         }
626         if (mOnTuneListener != null) {
627             mOnTuneListener.onStreamInfoChanged(this);
628         }
629         return true;
630     }
631 
632     @Override
getCurrentChannel()633     public Channel getCurrentChannel() {
634         return mCurrentChannel;
635     }
636 
637     /**
638      * Sets the current channel. Call this method only when setting the current channel without
639      * actually tuning to it.
640      *
641      * @param currentChannel The new current channel to set to.
642      */
setCurrentChannel(Channel currentChannel)643     public void setCurrentChannel(Channel currentChannel) {
644         mCurrentChannel = currentChannel;
645     }
646 
setStreamVolume(float volume)647     public void setStreamVolume(float volume) {
648         if (!mStarted) {
649             throw new IllegalStateException("TvView isn't started");
650         }
651         if (DEBUG) Log.d(TAG, "setStreamVolume " + volume);
652         mVolume = volume;
653         if (!mIsMuted) {
654             mTvView.setStreamVolume(volume);
655         }
656     }
657 
658     /**
659      * Sets fixed size for the internal {@link android.view.Surface} of
660      * {@link android.media.tv.TvView}. If either {@code width} or {@code height} is non positive,
661      * the {@link android.view.Surface}'s size will be matched to the layout.
662      *
663      * Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called,
664      * {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size
665      * of {@link android.view.SurfaceView} is changed without changing either left position or top
666      * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow().
667      */
setFixedSurfaceSize(int width, int height)668     public void setFixedSurfaceSize(int width, int height) {
669         mFixedSurfaceWidth = width;
670         mFixedSurfaceHeight = height;
671         if (mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
672             // When the input is changed, TvView recreates its SurfaceView internally.
673             // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
674             SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0);
675             surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
676         } else {
677             SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0);
678             surfaceView.getHolder().setSizeFromLayout();
679         }
680     }
681 
682     @Override
dispatchKeyEvent(KeyEvent event)683     public boolean dispatchKeyEvent(KeyEvent event) {
684         return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event);
685     }
686 
687     @Override
dispatchTouchEvent(MotionEvent event)688     public boolean dispatchTouchEvent(MotionEvent event) {
689         return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event);
690     }
691 
692     @Override
dispatchTrackballEvent(MotionEvent event)693     public boolean dispatchTrackballEvent(MotionEvent event) {
694         return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event);
695     }
696 
697     @Override
dispatchGenericMotionEvent(MotionEvent event)698     public boolean dispatchGenericMotionEvent(MotionEvent event) {
699         return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event);
700     }
701 
702     public interface OnTuneListener {
onTuneFailed(Channel channel)703         void onTuneFailed(Channel channel);
onUnexpectedStop(Channel channel)704         void onUnexpectedStop(Channel channel);
onStreamInfoChanged(StreamInfo info)705         void onStreamInfoChanged(StreamInfo info);
onChannelRetuned(Uri channel)706         void onChannelRetuned(Uri channel);
onContentBlocked()707         void onContentBlocked();
onContentAllowed()708         void onContentAllowed();
709     }
710 
unblockContent(TvContentRating rating)711     public void unblockContent(TvContentRating rating) {
712         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
713             try {
714                 Method method = TvView.class.getMethod("requestUnblockContent",
715                         TvContentRating.class);
716                 method.invoke(mTvView, rating);
717             } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) {
718                 e.printStackTrace();
719             }
720         } else {
721             mTvView.unblockContent(rating);
722         }
723     }
724 
725     @Override
getVideoWidth()726     public int getVideoWidth() {
727         return mVideoWidth;
728     }
729 
730     @Override
getVideoHeight()731     public int getVideoHeight() {
732         return mVideoHeight;
733     }
734 
735     @Override
getVideoDefinitionLevel()736     public int getVideoDefinitionLevel() {
737         return mVideoFormat;
738     }
739 
740     @Override
getVideoFrameRate()741     public float getVideoFrameRate() {
742         return mVideoFrameRate;
743     }
744 
745     /**
746      * Returns displayed aspect ratio (video width / video height * pixel ratio).
747      */
748     @Override
getVideoDisplayAspectRatio()749     public float getVideoDisplayAspectRatio() {
750         return mVideoDisplayAspectRatio;
751     }
752 
753     @Override
getAudioChannelCount()754     public int getAudioChannelCount() {
755         return mAudioChannelCount;
756     }
757 
758     @Override
hasClosedCaption()759     public boolean hasClosedCaption() {
760         return mHasClosedCaption;
761     }
762 
763     @Override
isVideoAvailable()764     public boolean isVideoAvailable() {
765         return mVideoAvailable;
766     }
767 
768     @Override
getVideoUnavailableReason()769     public int getVideoUnavailableReason() {
770         return mVideoUnavailableReason;
771     }
772 
773     /**
774      * Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}.
775      */
getSurfaceView()776     private SurfaceView getSurfaceView() {
777         return (SurfaceView) mTvView.getChildAt(0);
778     }
779 
setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener)780     public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
781         mTvView.setOnUnhandledInputEventListener(listener);
782     }
783 
setClosedCaptionEnabled(boolean enabled)784     public void setClosedCaptionEnabled(boolean enabled) {
785         mTvView.setCaptionEnabled(enabled);
786     }
787 
getTracks(int type)788     public List<TvTrackInfo> getTracks(int type) {
789         return mTvView.getTracks(type);
790     }
791 
getSelectedTrack(int type)792     public String getSelectedTrack(int type) {
793         return mTvView.getSelectedTrack(type);
794     }
795 
selectTrack(int type, String trackId)796     public void selectTrack(int type, String trackId) {
797         mTvView.selectTrack(type, trackId);
798     }
799 
800     /**
801      * Returns if the screen is blocked by {@link #blockScreen()}.
802      */
isScreenBlocked()803     public boolean isScreenBlocked() {
804         return mScreenBlocked;
805     }
806 
setOnScreenBlockedListener(OnScreenBlockingChangedListener listener)807     public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) {
808         mOnScreenBlockedListener = listener;
809     }
810 
811     /**
812      * Returns currently blocked content rating. {@code null} if it's not blocked.
813      */
getBlockedContentRating()814     public TvContentRating getBlockedContentRating() {
815         return mBlockedContentRating;
816     }
817 
818     /**
819      * Locks current TV screen and mutes.
820      * There would be black screen with lock icon in order to show that
821      * screen block is intended and not an error.
822      * TODO: Accept parameter to show lock icon or not.
823      */
blockScreen()824     public void blockScreen() {
825         mScreenBlocked = true;
826         checkBlockScreenAndMuteNeeded();
827         if (mOnScreenBlockedListener != null) {
828             mOnScreenBlockedListener.onScreenBlockingChanged(true);
829         }
830     }
831 
blockScreenByContentRating(TvContentRating rating)832     private void blockScreenByContentRating(TvContentRating rating) {
833         mBlockedContentRating = rating;
834         checkBlockScreenAndMuteNeeded();
835     }
836 
837     @Override
838     @SuppressLint("RtlHardcoded")
onLayout(boolean changed, int left, int top, int right, int bottom)839     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
840         super.onLayout(changed, left, top, right, bottom);
841         if (mIsPip) {
842             int height = bottom - top;
843             float scale;
844             if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) {
845                 scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight;
846             } else {
847                 scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight;
848             }
849             // TODO: need to get UX confirmation.
850             mBlockScreenView.scaleContainerView(scale);
851         }
852     }
853 
854     @Override
setLayoutParams(ViewGroup.LayoutParams params)855     public void setLayoutParams(ViewGroup.LayoutParams params) {
856         super.setLayoutParams(params);
857         if (mTvView != null) {
858             copyLayoutParamsToTvView();
859         }
860     }
861 
copyLayoutParamsToTvView()862     private void copyLayoutParamsToTvView() {
863         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
864         FrameLayout.LayoutParams tvViewLp = (FrameLayout.LayoutParams) mTvView.getLayoutParams();
865         if (tvViewLp.bottomMargin != lp.bottomMargin
866                 || tvViewLp.topMargin != lp.topMargin
867                 || tvViewLp.leftMargin != lp.leftMargin
868                 || tvViewLp.rightMargin != lp.rightMargin
869                 || tvViewLp.gravity != lp.gravity
870                 || tvViewLp.height != lp.height
871                 || tvViewLp.width != lp.width) {
872             if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin) {
873                 // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is
874                 // used, SurfaceView doesn't catch the width and height change. It causes a bug that
875                 // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for
876                 // small size PIP as a workaround.
877                 tvViewLp.leftMargin = lp.leftMargin + 1;
878             } else {
879                 tvViewLp.leftMargin = lp.leftMargin;
880             }
881             tvViewLp.topMargin = lp.topMargin;
882             tvViewLp.bottomMargin = lp.bottomMargin;
883             tvViewLp.rightMargin = lp.rightMargin;
884             tvViewLp.gravity = lp.gravity;
885             tvViewLp.height = lp.height;
886             tvViewLp.width = lp.width;
887             mTvView.setLayoutParams(tvViewLp);
888         }
889     }
890 
891     @Override
onVisibilityChanged(View changedView, int visibility)892     protected void onVisibilityChanged(View changedView, int visibility) {
893         super.onVisibilityChanged(changedView, visibility);
894         if (mTvView != null) {
895             mTvView.setVisibility(visibility);
896         }
897     }
898 
899     /**
900      * Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the
901      * block screen will not show any description such as a lock icon and a text for the blocked
902      * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block screen
903      * will show the description for shrunken tv view (Small icon and short text), and if
904      * {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the
905      * description for normal tv view (Big icon and long text).
906      *
907      * @param type The type of block screen to set.
908      */
setBlockScreenType(@lockScreenType int type)909     public void setBlockScreenType(@BlockScreenType int type) {
910         // TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse.
911         if (mBlockScreenType != type) {
912             mBlockScreenType = type;
913             updateBlockScreenUI(true);
914         }
915     }
916 
updateBlockScreenUI(boolean animation)917     private void updateBlockScreenUI(boolean animation) {
918         mBlockScreenView.endAnimations();
919 
920         if (!mScreenBlocked && mBlockedContentRating == null) {
921             mBlockScreenView.setVisibility(GONE);
922             return;
923         }
924 
925         mBlockScreenView.setVisibility(VISIBLE);
926         if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) {
927             adjustBlockScreenSpacingAndText();
928         }
929         mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
930     }
931 
adjustBlockScreenSpacingAndText()932     private void adjustBlockScreenSpacingAndText() {
933         // TODO: need to add animation for padding change when the block screen type is changed
934         // NORMAL to SHRUNKEN and vice verse.
935         mBlockScreenView.setSpacing(mBlockScreenType);
936         String text = getBlockScreenText();
937         if (text != null) {
938             mBlockScreenView.setText(text);
939         }
940     }
941 
942     /**
943      * Returns the block screen text corresponding to the current status.
944      * Note that returning {@code null} value means that the current text should not be changed.
945      */
getBlockScreenText()946     private String getBlockScreenText() {
947         if (mScreenBlocked) {
948             switch (mBlockScreenType) {
949                 case BLOCK_SCREEN_TYPE_NO_UI:
950                 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
951                     return "";
952                 case BLOCK_SCREEN_TYPE_NORMAL:
953                     if (mCanModifyParentalControls) {
954                         return getResources().getString(R.string.tvview_channel_locked);
955                     } else {
956                         return getResources().getString(
957                                 R.string.tvview_channel_locked_no_permission);
958                     }
959             }
960         } else if (mBlockedContentRating != null) {
961             String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating);
962             switch (mBlockScreenType) {
963                 case BLOCK_SCREEN_TYPE_NO_UI:
964                     return "";
965                 case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
966                     if (TextUtils.isEmpty(name)) {
967                         return getResources().getString(R.string.shrunken_tvview_content_locked);
968                     } else {
969                         return getContext().getString(
970                                 R.string.shrunken_tvview_content_locked_format, name);
971                     }
972                 case BLOCK_SCREEN_TYPE_NORMAL:
973                     if (TextUtils.isEmpty(name)) {
974                         if (mCanModifyParentalControls) {
975                             return getResources().getString(R.string.tvview_content_locked);
976                         } else {
977                             return getResources().getString(
978                                     R.string.tvview_content_locked_no_permission);
979                         }
980                     } else {
981                         if (mCanModifyParentalControls) {
982                             return getContext().getString(
983                                     R.string.tvview_content_locked_format, name);
984                         } else {
985                             return getContext().getString(
986                                     R.string.tvview_content_locked_format_no_permission, name);
987                         }
988                     }
989             }
990         }
991         return null;
992     }
993 
checkBlockScreenAndMuteNeeded()994     private void checkBlockScreenAndMuteNeeded() {
995         updateBlockScreenUI(false);
996         if (mScreenBlocked || mBlockedContentRating != null) {
997             mute();
998             if (mIsPip) {
999                 // If we don't make mTvView invisible, some frames are leaked when a user changes
1000                 // PIP layout in options.
1001                 // Note: When video is unavailable, we keep the mTvView's visibility, because
1002                 // TIS implementation may not send video available with no surface.
1003                 mTvView.setVisibility(View.INVISIBLE);
1004             }
1005         } else {
1006             unmuteIfPossible();
1007             if (mIsPip) {
1008                 mTvView.setVisibility(View.VISIBLE);
1009             }
1010         }
1011     }
1012 
unblockScreen()1013     public void unblockScreen() {
1014         mScreenBlocked = false;
1015         checkBlockScreenAndMuteNeeded();
1016         if (mOnScreenBlockedListener != null) {
1017             mOnScreenBlockedListener.onScreenBlockingChanged(false);
1018         }
1019     }
1020 
unblockScreenByContentRating()1021     private void unblockScreenByContentRating() {
1022         mBlockedContentRating = null;
1023         checkBlockScreenAndMuteNeeded();
1024     }
1025 
1026     @UiThread
hideScreenByVideoAvailability(int reason)1027     private void hideScreenByVideoAvailability(int reason) {
1028         mVideoAvailable = false;
1029         mVideoUnavailableReason = reason;
1030         if (mInternetCheckTask != null) {
1031             mInternetCheckTask.cancel(true);
1032             mInternetCheckTask = null;
1033         }
1034         switch (reason) {
1035             case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
1036                 mHideScreenView.setVisibility(VISIBLE);
1037                 mHideScreenView.setImageVisibility(false);
1038                 mHideScreenView.setText(R.string.tvview_msg_audio_only);
1039                 mBufferingSpinnerView.setVisibility(GONE);
1040                 unmuteIfPossible();
1041                 break;
1042             case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
1043                 mBufferingSpinnerView.setVisibility(VISIBLE);
1044                 mute();
1045                 break;
1046             case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
1047                 mHideScreenView.setVisibility(VISIBLE);
1048                 mHideScreenView.setText(R.string.tvview_msg_weak_signal);
1049                 mBufferingSpinnerView.setVisibility(GONE);
1050                 mute();
1051                 break;
1052             case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
1053             case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
1054                 mHideScreenView.setVisibility(VISIBLE);
1055                 mHideScreenView.setImageVisibility(false);
1056                 mHideScreenView.setText(null);
1057                 mBufferingSpinnerView.setVisibility(GONE);
1058                 mute();
1059                 break;
1060             case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
1061             default:
1062                 mHideScreenView.setVisibility(VISIBLE);
1063                 mHideScreenView.setImageVisibility(false);
1064                 mHideScreenView.setText(null);
1065                 mBufferingSpinnerView.setVisibility(GONE);
1066                 mute();
1067                 if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) {
1068                     mInternetCheckTask = new InternetCheckTask();
1069                     mInternetCheckTask.execute();
1070                 }
1071                 break;
1072         }
1073     }
1074 
unhideScreenByVideoAvailability()1075     private void unhideScreenByVideoAvailability() {
1076         mVideoAvailable = true;
1077         mHideScreenView.setVisibility(GONE);
1078         mBufferingSpinnerView.setVisibility(GONE);
1079         unmuteIfPossible();
1080     }
1081 
unmuteIfPossible()1082     private void unmuteIfPossible() {
1083         if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) {
1084             unmute();
1085         }
1086     }
1087 
mute()1088     private void mute() {
1089         mIsMuted = true;
1090         mTvView.setStreamVolume(0);
1091     }
1092 
unmute()1093     private void unmute() {
1094         mIsMuted = false;
1095         mTvView.setStreamVolume(mVolume);
1096     }
1097 
1098     /** Returns true if this view is faded out. */
isFadedOut()1099     public boolean isFadedOut() {
1100         return mFadeState == FADED_OUT;
1101     }
1102 
1103     /** Fade out this TunableTvView. Fade out by increasing the dimming. */
fadeOut(int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1104     public void fadeOut(int durationMillis, TimeInterpolator interpolator,
1105             final Runnable actionAfterFade) {
1106         mDimScreenView.setAlpha(0f);
1107         mDimScreenView.setVisibility(View.VISIBLE);
1108         mDimScreenView.animate()
1109                 .alpha(1f)
1110                 .setDuration(durationMillis)
1111                 .setInterpolator(interpolator)
1112                 .withStartAction(new Runnable() {
1113                     @Override
1114                     public void run() {
1115                         mFadeState = FADING_OUT;
1116                         mActionAfterFade = actionAfterFade;
1117                     }
1118                 })
1119                 .withEndAction(new Runnable() {
1120                     @Override
1121                     public void run() {
1122                         mFadeState = FADED_OUT;
1123                     }
1124                 });
1125     }
1126 
1127     /** Fade in this TunableTvView. Fade in by decreasing the dimming. */
fadeIn(int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade)1128     public void fadeIn(int durationMillis, TimeInterpolator interpolator,
1129             final Runnable actionAfterFade) {
1130         mDimScreenView.setAlpha(1f);
1131         mDimScreenView.setVisibility(View.VISIBLE);
1132         mDimScreenView.animate()
1133                 .alpha(0f)
1134                 .setDuration(durationMillis)
1135                 .setInterpolator(interpolator)
1136                 .withStartAction(new Runnable() {
1137                     @Override
1138                     public void run() {
1139                         mFadeState = FADING_IN;
1140                         mActionAfterFade = actionAfterFade;
1141                     }
1142                 })
1143                 .withEndAction(new Runnable() {
1144                     @Override
1145                     public void run() {
1146                         mFadeState = FADED_IN;
1147                         mDimScreenView.setVisibility(View.GONE);
1148                     }
1149                 });
1150     }
1151 
1152     /** Remove the fade effect. */
removeFadeEffect()1153     public void removeFadeEffect() {
1154         mDimScreenView.animate().cancel();
1155         mDimScreenView.setVisibility(View.GONE);
1156         mFadeState = FADED_IN;
1157     }
1158 
1159     /**
1160      * Sets the TimeShiftListener
1161      *
1162      * @param listener The instance of {@link TimeShiftListener}.
1163      */
setTimeShiftListener(TimeShiftListener listener)1164     public void setTimeShiftListener(TimeShiftListener listener) {
1165         mTimeShiftListener = listener;
1166     }
1167 
setTimeShiftAvailable(boolean isTimeShiftAvailable)1168     private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
1169         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || mTimeShiftAvailable == isTimeShiftAvailable) {
1170             return;
1171         }
1172         mTimeShiftAvailable = isTimeShiftAvailable;
1173         if (isTimeShiftAvailable) {
1174             mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() {
1175                 @Override
1176                 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
1177                     if (mTimeShiftListener != null && mCurrentChannel != null
1178                             && mCurrentChannel.getInputId().equals(inputId)) {
1179                         mTimeShiftListener.onRecordStartTimeChanged(timeMs);
1180                     }
1181                 }
1182 
1183                 @Override
1184                 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
1185                     mTimeShiftCurrentPositionMs = timeMs;
1186                 }
1187             });
1188         } else {
1189             mTvView.setTimeShiftPositionCallback(null);
1190         }
1191         if (mTimeShiftListener != null) {
1192             mTimeShiftListener.onAvailabilityChanged();
1193         }
1194     }
1195 
1196     /**
1197      * Returns if the time shift is available for the current channel.
1198      */
isTimeShiftAvailable()1199     public boolean isTimeShiftAvailable() {
1200         return mTimeShiftAvailable;
1201     }
1202 
1203     /**
1204      * Returns the current time-shift state. It returns one of {@link #TIME_SHIFT_STATE_NONE},
1205      * {@link #TIME_SHIFT_STATE_PLAY}, {@link #TIME_SHIFT_STATE_PAUSE},
1206      * {@link #TIME_SHIFT_STATE_REWIND}, {@link #TIME_SHIFT_STATE_FAST_FORWARD}
1207      * or {@link #TIME_SHIFT_STATE_PAUSE}.
1208      */
getTimeShiftState()1209     @TimeShiftState public int getTimeShiftState() {
1210         return mTimeShiftState;
1211     }
1212 
1213     /**
1214      * Plays the media, if the current input supports time-shifting.
1215      */
timeshiftPlay()1216     public void timeshiftPlay() {
1217         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1218             Log.w(TAG, "Time shifting is not supported in this platform.");
1219             return;
1220         }
1221         if (!isTimeShiftAvailable()) {
1222             throw new IllegalStateException("Time-shift is not supported for the current channel");
1223         }
1224         if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) {
1225             return;
1226         }
1227         mTvView.timeShiftResume();
1228     }
1229 
1230     /**
1231      * Pauses the media, if the current input supports time-shifting.
1232      */
timeshiftPause()1233     public void timeshiftPause() {
1234         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1235             Log.w(TAG, "Time shifting is not supported in this platform.");
1236             return;
1237         }
1238         if (!isTimeShiftAvailable()) {
1239             throw new IllegalStateException("Time-shift is not supported for the current channel");
1240         }
1241         if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) {
1242             return;
1243         }
1244         mTvView.timeShiftPause();
1245     }
1246 
1247     /**
1248      * Rewinds the media with the given speed, if the current input supports time-shifting.
1249      *
1250      * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
1251      */
timeshiftRewind(int speed)1252     public void timeshiftRewind(int speed) {
1253         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1254             Log.w(TAG, "Time shifting is not supported in this platform.");
1255         } else if (!isTimeShiftAvailable()) {
1256             throw new IllegalStateException("Time-shift is not supported for the current channel");
1257         } else {
1258             if (speed <= 0) {
1259                 throw new IllegalArgumentException("The speed should be a positive integer.");
1260             }
1261             mTimeShiftState = TIME_SHIFT_STATE_REWIND;
1262             PlaybackParams params = new PlaybackParams();
1263             params.setSpeed(speed * -1);
1264             mTvView.timeShiftSetPlaybackParams(params);
1265         }
1266     }
1267 
1268     /**
1269      * Fast-forwards the media with the given speed, if the current input supports time-shifting.
1270      *
1271      * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
1272      */
timeshiftFastForward(int speed)1273     public void timeshiftFastForward(int speed) {
1274         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1275             Log.w(TAG, "Time shifting is not supported in this platform.");
1276         } else if (!isTimeShiftAvailable()) {
1277             throw new IllegalStateException("Time-shift is not supported for the current channel");
1278         } else {
1279             if (speed <= 0) {
1280                 throw new IllegalArgumentException("The speed should be a positive integer.");
1281             }
1282             mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD;
1283             PlaybackParams params = new PlaybackParams();
1284             params.setSpeed(speed);
1285             mTvView.timeShiftSetPlaybackParams(params);
1286         }
1287     }
1288 
1289     /**
1290      * Seek to the given time position.
1291      *
1292      * @param timeMs The time in milliseconds to seek to.
1293      */
timeshiftSeekTo(long timeMs)1294     public void timeshiftSeekTo(long timeMs) {
1295         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1296             Log.w(TAG, "Time shifting is not supported in this platform.");
1297             return;
1298         }
1299         if (!isTimeShiftAvailable()) {
1300             throw new IllegalStateException("Time-shift is not supported for the current channel");
1301         }
1302         mTvView.timeShiftSeekTo(timeMs);
1303     }
1304 
1305     /**
1306      * Returns the current playback position in milliseconds.
1307      */
timeshiftGetCurrentPositionMs()1308     public long timeshiftGetCurrentPositionMs() {
1309         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1310             Log.w(TAG, "Time shifting is not supported in this platform.");
1311             return INVALID_TIME;
1312         }
1313         if (!isTimeShiftAvailable()) {
1314             throw new IllegalStateException("Time-shift is not supported for the current channel");
1315         }
1316         if (DEBUG) {
1317             Log.d(TAG, "timeshiftGetCurrentPositionMs: current position ="
1318                     + Utils.toTimeString(mTimeShiftCurrentPositionMs));
1319         }
1320         return mTimeShiftCurrentPositionMs;
1321     }
1322 
1323     /**
1324      * Used to receive the time-shift events.
1325      */
1326     public static abstract class TimeShiftListener {
1327         /**
1328          * Called when the availability of the time-shift for the current channel has been changed.
1329          * It should be guaranteed that this is called only when the availability is really changed.
1330          */
onAvailabilityChanged()1331         public abstract void onAvailabilityChanged();
1332 
1333         /**
1334          * Called when the record start time has been changed.
1335          */
onRecordStartTimeChanged(long recordStartTimeMs)1336         public abstract void onRecordStartTimeChanged(long recordStartTimeMs);
1337     }
1338 
1339     /**
1340      * A listener which receives the notification when the screen is blocked/unblocked.
1341      */
1342     public static abstract class OnScreenBlockingChangedListener {
1343         /**
1344          * Called when the screen is blocked/unblocked.
1345          */
onScreenBlockingChanged(boolean blocked)1346         public abstract void onScreenBlockingChanged(boolean blocked);
1347     }
1348 
1349     public class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
1350         @Override
doInBackground(Void... params)1351         protected Boolean doInBackground(Void... params) {
1352             return NetworkUtils.isNetworkAvailable(mConnectivityManager);
1353         }
1354 
1355         @Override
onPostExecute(Boolean networkAvailable)1356         protected void onPostExecute(Boolean networkAvailable) {
1357             mInternetCheckTask = null;
1358             if (!mVideoAvailable && !networkAvailable && isAttachedToWindow()
1359                     && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
1360                 mHideScreenView.setImageVisibility(true);
1361                 mHideScreenView.setImage(R.drawable.ic_sad_cloud);
1362                 mHideScreenView.setText(R.string.tvview_msg_no_internet_connection);
1363             }
1364         }
1365     }
1366 }
1367