1/*
2 * Copyright (C) 2014 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 android.media.tv;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.SystemApi;
22import android.content.Context;
23import android.graphics.Canvas;
24import android.graphics.PorterDuff;
25import android.graphics.Rect;
26import android.graphics.Region;
27import android.media.PlaybackParams;
28import android.media.tv.TvInputManager.Session;
29import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
30import android.media.tv.TvInputManager.SessionCallback;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.Pair;
38import android.view.InputEvent;
39import android.view.KeyEvent;
40import android.view.MotionEvent;
41import android.view.Surface;
42import android.view.SurfaceHolder;
43import android.view.SurfaceView;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ViewRootImpl;
47
48import java.lang.ref.WeakReference;
49import java.util.ArrayDeque;
50import java.util.List;
51import java.util.Queue;
52
53/**
54 * Displays TV contents. The TvView class provides a high level interface for applications to show
55 * TV programs from various TV sources that implement {@link TvInputService}. (Note that the list of
56 * TV inputs available on the system can be obtained by calling
57 * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.)
58 *
59 * <p>Once the application supplies the URI for a specific TV channel to {@link #tune(String, Uri)}
60 * method, it takes care of underlying service binding (and unbinding if the current TvView is
61 * already bound to a service) and automatically allocates/deallocates resources needed. In addition
62 * to a few essential methods to control how the contents are presented, it also provides a way to
63 * dispatch input events to the connected TvInputService in order to enable custom key actions for
64 * the TV input.
65 */
66public class TvView extends ViewGroup {
67    private static final String TAG = "TvView";
68    private static final boolean DEBUG = false;
69
70    private static final int ZORDER_MEDIA = 0;
71    private static final int ZORDER_MEDIA_OVERLAY = 1;
72    private static final int ZORDER_ON_TOP = 2;
73
74    private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null);
75
76    private static final Object sMainTvViewLock = new Object();
77    private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW;
78
79    private final Handler mHandler = new Handler();
80    private Session mSession;
81    private SurfaceView mSurfaceView;
82    private Surface mSurface;
83    private boolean mOverlayViewCreated;
84    private Rect mOverlayViewFrame;
85    private final TvInputManager mTvInputManager;
86    private MySessionCallback mSessionCallback;
87    private TvInputCallback mCallback;
88    private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
89    private Float mStreamVolume;
90    private Boolean mCaptionEnabled;
91    private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>();
92
93    private boolean mSurfaceChanged;
94    private int mSurfaceFormat;
95    private int mSurfaceWidth;
96    private int mSurfaceHeight;
97    private final AttributeSet mAttrs;
98    private final int mDefStyleAttr;
99    private int mWindowZOrder;
100    private boolean mUseRequestedSurfaceLayout;
101    private int mSurfaceViewLeft;
102    private int mSurfaceViewRight;
103    private int mSurfaceViewTop;
104    private int mSurfaceViewBottom;
105    private TimeShiftPositionCallback mTimeShiftPositionCallback;
106
107    private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
108        @Override
109        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
110            if (DEBUG) {
111                Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width="
112                    + width + ", height=" + height + ")");
113            }
114            mSurfaceFormat = format;
115            mSurfaceWidth = width;
116            mSurfaceHeight = height;
117            mSurfaceChanged = true;
118            dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
119        }
120
121        @Override
122        public void surfaceCreated(SurfaceHolder holder) {
123            mSurface = holder.getSurface();
124            setSessionSurface(mSurface);
125        }
126
127        @Override
128        public void surfaceDestroyed(SurfaceHolder holder) {
129            mSurface = null;
130            mSurfaceChanged = false;
131            setSessionSurface(null);
132        }
133    };
134
135    private final FinishedInputEventCallback mFinishedInputEventCallback =
136            new FinishedInputEventCallback() {
137        @Override
138        public void onFinishedInputEvent(Object token, boolean handled) {
139            if (DEBUG) {
140                Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")");
141            }
142            if (handled) {
143                return;
144            }
145            // TODO: Re-order unhandled events.
146            InputEvent event = (InputEvent) token;
147            if (dispatchUnhandledInputEvent(event)) {
148                return;
149            }
150            ViewRootImpl viewRootImpl = getViewRootImpl();
151            if (viewRootImpl != null) {
152                viewRootImpl.dispatchUnhandledInputEvent(event);
153            }
154        }
155    };
156
157    public TvView(Context context) {
158        this(context, null, 0);
159    }
160
161    public TvView(Context context, AttributeSet attrs) {
162        this(context, attrs, 0);
163    }
164
165    public TvView(Context context, AttributeSet attrs, int defStyleAttr) {
166        super(context, attrs, defStyleAttr);
167        mAttrs = attrs;
168        mDefStyleAttr = defStyleAttr;
169        resetSurfaceView();
170        mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE);
171    }
172
173    /**
174     * Sets the callback to be invoked when an event is dispatched to this TvView.
175     *
176     * @param callback The callback to receive events. A value of {@code null} removes the existing
177     *            callback.
178     */
179    public void setCallback(@Nullable TvInputCallback callback) {
180        mCallback = callback;
181    }
182
183    /**
184     * Sets this as the main {@link TvView}.
185     *
186     * <p>The main {@link TvView} is a {@link TvView} whose corresponding TV input determines the
187     * HDMI-CEC active source device. For an HDMI port input, one of source devices that is
188     * connected to that HDMI port becomes the active source. For an HDMI-CEC logical device input,
189     * the corresponding HDMI-CEC logical device becomes the active source. For any non-HDMI input
190     * (including the tuner, composite, S-Video, etc.), the internal device (= TV itself) becomes
191     * the active source.
192     *
193     * <p>First tuned {@link TvView} becomes main automatically, and keeps to be main until either
194     * {@link #reset} is called for the main {@link TvView} or {@code setMain()} is called for other
195     * {@link TvView}.
196     * @hide
197     */
198    @SystemApi
199    public void setMain() {
200        synchronized (sMainTvViewLock) {
201            sMainTvView = new WeakReference<>(this);
202            if (hasWindowFocus() && mSession != null) {
203                mSession.setMain();
204            }
205        }
206    }
207
208    /**
209     * Sets the Z order of a window owning the surface of this TvView above the normal TvView
210     * but below an application.
211     *
212     * @see SurfaceView#setZOrderMediaOverlay
213     * @hide
214     */
215    @SystemApi
216    public void setZOrderMediaOverlay(boolean isMediaOverlay) {
217        if (isMediaOverlay) {
218            mWindowZOrder = ZORDER_MEDIA_OVERLAY;
219            removeSessionOverlayView();
220        } else {
221            mWindowZOrder = ZORDER_MEDIA;
222            createSessionOverlayView();
223        }
224        if (mSurfaceView != null) {
225            // ZOrderOnTop(false) removes WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
226            // from WindowLayoutParam as well as changes window type.
227            mSurfaceView.setZOrderOnTop(false);
228            mSurfaceView.setZOrderMediaOverlay(isMediaOverlay);
229        }
230    }
231
232    /**
233     * Sets the Z order of a window owning the surface of this TvView on top of an application.
234     *
235     * @see SurfaceView#setZOrderOnTop
236     * @hide
237     */
238    @SystemApi
239    public void setZOrderOnTop(boolean onTop) {
240        if (onTop) {
241            mWindowZOrder = ZORDER_ON_TOP;
242            removeSessionOverlayView();
243        } else {
244            mWindowZOrder = ZORDER_MEDIA;
245            createSessionOverlayView();
246        }
247        if (mSurfaceView != null) {
248            mSurfaceView.setZOrderMediaOverlay(false);
249            mSurfaceView.setZOrderOnTop(onTop);
250        }
251     }
252
253    /**
254     * Sets the relative stream volume of this TvView.
255     *
256     * <p>This method is primarily used to handle audio focus changes or mute a specific TvView when
257     * multiple views are displayed. If the method has not yet been called, the TvView assumes the
258     * default value of {@code 1.0f}.
259     *
260     * @param volume A volume value between {@code 0.0f} to {@code 1.0f}.
261     */
262    public void setStreamVolume(float volume) {
263        if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")");
264        mStreamVolume = volume;
265        if (mSession == null) {
266            // Volume will be set once the connection has been made.
267            return;
268        }
269        mSession.setStreamVolume(volume);
270    }
271
272    /**
273     * Tunes to a given channel.
274     *
275     * @param inputId The ID of the TV input for the given channel.
276     * @param channelUri The URI of a channel.
277     */
278    public void tune(@NonNull String inputId, Uri channelUri) {
279        tune(inputId, channelUri, null);
280    }
281
282    /**
283     * Tunes to a given channel.
284     *
285     * @param inputId The ID of TV input for the given channel.
286     * @param channelUri The URI of a channel.
287     * @param params Extra parameters.
288     * @hide
289     */
290    @SystemApi
291    public void tune(String inputId, Uri channelUri, Bundle params) {
292        if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")");
293        if (TextUtils.isEmpty(inputId)) {
294            throw new IllegalArgumentException("inputId cannot be null or an empty string");
295        }
296        synchronized (sMainTvViewLock) {
297            if (sMainTvView.get() == null) {
298                sMainTvView = new WeakReference<>(this);
299            }
300        }
301        if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) {
302            if (mSession != null) {
303                mSession.tune(channelUri, params);
304            } else {
305                // createSession() was called but the actual session for the given inputId has not
306                // yet been created. Just replace the existing tuning params in the callback with
307                // the new ones and tune later in onSessionCreated(). It is not necessary to create
308                // a new callback because this tuning request was made on the same inputId.
309                mSessionCallback.mChannelUri = channelUri;
310                mSessionCallback.mTuneParams = params;
311            }
312        } else {
313            resetInternal();
314            // In case createSession() is called multiple times across different inputId's before
315            // any session is created (e.g. when quickly tuning to a channel from input A and then
316            // to another channel from input B), only the callback for the last createSession()
317            // should be invoked. (The previous callbacks are simply ignored.) To do that, we create
318            // a new callback each time and keep mSessionCallback pointing to the last one. If
319            // MySessionCallback.this is different from mSessionCallback, we know that this callback
320            // is obsolete and should ignore it.
321            mSessionCallback = new MySessionCallback(inputId, channelUri, params);
322            if (mTvInputManager != null) {
323                mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
324            }
325        }
326    }
327
328    /**
329     * Resets this TvView.
330     *
331     * <p>This method is primarily used to un-tune the current TvView.
332     */
333    public void reset() {
334        if (DEBUG) Log.d(TAG, "reset()");
335        synchronized (sMainTvViewLock) {
336            if (this == sMainTvView.get()) {
337                sMainTvView = NULL_TV_VIEW;
338            }
339        }
340        resetInternal();
341    }
342
343    private void resetInternal() {
344        mSessionCallback = null;
345        mPendingAppPrivateCommands.clear();
346        if (mSession != null) {
347            setSessionSurface(null);
348            removeSessionOverlayView();
349            mUseRequestedSurfaceLayout = false;
350            mSession.release();
351            mSession = null;
352            resetSurfaceView();
353        }
354    }
355
356    /**
357     * Requests to unblock TV content according to the given rating.
358     *
359     * <p>This notifies TV input that blocked content is now OK to play.
360     *
361     * @param unblockedRating A TvContentRating to unblock.
362     * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
363     * @hide
364     * @deprecated Use {@link #unblockContent} instead.
365     */
366    @Deprecated
367    @SystemApi
368    public void requestUnblockContent(TvContentRating unblockedRating) {
369        unblockContent(unblockedRating);
370    }
371
372    /**
373     * Requests to unblock TV content according to the given rating.
374     *
375     * <p>This notifies TV input that blocked content is now OK to play.
376     *
377     * @param unblockedRating A TvContentRating to unblock.
378     * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
379     * @hide
380     */
381    @SystemApi
382    public void unblockContent(TvContentRating unblockedRating) {
383        if (mSession != null) {
384            mSession.unblockContent(unblockedRating);
385        }
386    }
387
388    /**
389     * Enables or disables the caption in this TvView.
390     *
391     * <p>Note that this method does not take any effect unless the current TvView is tuned.
392     *
393     * @param enabled {@code true} to enable, {@code false} to disable.
394     */
395    public void setCaptionEnabled(boolean enabled) {
396        if (DEBUG) Log.d(TAG, "setCaptionEnabled(" + enabled + ")");
397        mCaptionEnabled = enabled;
398        if (mSession != null) {
399            mSession.setCaptionEnabled(enabled);
400        }
401    }
402
403    /**
404     * Selects a track.
405     *
406     * @param type The type of the track to select. The type can be {@link TvTrackInfo#TYPE_AUDIO},
407     *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
408     * @param trackId The ID of the track to select. {@code null} means to unselect the current
409     *            track for a given type.
410     * @see #getTracks
411     * @see #getSelectedTrack
412     */
413    public void selectTrack(int type, String trackId) {
414        if (mSession != null) {
415            mSession.selectTrack(type, trackId);
416        }
417    }
418
419    /**
420     * Returns the list of tracks. Returns {@code null} if the information is not available.
421     *
422     * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
423     *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
424     * @see #selectTrack
425     * @see #getSelectedTrack
426     */
427    public List<TvTrackInfo> getTracks(int type) {
428        if (mSession == null) {
429            return null;
430        }
431        return mSession.getTracks(type);
432    }
433
434    /**
435     * Returns the ID of the selected track for a given type. Returns {@code null} if the
436     * information is not available or the track is not selected.
437     *
438     * @param type The type of the selected tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
439     *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
440     * @see #selectTrack
441     * @see #getTracks
442     */
443    public String getSelectedTrack(int type) {
444        if (mSession == null) {
445            return null;
446        }
447        return mSession.getSelectedTrack(type);
448    }
449
450    /**
451     * Pauses playback. No-op if it is already paused. Call {@link #timeShiftResume} to resume.
452     */
453    public void timeShiftPause() {
454        if (mSession != null) {
455            mSession.timeShiftPause();
456        }
457    }
458
459    /**
460     * Resumes playback. No-op if it is already resumed. Call {@link #timeShiftPause} to pause.
461     */
462    public void timeShiftResume() {
463        if (mSession != null) {
464            mSession.timeShiftResume();
465        }
466    }
467
468    /**
469     * Seeks to a specified time position. {@code timeMs} must be equal to or greater than the start
470     * position returned by {@link TimeShiftPositionCallback#onTimeShiftStartPositionChanged} and
471     * equal to or less than the current time.
472     *
473     * @param timeMs The time position to seek to, in milliseconds since the epoch.
474     */
475    public void timeShiftSeekTo(long timeMs) {
476        if (mSession != null) {
477            mSession.timeShiftSeekTo(timeMs);
478        }
479    }
480
481    /**
482     * Sets playback rate using {@link android.media.PlaybackParams}.
483     *
484     * @param params The playback params.
485     */
486    public void timeShiftSetPlaybackParams(@NonNull PlaybackParams params) {
487        if (mSession != null) {
488            mSession.timeShiftSetPlaybackParams(params);
489        }
490    }
491
492    /**
493     * Sets the callback to be invoked when the time shift position is changed.
494     *
495     * @param callback The callback to receive time shift position changes. A value of {@code null}
496     *            removes the existing callback.
497     */
498    public void setTimeShiftPositionCallback(@Nullable TimeShiftPositionCallback callback) {
499        mTimeShiftPositionCallback = callback;
500        ensurePositionTracking();
501    }
502
503    private void ensurePositionTracking() {
504        if (mSession == null) {
505            return;
506        }
507        mSession.timeShiftEnablePositionTracking(mTimeShiftPositionCallback != null);
508    }
509
510    /**
511     * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
512     * TvInputService.Session.appPrivateCommand()} on the current TvView.
513     *
514     * @param action The name of the private command to send. This <em>must</em> be a scoped name,
515     *            i.e. prefixed with a package name you own, so that different developers will not
516     *            create conflicting commands.
517     * @param data An optional bundle to send with the command.
518     * @hide
519     */
520    @SystemApi
521    public void sendAppPrivateCommand(@NonNull String action, Bundle data) {
522        if (TextUtils.isEmpty(action)) {
523            throw new IllegalArgumentException("action cannot be null or an empty string");
524        }
525        if (mSession != null) {
526            mSession.sendAppPrivateCommand(action, data);
527        } else {
528            Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action
529                    + "\" pending)");
530            mPendingAppPrivateCommands.add(Pair.create(action, data));
531        }
532    }
533
534    /**
535     * Dispatches an unhandled input event to the next receiver.
536     *
537     * <p>Except system keys, TvView always consumes input events in the normal flow. This is called
538     * asynchronously from where the event is dispatched. It gives the host application a chance to
539     * dispatch the unhandled input events.
540     *
541     * @param event The input event.
542     * @return {@code true} if the event was handled by the view, {@code false} otherwise.
543     */
544    public boolean dispatchUnhandledInputEvent(InputEvent event) {
545        if (mOnUnhandledInputEventListener != null) {
546            if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
547                return true;
548            }
549        }
550        return onUnhandledInputEvent(event);
551    }
552
553    /**
554     * Called when an unhandled input event also has not been handled by the user provided
555     * callback. This is the last chance to handle the unhandled input event in the TvView.
556     *
557     * @param event The input event.
558     * @return If you handled the event, return {@code true}. If you want to allow the event to be
559     *         handled by the next receiver, return {@code false}.
560     */
561    public boolean onUnhandledInputEvent(InputEvent event) {
562        return false;
563    }
564
565    /**
566     * Registers a callback to be invoked when an input event is not handled by the bound TV input.
567     *
568     * @param listener The callback to be invoked when the unhandled input event is received.
569     */
570    public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
571        mOnUnhandledInputEventListener = listener;
572    }
573
574    @Override
575    public boolean dispatchKeyEvent(KeyEvent event) {
576        if (super.dispatchKeyEvent(event)) {
577            return true;
578        }
579        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
580        if (mSession == null) {
581            return false;
582        }
583        InputEvent copiedEvent = event.copy();
584        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
585                mHandler);
586        return ret != Session.DISPATCH_NOT_HANDLED;
587    }
588
589    @Override
590    public boolean dispatchTouchEvent(MotionEvent event) {
591        if (super.dispatchTouchEvent(event)) {
592            return true;
593        }
594        if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
595        if (mSession == null) {
596            return false;
597        }
598        InputEvent copiedEvent = event.copy();
599        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
600                mHandler);
601        return ret != Session.DISPATCH_NOT_HANDLED;
602    }
603
604    @Override
605    public boolean dispatchTrackballEvent(MotionEvent event) {
606        if (super.dispatchTrackballEvent(event)) {
607            return true;
608        }
609        if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
610        if (mSession == null) {
611            return false;
612        }
613        InputEvent copiedEvent = event.copy();
614        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
615                mHandler);
616        return ret != Session.DISPATCH_NOT_HANDLED;
617    }
618
619    @Override
620    public boolean dispatchGenericMotionEvent(MotionEvent event) {
621        if (super.dispatchGenericMotionEvent(event)) {
622            return true;
623        }
624        if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
625        if (mSession == null) {
626            return false;
627        }
628        InputEvent copiedEvent = event.copy();
629        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
630                mHandler);
631        return ret != Session.DISPATCH_NOT_HANDLED;
632    }
633
634    @Override
635    public void dispatchWindowFocusChanged(boolean hasFocus) {
636        super.dispatchWindowFocusChanged(hasFocus);
637        // Other app may have shown its own main TvView.
638        // Set main again to regain main session.
639        synchronized (sMainTvViewLock) {
640            if (hasFocus && this == sMainTvView.get() && mSession != null) {
641                mSession.setMain();
642            }
643        }
644    }
645
646    @Override
647    protected void onAttachedToWindow() {
648        super.onAttachedToWindow();
649        createSessionOverlayView();
650    }
651
652    @Override
653    protected void onDetachedFromWindow() {
654        removeSessionOverlayView();
655        super.onDetachedFromWindow();
656    }
657
658    @Override
659    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
660        if (DEBUG) {
661            Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
662                    + ", bottom=" + bottom + ",)");
663        }
664        if (mUseRequestedSurfaceLayout) {
665            mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
666                    mSurfaceViewBottom);
667        } else {
668            mSurfaceView.layout(0, 0, right - left, bottom - top);
669        }
670    }
671
672    @Override
673    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
674        mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
675        int width = mSurfaceView.getMeasuredWidth();
676        int height = mSurfaceView.getMeasuredHeight();
677        int childState = mSurfaceView.getMeasuredState();
678        setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
679                resolveSizeAndState(height, heightMeasureSpec,
680                        childState << MEASURED_HEIGHT_STATE_SHIFT));
681    }
682
683    @Override
684    public boolean gatherTransparentRegion(Region region) {
685        if (mWindowZOrder != ZORDER_ON_TOP) {
686            if (region != null) {
687                int width = getWidth();
688                int height = getHeight();
689                if (width > 0 && height > 0) {
690                    int location[] = new int[2];
691                    getLocationInWindow(location);
692                    int left = location[0];
693                    int top = location[1];
694                    region.op(left, top, left + width, top + height, Region.Op.UNION);
695                }
696            }
697        }
698        return super.gatherTransparentRegion(region);
699    }
700
701    @Override
702    public void draw(Canvas canvas) {
703        if (mWindowZOrder != ZORDER_ON_TOP) {
704            // Punch a hole so that the underlying overlay view and surface can be shown.
705            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
706        }
707        super.draw(canvas);
708    }
709
710    @Override
711    protected void dispatchDraw(Canvas canvas) {
712        if (mWindowZOrder != ZORDER_ON_TOP) {
713            // Punch a hole so that the underlying overlay view and surface can be shown.
714            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
715        }
716        super.dispatchDraw(canvas);
717    }
718
719    @Override
720    protected void onVisibilityChanged(View changedView, int visibility) {
721        super.onVisibilityChanged(changedView, visibility);
722        mSurfaceView.setVisibility(visibility);
723        if (visibility == View.VISIBLE) {
724            createSessionOverlayView();
725        } else {
726            removeSessionOverlayView();
727        }
728    }
729
730    private void resetSurfaceView() {
731        if (mSurfaceView != null) {
732            mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
733            removeView(mSurfaceView);
734        }
735        mSurface = null;
736        mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) {
737            @Override
738            protected void updateWindow(boolean force, boolean redrawNeeded) {
739                super.updateWindow(force, redrawNeeded);
740                relayoutSessionOverlayView();
741            }};
742        mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
743        if (mWindowZOrder == ZORDER_MEDIA_OVERLAY) {
744            mSurfaceView.setZOrderMediaOverlay(true);
745        } else if (mWindowZOrder == ZORDER_ON_TOP) {
746            mSurfaceView.setZOrderOnTop(true);
747        }
748        addView(mSurfaceView);
749    }
750
751    private void setSessionSurface(Surface surface) {
752        if (mSession == null) {
753            return;
754        }
755        mSession.setSurface(surface);
756    }
757
758    private void dispatchSurfaceChanged(int format, int width, int height) {
759        if (mSession == null) {
760            return;
761        }
762        mSession.dispatchSurfaceChanged(format, width, height);
763    }
764
765    private void createSessionOverlayView() {
766        if (mSession == null || !isAttachedToWindow()
767                || mOverlayViewCreated || mWindowZOrder != ZORDER_MEDIA) {
768            return;
769        }
770        mOverlayViewFrame = getViewFrameOnScreen();
771        mSession.createOverlayView(this, mOverlayViewFrame);
772        mOverlayViewCreated = true;
773    }
774
775    private void removeSessionOverlayView() {
776        if (mSession == null || !mOverlayViewCreated) {
777            return;
778        }
779        mSession.removeOverlayView();
780        mOverlayViewCreated = false;
781        mOverlayViewFrame = null;
782    }
783
784    private void relayoutSessionOverlayView() {
785        if (mSession == null || !isAttachedToWindow() || !mOverlayViewCreated
786                || mWindowZOrder != ZORDER_MEDIA) {
787            return;
788        }
789        Rect viewFrame = getViewFrameOnScreen();
790        if (viewFrame.equals(mOverlayViewFrame)) {
791            return;
792        }
793        mSession.relayoutOverlayView(viewFrame);
794        mOverlayViewFrame = viewFrame;
795    }
796
797    private Rect getViewFrameOnScreen() {
798        int[] location = new int[2];
799        getLocationOnScreen(location);
800        return new Rect(location[0], location[1],
801                location[0] + getWidth(), location[1] + getHeight());
802    }
803
804    /**
805     * Callback used to receive time shift position changes.
806     */
807    public abstract static class TimeShiftPositionCallback {
808
809        /**
810         * This is called when the start playback position is changed.
811         *
812         * <p>The start playback position of the time shifted program can be adjusted by the TV
813         * input when it cannot retain the whole recorded program due to some reason (e.g.
814         * limitation on storage space). The application should not allow the user to seek to a
815         * position earlier than the start position.
816         *
817         * <p>Note that {@code timeMs} is not relative time in the program but wall-clock time,
818         * which is intended to avoid calling this method unnecessarily around program boundaries.
819         *
820         * @param inputId The ID of the TV input bound to this view.
821         * @param timeMs The start playback position of the time shifted program, in milliseconds
822         *            since the epoch.
823         */
824        public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
825        }
826
827        /**
828         * This is called when the current playback position is changed.
829         *
830         * <p>Note that {@code timeMs} is not relative time in the program but wall-clock time,
831         * which is intended to avoid calling this method unnecessarily around program boundaries.
832         *
833         * @param inputId The ID of the TV input bound to this view.
834         * @param timeMs The current playback position of the time shifted program, in milliseconds
835         *            since the epoch.
836         */
837        public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
838        }
839    }
840
841    /**
842     * Callback used to receive various status updates on the {@link TvView}.
843     */
844    public abstract static class TvInputCallback {
845
846        /**
847         * This is invoked when an error occurred while establishing a connection to the underlying
848         * TV input.
849         *
850         * @param inputId The ID of the TV input bound to this view.
851         */
852        public void onConnectionFailed(String inputId) {
853        }
854
855        /**
856         * This is invoked when the existing connection to the underlying TV input is lost.
857         *
858         * @param inputId The ID of the TV input bound to this view.
859         */
860        public void onDisconnected(String inputId) {
861        }
862
863        /**
864         * This is invoked when the channel of this TvView is changed by the underlying TV input
865         * without any {@link TvView#tune(String, Uri)} request.
866         *
867         * @param inputId The ID of the TV input bound to this view.
868         * @param channelUri The URI of a channel.
869         */
870        public void onChannelRetuned(String inputId, Uri channelUri) {
871        }
872
873        /**
874         * This is called when the track information has been changed.
875         *
876         * @param inputId The ID of the TV input bound to this view.
877         * @param tracks A list which includes track information.
878         */
879        public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
880        }
881
882        /**
883         * This is called when there is a change on the selected tracks.
884         *
885         * @param inputId The ID of the TV input bound to this view.
886         * @param type The type of the track selected. The type can be
887         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
888         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
889         * @param trackId The ID of the track selected.
890         */
891        public void onTrackSelected(String inputId, int type, String trackId) {
892        }
893
894        /**
895         * This is invoked when the video size has been changed. It is also called when the first
896         * time video size information becomes available after this view is tuned to a specific
897         * channel.
898         *
899         * @param inputId The ID of the TV input bound to this view.
900         * @param width The width of the video.
901         * @param height The height of the video.
902         */
903        public void onVideoSizeChanged(String inputId, int width, int height) {
904        }
905
906        /**
907         * This is called when the video is available, so the TV input starts the playback.
908         *
909         * @param inputId The ID of the TV input bound to this view.
910         */
911        public void onVideoAvailable(String inputId) {
912        }
913
914        /**
915         * This is called when the video is not available, so the TV input stops the playback.
916         *
917         * @param inputId The ID of the TV input bound to this view.
918         * @param reason The reason why the TV input stopped the playback:
919         * <ul>
920         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
921         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
922         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
923         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
924         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY}
925         * </ul>
926         */
927        public void onVideoUnavailable(String inputId, int reason) {
928        }
929
930        /**
931         * This is called when the current program content turns out to be allowed to watch since
932         * its content rating is not blocked by parental controls.
933         *
934         * @param inputId The ID of the TV input bound to this view.
935         */
936        public void onContentAllowed(String inputId) {
937        }
938
939        /**
940         * This is called when the current program content turns out to be not allowed to watch
941         * since its content rating is blocked by parental controls.
942         *
943         * @param inputId The ID of the TV input bound to this view.
944         * @param rating The content rating of the blocked program.
945         */
946        public void onContentBlocked(String inputId, TvContentRating rating) {
947        }
948
949        /**
950         * This is invoked when a custom event from the bound TV input is sent to this view.
951         *
952         * @param inputId The ID of the TV input bound to this view.
953         * @param eventType The type of the event.
954         * @param eventArgs Optional arguments of the event.
955         * @hide
956         */
957        @SystemApi
958        public void onEvent(String inputId, String eventType, Bundle eventArgs) {
959        }
960
961        /**
962         * This is called when the time shift status is changed.
963         *
964         * @param inputId The ID of the TV input bound to this view.
965         * @param status The current time shift status. Should be one of the followings.
966         * <ul>
967         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
968         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
969         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
970         * </ul>
971         */
972        public void onTimeShiftStatusChanged(String inputId, int status) {
973        }
974    }
975
976    /**
977     * Interface definition for a callback to be invoked when the unhandled input event is received.
978     */
979    public interface OnUnhandledInputEventListener {
980        /**
981         * Called when an input event was not handled by the bound TV input.
982         *
983         * <p>This is called asynchronously from where the event is dispatched. It gives the host
984         * application a chance to handle the unhandled input events.
985         *
986         * @param event The input event.
987         * @return If you handled the event, return {@code true}. If you want to allow the event to
988         *         be handled by the next receiver, return {@code false}.
989         */
990        boolean onUnhandledInputEvent(InputEvent event);
991    }
992
993    private class MySessionCallback extends SessionCallback {
994        final String mInputId;
995        Uri mChannelUri;
996        Bundle mTuneParams;
997
998        MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) {
999            mInputId = inputId;
1000            mChannelUri = channelUri;
1001            mTuneParams = tuneParams;
1002        }
1003
1004        @Override
1005        public void onSessionCreated(Session session) {
1006            if (DEBUG) {
1007                Log.d(TAG, "onSessionCreated()");
1008            }
1009            if (this != mSessionCallback) {
1010                Log.w(TAG, "onSessionCreated - session already created");
1011                // This callback is obsolete.
1012                if (session != null) {
1013                    session.release();
1014                }
1015                return;
1016            }
1017            mSession = session;
1018            if (session != null) {
1019                // Sends the pending app private commands first.
1020                for (Pair<String, Bundle> command : mPendingAppPrivateCommands) {
1021                    mSession.sendAppPrivateCommand(command.first, command.second);
1022                }
1023                mPendingAppPrivateCommands.clear();
1024
1025                synchronized (sMainTvViewLock) {
1026                    if (hasWindowFocus() && TvView.this == sMainTvView.get()) {
1027                        mSession.setMain();
1028                    }
1029                }
1030                // mSurface may not be ready yet as soon as starting an application.
1031                // In the case, we don't send Session.setSurface(null) unnecessarily.
1032                // setSessionSurface will be called in surfaceCreated.
1033                if (mSurface != null) {
1034                    setSessionSurface(mSurface);
1035                    if (mSurfaceChanged) {
1036                        dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
1037                    }
1038                }
1039                createSessionOverlayView();
1040                if (mStreamVolume != null) {
1041                    mSession.setStreamVolume(mStreamVolume);
1042                }
1043                if (mCaptionEnabled != null) {
1044                    mSession.setCaptionEnabled(mCaptionEnabled);
1045                }
1046                mSession.tune(mChannelUri, mTuneParams);
1047                ensurePositionTracking();
1048            } else {
1049                mSessionCallback = null;
1050                if (mCallback != null) {
1051                    mCallback.onConnectionFailed(mInputId);
1052                }
1053            }
1054        }
1055
1056        @Override
1057        public void onSessionReleased(Session session) {
1058            if (DEBUG) {
1059                Log.d(TAG, "onSessionReleased()");
1060            }
1061            if (this != mSessionCallback) {
1062                Log.w(TAG, "onSessionReleased - session not created");
1063                return;
1064            }
1065            mOverlayViewCreated = false;
1066            mOverlayViewFrame = null;
1067            mSessionCallback = null;
1068            mSession = null;
1069            if (mCallback != null) {
1070                mCallback.onDisconnected(mInputId);
1071            }
1072        }
1073
1074        @Override
1075        public void onChannelRetuned(Session session, Uri channelUri) {
1076            if (DEBUG) {
1077                Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")");
1078            }
1079            if (this != mSessionCallback) {
1080                Log.w(TAG, "onChannelRetuned - session not created");
1081                return;
1082            }
1083            if (mCallback != null) {
1084                mCallback.onChannelRetuned(mInputId, channelUri);
1085            }
1086        }
1087
1088        @Override
1089        public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
1090            if (DEBUG) {
1091                Log.d(TAG, "onTracksChanged(" + tracks + ")");
1092            }
1093            if (this != mSessionCallback) {
1094                Log.w(TAG, "onTracksChanged - session not created");
1095                return;
1096            }
1097            if (mCallback != null) {
1098                mCallback.onTracksChanged(mInputId, tracks);
1099            }
1100        }
1101
1102        @Override
1103        public void onTrackSelected(Session session, int type, String trackId) {
1104            if (DEBUG) {
1105                Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")");
1106            }
1107            if (this != mSessionCallback) {
1108                Log.w(TAG, "onTrackSelected - session not created");
1109                return;
1110            }
1111            if (mCallback != null) {
1112                mCallback.onTrackSelected(mInputId, type, trackId);
1113            }
1114        }
1115
1116        @Override
1117        public void onVideoSizeChanged(Session session, int width, int height) {
1118            if (DEBUG) {
1119                Log.d(TAG, "onVideoSizeChanged()");
1120            }
1121            if (this != mSessionCallback) {
1122                Log.w(TAG, "onVideoSizeChanged - session not created");
1123                return;
1124            }
1125            if (mCallback != null) {
1126                mCallback.onVideoSizeChanged(mInputId, width, height);
1127            }
1128        }
1129
1130        @Override
1131        public void onVideoAvailable(Session session) {
1132            if (DEBUG) {
1133                Log.d(TAG, "onVideoAvailable()");
1134            }
1135            if (this != mSessionCallback) {
1136                Log.w(TAG, "onVideoAvailable - session not created");
1137                return;
1138            }
1139            if (mCallback != null) {
1140                mCallback.onVideoAvailable(mInputId);
1141            }
1142        }
1143
1144        @Override
1145        public void onVideoUnavailable(Session session, int reason) {
1146            if (DEBUG) {
1147                Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")");
1148            }
1149            if (this != mSessionCallback) {
1150                Log.w(TAG, "onVideoUnavailable - session not created");
1151                return;
1152            }
1153            if (mCallback != null) {
1154                mCallback.onVideoUnavailable(mInputId, reason);
1155            }
1156        }
1157
1158        @Override
1159        public void onContentAllowed(Session session) {
1160            if (DEBUG) {
1161                Log.d(TAG, "onContentAllowed()");
1162            }
1163            if (this != mSessionCallback) {
1164                Log.w(TAG, "onContentAllowed - session not created");
1165                return;
1166            }
1167            if (mCallback != null) {
1168                mCallback.onContentAllowed(mInputId);
1169            }
1170        }
1171
1172        @Override
1173        public void onContentBlocked(Session session, TvContentRating rating) {
1174            if (DEBUG) {
1175                Log.d(TAG, "onContentBlocked(rating=" + rating + ")");
1176            }
1177            if (this != mSessionCallback) {
1178                Log.w(TAG, "onContentBlocked - session not created");
1179                return;
1180            }
1181            if (mCallback != null) {
1182                mCallback.onContentBlocked(mInputId, rating);
1183            }
1184        }
1185
1186        @Override
1187        public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
1188            if (DEBUG) {
1189                Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
1190                        + right + ", bottom=" + bottom + ",)");
1191            }
1192            if (this != mSessionCallback) {
1193                Log.w(TAG, "onLayoutSurface - session not created");
1194                return;
1195            }
1196            mSurfaceViewLeft = left;
1197            mSurfaceViewTop = top;
1198            mSurfaceViewRight = right;
1199            mSurfaceViewBottom = bottom;
1200            mUseRequestedSurfaceLayout = true;
1201            requestLayout();
1202        }
1203
1204        @Override
1205        public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
1206            if (DEBUG) {
1207                Log.d(TAG, "onSessionEvent(" + eventType + ")");
1208            }
1209            if (this != mSessionCallback) {
1210                Log.w(TAG, "onSessionEvent - session not created");
1211                return;
1212            }
1213            if (mCallback != null) {
1214                mCallback.onEvent(mInputId, eventType, eventArgs);
1215            }
1216        }
1217
1218        @Override
1219        public void onTimeShiftStatusChanged(Session session, int status) {
1220            if (DEBUG) {
1221                Log.d(TAG, "onTimeShiftStatusChanged()");
1222            }
1223            if (this != mSessionCallback) {
1224                Log.w(TAG, "onTimeShiftStatusChanged - session not created");
1225                return;
1226            }
1227            if (mCallback != null) {
1228                mCallback.onTimeShiftStatusChanged(mInputId, status);
1229            }
1230        }
1231
1232        @Override
1233        public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
1234            if (DEBUG) {
1235                Log.d(TAG, "onTimeShiftStartPositionChanged()");
1236            }
1237            if (this != mSessionCallback) {
1238                Log.w(TAG, "onTimeShiftStartPositionChanged - session not created");
1239                return;
1240            }
1241            if (mTimeShiftPositionCallback != null) {
1242                mTimeShiftPositionCallback.onTimeShiftStartPositionChanged(mInputId, timeMs);
1243            }
1244        }
1245
1246        @Override
1247        public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
1248            if (DEBUG) {
1249                Log.d(TAG, "onTimeShiftCurrentPositionChanged()");
1250            }
1251            if (this != mSessionCallback) {
1252                Log.w(TAG, "onTimeShiftCurrentPositionChanged - session not created");
1253                return;
1254            }
1255            if (mTimeShiftPositionCallback != null) {
1256                mTimeShiftPositionCallback.onTimeShiftCurrentPositionChanged(mInputId, timeMs);
1257            }
1258        }
1259    }
1260}
1261