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