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.SystemApi;
20import android.content.Context;
21import android.graphics.Canvas;
22import android.graphics.PorterDuff;
23import android.graphics.Rect;
24import android.graphics.Region;
25import android.media.tv.TvInputManager.Session;
26import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
27import android.media.tv.TvInputManager.SessionCallback;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.view.InputEvent;
35import android.view.KeyEvent;
36import android.view.MotionEvent;
37import android.view.Surface;
38import android.view.SurfaceHolder;
39import android.view.SurfaceView;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.ViewRootImpl;
43
44import java.lang.ref.WeakReference;
45import java.util.List;
46
47/**
48 * Displays TV contents. The TvView class provides a high level interface for applications to show
49 * TV programs from various TV sources that implement {@link TvInputService}. (Note that the list of
50 * TV inputs available on the system can be obtained by calling
51 * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.)
52 * <p>
53 * Once the application supplies the URI for a specific TV channel to {@link #tune(String, Uri)}
54 * method, it takes care of underlying service binding (and unbinding if the current TvView is
55 * already bound to a service) and automatically allocates/deallocates resources needed. In addition
56 * to a few essential methods to control how the contents are presented, it also provides a way to
57 * dispatch input events to the connected TvInputService in order to enable custom key actions for
58 * the TV input.
59 * </p>
60 */
61public class TvView extends ViewGroup {
62    private static final String TAG = "TvView";
63    private static final boolean DEBUG = false;
64
65    private static final int ZORDER_MEDIA = 0;
66    private static final int ZORDER_MEDIA_OVERLAY = 1;
67    private static final int ZORDER_ON_TOP = 2;
68
69    private static final int CAPTION_DEFAULT = 0;
70    private static final int CAPTION_ENABLED = 1;
71    private static final int CAPTION_DISABLED = 2;
72
73    private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null);
74
75    private static final Object sMainTvViewLock = new Object();
76    private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW;
77
78    private final Handler mHandler = new Handler();
79    private Session mSession;
80    private SurfaceView mSurfaceView;
81    private Surface mSurface;
82    private boolean mOverlayViewCreated;
83    private Rect mOverlayViewFrame;
84    private final TvInputManager mTvInputManager;
85    private MySessionCallback mSessionCallback;
86    private TvInputCallback mCallback;
87    private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
88    private boolean mHasStreamVolume;
89    private float mStreamVolume;
90    private int mCaptionEnabled;
91    private String mAppPrivateCommandAction;
92    private Bundle mAppPrivateCommandData;
93
94    private boolean mSurfaceChanged;
95    private int mSurfaceFormat;
96    private int mSurfaceWidth;
97    private int mSurfaceHeight;
98    private final AttributeSet mAttrs;
99    private final int mDefStyleAttr;
100    private int mWindowZOrder;
101    private boolean mUseRequestedSurfaceLayout;
102    private int mSurfaceViewLeft;
103    private int mSurfaceViewRight;
104    private int mSurfaceViewTop;
105    private int mSurfaceViewBottom;
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 any existing
177     *            callbacks.
178     */
179    public void setCallback(TvInputCallback callback) {
180        mCallback = callback;
181    }
182
183    /**
184     * Sets this as the main {@link TvView}.
185     * <p>
186     * 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     * </p><p>
193     * 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 {@link #setMain} is called for other
195     * {@link TvView}.
196     * </p>
197     * @hide
198     */
199    @SystemApi
200    public void setMain() {
201        synchronized (sMainTvViewLock) {
202            sMainTvView = new WeakReference<>(this);
203            if (hasWindowFocus() && mSession != null) {
204                mSession.setMain();
205            }
206        }
207    }
208
209    /**
210     * Sets the Z order of a window owning the surface of this TvView above the normal TvView
211     * but below an application.
212     *
213     * @see SurfaceView#setZOrderMediaOverlay
214     * @hide
215     */
216    @SystemApi
217    public void setZOrderMediaOverlay(boolean isMediaOverlay) {
218        if (isMediaOverlay) {
219            mWindowZOrder = ZORDER_MEDIA_OVERLAY;
220            removeSessionOverlayView();
221        } else {
222            mWindowZOrder = ZORDER_MEDIA;
223            createSessionOverlayView();
224        }
225        if (mSurfaceView != null) {
226            // ZOrderOnTop(false) removes WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
227            // from WindowLayoutParam as well as changes window type.
228            mSurfaceView.setZOrderOnTop(false);
229            mSurfaceView.setZOrderMediaOverlay(isMediaOverlay);
230        }
231    }
232
233    /**
234     * Sets the Z order of a window owning the surface of this TvView on top of an application.
235     *
236     * @see SurfaceView#setZOrderOnTop
237     * @hide
238     */
239    @SystemApi
240    public void setZOrderOnTop(boolean onTop) {
241        if (onTop) {
242            mWindowZOrder = ZORDER_ON_TOP;
243            removeSessionOverlayView();
244        } else {
245            mWindowZOrder = ZORDER_MEDIA;
246            createSessionOverlayView();
247        }
248        if (mSurfaceView != null) {
249            mSurfaceView.setZOrderMediaOverlay(false);
250            mSurfaceView.setZOrderOnTop(onTop);
251        }
252     }
253
254    /**
255     * Sets the relative stream volume of this session to handle a change of audio focus.
256     *
257     * @param volume A volume value between 0.0f to 1.0f.
258     */
259    public void setStreamVolume(float volume) {
260        if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")");
261        mHasStreamVolume = true;
262        mStreamVolume = volume;
263        if (mSession == null) {
264            // Volume will be set once the connection has been made.
265            return;
266        }
267        mSession.setStreamVolume(volume);
268    }
269
270    /**
271     * Tunes to a given channel.
272     *
273     * @param inputId The ID of TV input which will play the given channel.
274     * @param channelUri The URI of a channel.
275     */
276    public void tune(String inputId, Uri channelUri) {
277        tune(inputId, channelUri, null);
278    }
279
280    /**
281     * Tunes to a given channel.
282     *
283     * @param inputId The ID of TV input which will play the given channel.
284     * @param channelUri The URI of a channel.
285     * @param params Extra parameters which might be handled with the tune event.
286     * @hide
287     */
288    @SystemApi
289    public void tune(String inputId, Uri channelUri, Bundle params) {
290        if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")");
291        if (TextUtils.isEmpty(inputId)) {
292            throw new IllegalArgumentException("inputId cannot be null or an empty string");
293        }
294        synchronized (sMainTvViewLock) {
295            if (sMainTvView.get() == null) {
296                sMainTvView = new WeakReference<>(this);
297            }
298        }
299        if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) {
300            if (mSession != null) {
301                mSession.tune(channelUri, params);
302            } else {
303                // Session is not created yet. Replace the channel which will be set once the
304                // session is made.
305                mSessionCallback.mChannelUri = channelUri;
306                mSessionCallback.mTuneParams = params;
307            }
308        } else {
309            resetInternal();
310            // When createSession() is called multiple times before the callback is called,
311            // only the callback of the last createSession() call will be actually called back.
312            // The previous callbacks will be ignored. For the logic, mSessionCallback
313            // is newly assigned for every createSession request and compared with
314            // MySessionCreateCallback.this.
315            mSessionCallback = new MySessionCallback(inputId, channelUri, params);
316            if (mTvInputManager != null) {
317                mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
318            }
319        }
320    }
321
322    /**
323     * Resets this TvView.
324     * <p>
325     * This method is primarily used to un-tune the current TvView.
326     */
327    public void reset() {
328        if (DEBUG) Log.d(TAG, "reset()");
329        synchronized (sMainTvViewLock) {
330            if (this == sMainTvView.get()) {
331                sMainTvView = NULL_TV_VIEW;
332            }
333        }
334        resetInternal();
335    }
336
337    private void resetInternal() {
338        if (mSession != null) {
339            release();
340            resetSurfaceView();
341        }
342    }
343
344    /**
345     * Requests to unblock TV content according to the given rating.
346     * <p>
347     * This notifies TV input that blocked content is now OK to play.
348     * </p>
349     *
350     * @param unblockedRating A TvContentRating to unblock.
351     * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
352     * @hide
353     */
354    @SystemApi
355    public void requestUnblockContent(TvContentRating unblockedRating) {
356        if (mSession != null) {
357            mSession.requestUnblockContent(unblockedRating);
358        }
359    }
360
361    /**
362     * Enables or disables the caption in this TvView.
363     * <p>
364     * Note that this method does not take any effect unless the current TvView is tuned.
365     *
366     * @param enabled {@code true} to enable, {@code false} to disable.
367     */
368    public void setCaptionEnabled(boolean enabled) {
369        mCaptionEnabled = enabled ? CAPTION_ENABLED : CAPTION_DISABLED;
370        if (mSession != null) {
371            mSession.setCaptionEnabled(enabled);
372        }
373    }
374
375    /**
376     * Selects a track.
377     *
378     * @param type The type of the track to select. The type can be {@link TvTrackInfo#TYPE_AUDIO},
379     *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
380     * @param trackId The ID of the track to select. {@code null} means to unselect the current
381     *            track for a given type.
382     * @see #getTracks
383     * @see #getSelectedTrack
384     */
385    public void selectTrack(int type, String trackId) {
386        if (mSession != null) {
387            mSession.selectTrack(type, trackId);
388        }
389    }
390
391    /**
392     * Returns the list of tracks. Returns {@code null} if the information is not available.
393     *
394     * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
395     *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
396     * @see #selectTrack
397     * @see #getSelectedTrack
398     */
399    public List<TvTrackInfo> getTracks(int type) {
400        if (mSession == null) {
401            return null;
402        }
403        return mSession.getTracks(type);
404    }
405
406    /**
407     * Returns the ID of the selected track for a given type. Returns {@code null} if the
408     * information is not available or the track is not selected.
409     *
410     * @param type The type of the selected tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
411     *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
412     * @see #selectTrack
413     * @see #getTracks
414     */
415    public String getSelectedTrack(int type) {
416        if (mSession == null) {
417            return null;
418        }
419        return mSession.getSelectedTrack(type);
420    }
421
422    /**
423     * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
424     * TvInputService.Session.appPrivateCommand()} on the current TvView.
425     *
426     * @param action The name of the private command to send. This <em>must</em> be a scoped name,
427     *            i.e. prefixed with a package name you own, so that different developers will not
428     *            create conflicting commands.
429     * @param data An optional bundle to send with the command.
430     * @hide
431     */
432    @SystemApi
433    public void sendAppPrivateCommand(String action, Bundle data) {
434        if (TextUtils.isEmpty(action)) {
435            throw new IllegalArgumentException("action cannot be null or an empty string");
436        }
437        if (mSession != null) {
438            mSession.sendAppPrivateCommand(action, data);
439        } else {
440            Log.w(TAG, "sendAppPrivateCommand - session not created (action " + action + " cached)");
441            if (mAppPrivateCommandAction != null) {
442                Log.w(TAG, "previous cached action " + action + " removed");
443            }
444            mAppPrivateCommandAction = action;
445            mAppPrivateCommandData = data;
446        }
447    }
448
449    /**
450     * Dispatches an unhandled input event to the next receiver.
451     * <p>
452     * Except system keys, TvView always consumes input events in the normal flow. This is called
453     * asynchronously from where the event is dispatched. It gives the host application a chance to
454     * dispatch the unhandled input events.
455     *
456     * @param event The input event.
457     * @return {@code true} if the event was handled by the view, {@code false} otherwise.
458     */
459    public boolean dispatchUnhandledInputEvent(InputEvent event) {
460        if (mOnUnhandledInputEventListener != null) {
461            if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
462                return true;
463            }
464        }
465        return onUnhandledInputEvent(event);
466    }
467
468    /**
469     * Called when an unhandled input event also has not been handled by the user provided
470     * callback. This is the last chance to handle the unhandled input event in the TvView.
471     *
472     * @param event The input event.
473     * @return If you handled the event, return {@code true}. If you want to allow the event to be
474     *         handled by the next receiver, return {@code false}.
475     */
476    public boolean onUnhandledInputEvent(InputEvent event) {
477        return false;
478    }
479
480    /**
481     * Registers a callback to be invoked when an input event is not handled by the bound TV input.
482     *
483     * @param listener The callback to be invoked when the unhandled input event is received.
484     */
485    public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
486        mOnUnhandledInputEventListener = listener;
487    }
488
489    @Override
490    public boolean dispatchKeyEvent(KeyEvent event) {
491        if (super.dispatchKeyEvent(event)) {
492            return true;
493        }
494        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
495        if (mSession == null) {
496            return false;
497        }
498        InputEvent copiedEvent = event.copy();
499        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
500                mHandler);
501        return ret != Session.DISPATCH_NOT_HANDLED;
502    }
503
504    @Override
505    public boolean dispatchTouchEvent(MotionEvent event) {
506        if (super.dispatchTouchEvent(event)) {
507            return true;
508        }
509        if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
510        if (mSession == null) {
511            return false;
512        }
513        InputEvent copiedEvent = event.copy();
514        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
515                mHandler);
516        return ret != Session.DISPATCH_NOT_HANDLED;
517    }
518
519    @Override
520    public boolean dispatchTrackballEvent(MotionEvent event) {
521        if (super.dispatchTrackballEvent(event)) {
522            return true;
523        }
524        if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
525        if (mSession == null) {
526            return false;
527        }
528        InputEvent copiedEvent = event.copy();
529        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
530                mHandler);
531        return ret != Session.DISPATCH_NOT_HANDLED;
532    }
533
534    @Override
535    public boolean dispatchGenericMotionEvent(MotionEvent event) {
536        if (super.dispatchGenericMotionEvent(event)) {
537            return true;
538        }
539        if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
540        if (mSession == null) {
541            return false;
542        }
543        InputEvent copiedEvent = event.copy();
544        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
545                mHandler);
546        return ret != Session.DISPATCH_NOT_HANDLED;
547    }
548
549    @Override
550    public void dispatchWindowFocusChanged(boolean hasFocus) {
551        super.dispatchWindowFocusChanged(hasFocus);
552        // Other app may have shown its own main TvView.
553        // Set main again to regain main session.
554        synchronized (sMainTvViewLock) {
555            if (hasFocus && this == sMainTvView.get() && mSession != null) {
556                mSession.setMain();
557            }
558        }
559    }
560
561    @Override
562    protected void onAttachedToWindow() {
563        super.onAttachedToWindow();
564        createSessionOverlayView();
565    }
566
567    @Override
568    protected void onDetachedFromWindow() {
569        removeSessionOverlayView();
570        super.onDetachedFromWindow();
571    }
572
573    @Override
574    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
575        if (DEBUG) {
576            Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
577                    + ", bottom=" + bottom + ",)");
578        }
579        if (mUseRequestedSurfaceLayout) {
580            mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
581                    mSurfaceViewBottom);
582        } else {
583            mSurfaceView.layout(0, 0, right - left, bottom - top);
584        }
585    }
586
587    @Override
588    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
589        mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
590        int width = mSurfaceView.getMeasuredWidth();
591        int height = mSurfaceView.getMeasuredHeight();
592        int childState = mSurfaceView.getMeasuredState();
593        setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
594                resolveSizeAndState(height, heightMeasureSpec,
595                        childState << MEASURED_HEIGHT_STATE_SHIFT));
596    }
597
598    @Override
599    public boolean gatherTransparentRegion(Region region) {
600        if (mWindowZOrder != ZORDER_ON_TOP) {
601            if (region != null) {
602                int width = getWidth();
603                int height = getHeight();
604                if (width > 0 && height > 0) {
605                    int location[] = new int[2];
606                    getLocationInWindow(location);
607                    int left = location[0];
608                    int top = location[1];
609                    region.op(left, top, left + width, top + height, Region.Op.UNION);
610                }
611            }
612        }
613        return super.gatherTransparentRegion(region);
614    }
615
616    @Override
617    public void draw(Canvas canvas) {
618        if (mWindowZOrder != ZORDER_ON_TOP) {
619            // Punch a hole so that the underlying overlay view and surface can be shown.
620            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
621        }
622        super.draw(canvas);
623    }
624
625    @Override
626    protected void dispatchDraw(Canvas canvas) {
627        if (mWindowZOrder != ZORDER_ON_TOP) {
628            // Punch a hole so that the underlying overlay view and surface can be shown.
629            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
630        }
631        super.dispatchDraw(canvas);
632    }
633
634    @Override
635    protected void onVisibilityChanged(View changedView, int visibility) {
636        super.onVisibilityChanged(changedView, visibility);
637        mSurfaceView.setVisibility(visibility);
638        if (visibility == View.VISIBLE) {
639            createSessionOverlayView();
640        } else {
641            removeSessionOverlayView();
642        }
643    }
644
645    private void resetSurfaceView() {
646        if (mSurfaceView != null) {
647            mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
648            removeView(mSurfaceView);
649        }
650        mSurface = null;
651        mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) {
652            @Override
653            protected void updateWindow(boolean force, boolean redrawNeeded) {
654                super.updateWindow(force, redrawNeeded);
655                relayoutSessionOverlayView();
656            }};
657        mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
658        if (mWindowZOrder == ZORDER_MEDIA_OVERLAY) {
659            mSurfaceView.setZOrderMediaOverlay(true);
660        } else if (mWindowZOrder == ZORDER_ON_TOP) {
661            mSurfaceView.setZOrderOnTop(true);
662        }
663        addView(mSurfaceView);
664    }
665
666    private void release() {
667        mAppPrivateCommandAction = null;
668        mAppPrivateCommandData = null;
669
670        setSessionSurface(null);
671        removeSessionOverlayView();
672        mUseRequestedSurfaceLayout = false;
673        mSession.release();
674        mSession = null;
675        mSessionCallback = null;
676    }
677
678    private void setSessionSurface(Surface surface) {
679        if (mSession == null) {
680            return;
681        }
682        mSession.setSurface(surface);
683    }
684
685    private void dispatchSurfaceChanged(int format, int width, int height) {
686        if (mSession == null) {
687            return;
688        }
689        mSession.dispatchSurfaceChanged(format, width, height);
690    }
691
692    private void createSessionOverlayView() {
693        if (mSession == null || !isAttachedToWindow()
694                || mOverlayViewCreated || mWindowZOrder != ZORDER_MEDIA) {
695            return;
696        }
697        mOverlayViewFrame = getViewFrameOnScreen();
698        mSession.createOverlayView(this, mOverlayViewFrame);
699        mOverlayViewCreated = true;
700    }
701
702    private void removeSessionOverlayView() {
703        if (mSession == null || !mOverlayViewCreated) {
704            return;
705        }
706        mSession.removeOverlayView();
707        mOverlayViewCreated = false;
708        mOverlayViewFrame = null;
709    }
710
711    private void relayoutSessionOverlayView() {
712        if (mSession == null || !isAttachedToWindow() || !mOverlayViewCreated
713                || mWindowZOrder != ZORDER_MEDIA) {
714            return;
715        }
716        Rect viewFrame = getViewFrameOnScreen();
717        if (viewFrame.equals(mOverlayViewFrame)) {
718            return;
719        }
720        mSession.relayoutOverlayView(viewFrame);
721        mOverlayViewFrame = viewFrame;
722    }
723
724    private Rect getViewFrameOnScreen() {
725        int[] location = new int[2];
726        getLocationOnScreen(location);
727        return new Rect(location[0], location[1],
728                location[0] + getWidth(), location[1] + getHeight());
729    }
730
731    /**
732     * Callback used to receive various status updates on the {@link TvView}.
733     */
734    public abstract static class TvInputCallback {
735
736        /**
737         * This is invoked when an error occurred while establishing a connection to the underlying
738         * TV input.
739         *
740         * @param inputId The ID of the TV input bound to this view.
741         */
742        public void onConnectionFailed(String inputId) {
743        }
744
745        /**
746         * This is invoked when the existing connection to the underlying TV input is lost.
747         *
748         * @param inputId The ID of the TV input bound to this view.
749         */
750        public void onDisconnected(String inputId) {
751        }
752
753        /**
754         * This is invoked when the channel of this TvView is changed by the underlying TV input
755         * without any {@link TvView#tune(String, Uri)} request.
756         *
757         * @param inputId The ID of the TV input bound to this view.
758         * @param channelUri The URI of a channel.
759         */
760        public void onChannelRetuned(String inputId, Uri channelUri) {
761        }
762
763        /**
764         * This is called when the track information has been changed.
765         *
766         * @param inputId The ID of the TV input bound to this view.
767         * @param tracks A list which includes track information.
768         */
769        public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
770        }
771
772        /**
773         * This is called when there is a change on the selected tracks.
774         *
775         * @param inputId The ID of the TV input bound to this view.
776         * @param type The type of the track selected. The type can be
777         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
778         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
779         * @param trackId The ID of the track selected.
780         */
781        public void onTrackSelected(String inputId, int type, String trackId) {
782        }
783
784        /**
785         * This is invoked when the video size has been changed. It is also called when the first
786         * time video size information becomes available after this view is tuned to a specific
787         * channel.
788         *
789         * @param inputId The ID of the TV input bound to this view.
790         * @param width The width of the video.
791         * @param height The height of the video.
792         */
793        public void onVideoSizeChanged(String inputId, int width, int height) {
794        }
795
796        /**
797         * This is called when the video is available, so the TV input starts the playback.
798         *
799         * @param inputId The ID of the TV input bound to this view.
800         */
801        public void onVideoAvailable(String inputId) {
802        }
803
804        /**
805         * This is called when the video is not available, so the TV input stops the playback.
806         *
807         * @param inputId The ID of the TV input bound to this view.
808         * @param reason The reason why the TV input stopped the playback:
809         * <ul>
810         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
811         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
812         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
813         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
814         * </ul>
815         */
816        public void onVideoUnavailable(String inputId, int reason) {
817        }
818
819        /**
820         * This is called when the current program content turns out to be allowed to watch since
821         * its content rating is not blocked by parental controls.
822         *
823         * @param inputId The ID of the TV input bound to this view.
824         */
825        public void onContentAllowed(String inputId) {
826        }
827
828        /**
829         * This is called when the current program content turns out to be not allowed to watch
830         * since its content rating is blocked by parental controls.
831         *
832         * @param inputId The ID of the TV input bound to this view.
833         * @param rating The content rating of the blocked program.
834         */
835        public void onContentBlocked(String inputId, TvContentRating rating) {
836        }
837
838        /**
839         * This is invoked when a custom event from the bound TV input is sent to this view.
840         *
841         * @param eventType The type of the event.
842         * @param eventArgs Optional arguments of the event.
843         * @hide
844         */
845        @SystemApi
846        public void onEvent(String inputId, String eventType, Bundle eventArgs) {
847        }
848    }
849
850    /**
851     * Interface definition for a callback to be invoked when the unhandled input event is received.
852     */
853    public interface OnUnhandledInputEventListener {
854        /**
855         * Called when an input event was not handled by the bound TV input.
856         * <p>
857         * This is called asynchronously from where the event is dispatched. It gives the host
858         * application a chance to handle the unhandled input events.
859         *
860         * @param event The input event.
861         * @return If you handled the event, return {@code true}. If you want to allow the event to
862         *         be handled by the next receiver, return {@code false}.
863         */
864        boolean onUnhandledInputEvent(InputEvent event);
865    }
866
867    private class MySessionCallback extends SessionCallback {
868        final String mInputId;
869        Uri mChannelUri;
870        Bundle mTuneParams;
871
872        MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) {
873            mInputId = inputId;
874            mChannelUri = channelUri;
875            mTuneParams = tuneParams;
876        }
877
878        @Override
879        public void onSessionCreated(Session session) {
880            if (DEBUG) {
881                Log.d(TAG, "onSessionCreated()");
882            }
883            if (this != mSessionCallback) {
884                Log.w(TAG, "onSessionCreated - session already created");
885                // This callback is obsolete.
886                if (session != null) {
887                    session.release();
888                }
889                return;
890            }
891            mSession = session;
892            if (session != null) {
893                synchronized (sMainTvViewLock) {
894                    if (hasWindowFocus() && TvView.this == sMainTvView.get()) {
895                        mSession.setMain();
896                    }
897                }
898                // mSurface may not be ready yet as soon as starting an application.
899                // In the case, we don't send Session.setSurface(null) unnecessarily.
900                // setSessionSurface will be called in surfaceCreated.
901                if (mSurface != null) {
902                    setSessionSurface(mSurface);
903                    if (mSurfaceChanged) {
904                        dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
905                    }
906                }
907                createSessionOverlayView();
908                if (mCaptionEnabled != CAPTION_DEFAULT) {
909                    mSession.setCaptionEnabled(mCaptionEnabled == CAPTION_ENABLED);
910                }
911                mSession.tune(mChannelUri, mTuneParams);
912                if (mHasStreamVolume) {
913                    mSession.setStreamVolume(mStreamVolume);
914                }
915                if (mAppPrivateCommandAction != null) {
916                    mSession.sendAppPrivateCommand(
917                            mAppPrivateCommandAction, mAppPrivateCommandData);
918                    mAppPrivateCommandAction = null;
919                    mAppPrivateCommandData = null;
920                }
921            } else {
922                mSessionCallback = null;
923                if (mCallback != null) {
924                    mCallback.onConnectionFailed(mInputId);
925                }
926            }
927        }
928
929        @Override
930        public void onSessionReleased(Session session) {
931            if (DEBUG) {
932                Log.d(TAG, "onSessionReleased()");
933            }
934            if (this != mSessionCallback) {
935                Log.w(TAG, "onSessionReleased - session not created");
936                return;
937            }
938            mOverlayViewCreated = false;
939            mOverlayViewFrame = null;
940            mSessionCallback = null;
941            mSession = null;
942            if (mCallback != null) {
943                mCallback.onDisconnected(mInputId);
944            }
945        }
946
947        @Override
948        public void onChannelRetuned(Session session, Uri channelUri) {
949            if (DEBUG) {
950                Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")");
951            }
952            if (this != mSessionCallback) {
953                Log.w(TAG, "onChannelRetuned - session not created");
954                return;
955            }
956            if (mCallback != null) {
957                mCallback.onChannelRetuned(mInputId, channelUri);
958            }
959        }
960
961        @Override
962        public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
963            if (DEBUG) {
964                Log.d(TAG, "onTracksChanged(" + tracks + ")");
965            }
966            if (this != mSessionCallback) {
967                Log.w(TAG, "onTracksChanged - session not created");
968                return;
969            }
970            if (mCallback != null) {
971                mCallback.onTracksChanged(mInputId, tracks);
972            }
973        }
974
975        @Override
976        public void onTrackSelected(Session session, int type, String trackId) {
977            if (DEBUG) {
978                Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")");
979            }
980            if (this != mSessionCallback) {
981                Log.w(TAG, "onTrackSelected - session not created");
982                return;
983            }
984            if (mCallback != null) {
985                mCallback.onTrackSelected(mInputId, type, trackId);
986            }
987        }
988
989        @Override
990        public void onVideoSizeChanged(Session session, int width, int height) {
991            if (DEBUG) {
992                Log.d(TAG, "onVideoSizeChanged()");
993            }
994            if (this != mSessionCallback) {
995                Log.w(TAG, "onVideoSizeChanged - session not created");
996                return;
997            }
998            if (mCallback != null) {
999                mCallback.onVideoSizeChanged(mInputId, width, height);
1000            }
1001        }
1002
1003        @Override
1004        public void onVideoAvailable(Session session) {
1005            if (DEBUG) {
1006                Log.d(TAG, "onVideoAvailable()");
1007            }
1008            if (this != mSessionCallback) {
1009                Log.w(TAG, "onVideoAvailable - session not created");
1010                return;
1011            }
1012            if (mCallback != null) {
1013                mCallback.onVideoAvailable(mInputId);
1014            }
1015        }
1016
1017        @Override
1018        public void onVideoUnavailable(Session session, int reason) {
1019            if (DEBUG) {
1020                Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")");
1021            }
1022            if (this != mSessionCallback) {
1023                Log.w(TAG, "onVideoUnavailable - session not created");
1024                return;
1025            }
1026            if (mCallback != null) {
1027                mCallback.onVideoUnavailable(mInputId, reason);
1028            }
1029        }
1030
1031        @Override
1032        public void onContentAllowed(Session session) {
1033            if (DEBUG) {
1034                Log.d(TAG, "onContentAllowed()");
1035            }
1036            if (this != mSessionCallback) {
1037                Log.w(TAG, "onContentAllowed - session not created");
1038                return;
1039            }
1040            if (mCallback != null) {
1041                mCallback.onContentAllowed(mInputId);
1042            }
1043        }
1044
1045        @Override
1046        public void onContentBlocked(Session session, TvContentRating rating) {
1047            if (DEBUG) {
1048                Log.d(TAG, "onContentBlocked(rating=" + rating + ")");
1049            }
1050            if (this != mSessionCallback) {
1051                Log.w(TAG, "onContentBlocked - session not created");
1052                return;
1053            }
1054            if (mCallback != null) {
1055                mCallback.onContentBlocked(mInputId, rating);
1056            }
1057        }
1058
1059        @Override
1060        public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
1061            if (DEBUG) {
1062                Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
1063                        + right + ", bottom=" + bottom + ",)");
1064            }
1065            if (this != mSessionCallback) {
1066                Log.w(TAG, "onLayoutSurface - session not created");
1067                return;
1068            }
1069            mSurfaceViewLeft = left;
1070            mSurfaceViewTop = top;
1071            mSurfaceViewRight = right;
1072            mSurfaceViewBottom = bottom;
1073            mUseRequestedSurfaceLayout = true;
1074            requestLayout();
1075        }
1076
1077        @Override
1078        public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
1079            if (DEBUG) {
1080                Log.d(TAG, "onSessionEvent(" + eventType + ")");
1081            }
1082            if (this != mSessionCallback) {
1083                Log.w(TAG, "onSessionEvent - session not created");
1084                return;
1085            }
1086            if (mCallback != null) {
1087                mCallback.onEvent(mInputId, eventType, eventArgs);
1088            }
1089        }
1090    }
1091}
1092