TunableTvView.java revision 919e1ed7e914029a1a0054237d86dc7b19ced898
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
17package com.android.tv.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.TimeInterpolator;
22import android.annotation.SuppressLint;
23import android.content.Context;
24import android.content.pm.PackageManager;
25import android.media.PlaybackParams;
26import android.media.tv.TvContentRating;
27import android.media.tv.TvInputInfo;
28import android.media.tv.TvInputManager;
29import android.media.tv.TvTrackInfo;
30import android.media.tv.TvView;
31import android.media.tv.TvView.OnUnhandledInputEventListener;
32import android.media.tv.TvView.TvInputCallback;
33import android.net.ConnectivityManager;
34import android.net.Uri;
35import android.os.AsyncTask;
36import android.os.Bundle;
37import android.support.annotation.IntDef;
38import android.support.annotation.NonNull;
39import android.support.annotation.Nullable;
40import android.support.annotation.UiThread;
41import android.support.v4.os.BuildCompat;
42import android.text.TextUtils;
43import android.text.format.DateUtils;
44import android.util.AttributeSet;
45import android.util.Log;
46import android.view.KeyEvent;
47import android.view.MotionEvent;
48import android.view.SurfaceView;
49import android.view.View;
50import android.view.ViewGroup;
51import android.widget.FrameLayout;
52import android.widget.ImageView;
53
54import com.android.tv.ApplicationSingletons;
55import com.android.tv.InputSessionManager;
56import com.android.tv.InputSessionManager.TvViewSession;
57import com.android.tv.R;
58import com.android.tv.TvApplication;
59import com.android.tv.analytics.DurationTimer;
60import com.android.tv.analytics.Tracker;
61import com.android.tv.common.feature.CommonFeatures;
62import com.android.tv.data.Channel;
63import com.android.tv.data.StreamInfo;
64import com.android.tv.data.WatchedHistoryManager;
65import com.android.tv.parental.ContentRatingsManager;
66import com.android.tv.recommendation.NotificationService;
67import com.android.tv.util.NetworkUtils;
68import com.android.tv.util.PermissionUtils;
69import com.android.tv.util.TvInputManagerHelper;
70import com.android.tv.util.Utils;
71
72import java.lang.annotation.Retention;
73import java.lang.annotation.RetentionPolicy;
74import java.util.List;
75
76public 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
335    public TunableTvView(Context context) {
336        this(context, null);
337    }
338
339    public TunableTvView(Context context, AttributeSet attrs) {
340        this(context, attrs, 0);
341    }
342
343    public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) {
344        this(context, attrs, defStyleAttr, 0);
345    }
346
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
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
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     */
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
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     */
457    public void release() {
458        if (mInputSessionManager != null) {
459            mInputSessionManager.releaseTvViewSession(mTvViewSession);
460            mTvViewSession = null;
461        }
462    }
463
464    /**
465     * Reset TV view.
466     */
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     */
475    public void resetByRecording() {
476        resetInternal();
477    }
478
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
492    public void setMain() {
493        mTvView.setMain();
494    }
495
496    public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
497        mWatchedHistoryManager = watchedHistoryManager;
498    }
499
500    public boolean isPlaying() {
501        return mStarted;
502    }
503
504    /**
505     * Called when parental control is changed.
506     */
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     */
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
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     */
602    public void setCurrentChannel(Channel currentChannel) {
603        mCurrentChannel = currentChannel;
604    }
605
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     */
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
642    public boolean dispatchKeyEvent(KeyEvent event) {
643        return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event);
644    }
645
646    @Override
647    public boolean dispatchTouchEvent(MotionEvent event) {
648        return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event);
649    }
650
651    @Override
652    public boolean dispatchTrackballEvent(MotionEvent event) {
653        return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event);
654    }
655
656    @Override
657    public boolean dispatchGenericMotionEvent(MotionEvent event) {
658        return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event);
659    }
660
661    public interface OnTuneListener {
662        void onTuneFailed(Channel channel);
663        void onUnexpectedStop(Channel channel);
664        void onStreamInfoChanged(StreamInfo info);
665        void onChannelRetuned(Uri channel);
666        void onContentBlocked();
667        void onContentAllowed();
668    }
669
670    public void unblockContent(TvContentRating rating) {
671        mTvView.unblockContent(rating);
672    }
673
674    @Override
675    public int getVideoWidth() {
676        return mVideoWidth;
677    }
678
679    @Override
680    public int getVideoHeight() {
681        return mVideoHeight;
682    }
683
684    @Override
685    public int getVideoDefinitionLevel() {
686        return mVideoFormat;
687    }
688
689    @Override
690    public float getVideoFrameRate() {
691        return mVideoFrameRate;
692    }
693
694    /**
695     * Returns displayed aspect ratio (video width / video height * pixel ratio).
696     */
697    @Override
698    public float getVideoDisplayAspectRatio() {
699        return mVideoDisplayAspectRatio;
700    }
701
702    @Override
703    public int getAudioChannelCount() {
704        return mAudioChannelCount;
705    }
706
707    @Override
708    public boolean hasClosedCaption() {
709        return mHasClosedCaption;
710    }
711
712    @Override
713    public boolean isVideoAvailable() {
714        return mVideoAvailable;
715    }
716
717    @Override
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     */
725    private SurfaceView getSurfaceView() {
726        return (SurfaceView) mTvView.getChildAt(0);
727    }
728
729    public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
730        mTvView.setOnUnhandledInputEventListener(listener);
731    }
732
733    public void setClosedCaptionEnabled(boolean enabled) {
734        mTvView.setCaptionEnabled(enabled);
735    }
736
737    public List<TvTrackInfo> getTracks(int type) {
738        return mTvView.getTracks(type);
739    }
740
741    public String getSelectedTrack(int type) {
742        return mTvView.getSelectedTrack(type);
743    }
744
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     */
752    public boolean isScreenBlocked() {
753        return mScreenBlocked;
754    }
755
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
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     */
774    public void blockScreen() {
775        mScreenBlocked = true;
776        checkBlockScreenAndMuteNeeded();
777        if (mOnScreenBlockedListener != null) {
778            mOnScreenBlockedListener.onScreenBlockingChanged(true);
779        }
780    }
781
782    private void blockScreenByContentRating(TvContentRating rating) {
783        mBlockedContentRating = rating;
784        checkBlockScreenAndMuteNeeded();
785    }
786
787    @Override
788    @SuppressLint("RtlHardcoded")
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
805    public void setLayoutParams(ViewGroup.LayoutParams params) {
806        super.setLayoutParams(params);
807        if (mTvView != null) {
808            copyLayoutParamsToTvView();
809        }
810    }
811
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
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     */
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
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
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     */
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
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
965    public void unblockScreen() {
966        mScreenBlocked = false;
967        checkBlockScreenAndMuteNeeded();
968        if (mOnScreenBlockedListener != null) {
969            mOnScreenBlockedListener.onScreenBlockingChanged(false);
970        }
971    }
972
973    private void unblockScreenByContentRating() {
974        mBlockedContentRating = null;
975        checkBlockScreenAndMuteNeeded();
976    }
977
978    @UiThread
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
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
1053    private void unhideScreenByVideoAvailability() {
1054        mVideoAvailable = true;
1055        mHideScreenView.setVisibility(GONE);
1056        mBufferingSpinnerView.setVisibility(GONE);
1057        unmuteIfPossible();
1058    }
1059
1060    private void unmuteIfPossible() {
1061        if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) {
1062            unmute();
1063        }
1064    }
1065
1066    private void mute() {
1067        mIsMuted = true;
1068        mTvView.setStreamVolume(0);
1069    }
1070
1071    private void unmute() {
1072        mIsMuted = false;
1073        mTvView.setStreamVolume(mVolume);
1074    }
1075
1076    /** Returns true if this view is faded out. */
1077    public boolean isFadedOut() {
1078        return mFadeState == FADED_OUT;
1079    }
1080
1081    /** Fade out this TunableTvView. Fade out by increasing the dimming. */
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. */
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. */
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     */
1142    public void setTimeShiftListener(TimeShiftListener listener) {
1143        mTimeShiftListener = listener;
1144    }
1145
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     */
1177    public boolean isTimeShiftAvailable() {
1178        return mTimeShiftAvailable;
1179    }
1180
1181    /**
1182     * Plays the media, if the current input supports time-shifting.
1183     */
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     */
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     */
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     */
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     */
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     */
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         */
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         */
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         */
1295        public abstract void onScreenBlockingChanged(boolean blocked);
1296    }
1297
1298    private class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
1299        @Override
1300        protected Boolean doInBackground(Void... params) {
1301            return NetworkUtils.isNetworkAvailable(mConnectivityManager);
1302        }
1303
1304        @Override
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