TvView.java revision 6057102dbb746593a7d59cf377c969b62e38c664
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.content.Context;
20import android.graphics.Rect;
21import android.media.tv.TvInputManager.Session;
22import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
23import android.media.tv.TvInputManager.SessionCallback;
24import android.net.Uri;
25import android.os.Bundle;
26import android.os.Handler;
27import android.text.TextUtils;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.InputEvent;
31import android.view.KeyEvent;
32import android.view.MotionEvent;
33import android.view.Surface;
34import android.view.SurfaceHolder;
35import android.view.SurfaceView;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.ViewRootImpl;
39
40import java.util.List;
41
42/**
43 * View playing TV
44 */
45public class TvView extends ViewGroup {
46    private static final String TAG = "TvView";
47    // STOPSHIP: Turn debugging off.
48    private static final boolean DEBUG = true;
49
50    /**
51     * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the requested TV
52     * input is busy and unable to handle the request.
53     */
54    public static final int ERROR_BUSY = 0;
55
56    /**
57     * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the underlying TV
58     * input has been disconnected.
59     */
60    public static final int ERROR_TV_INPUT_DISCONNECTED = 1;
61
62    private static final int VIDEO_SIZE_VALUE_UNKNOWN = 0;
63
64    private final Handler mHandler = new Handler();
65    private TvInputManager.Session mSession;
66    private final SurfaceView mSurfaceView;
67    private Surface mSurface;
68    private boolean mOverlayViewCreated;
69    private Rect mOverlayViewFrame;
70    private final TvInputManager mTvInputManager;
71    private MySessionCallback mSessionCallback;
72    private TvInputListener mListener;
73    private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
74    private boolean mHasStreamVolume;
75    private float mStreamVolume;
76    private int mVideoWidth = VIDEO_SIZE_VALUE_UNKNOWN;
77    private int mVideoHeight = VIDEO_SIZE_VALUE_UNKNOWN;
78
79    private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
80        @Override
81        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
82            Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width
83                    + ", height=" + height + ")");
84            if (holder.getSurface() == mSurface) {
85                return;
86            }
87            mSurface = holder.getSurface();
88            setSessionSurface(mSurface);
89        }
90
91        @Override
92        public void surfaceCreated(SurfaceHolder holder) {
93            mSurface = holder.getSurface();
94            setSessionSurface(mSurface);
95        }
96
97        @Override
98        public void surfaceDestroyed(SurfaceHolder holder) {
99            mSurface = null;
100            setSessionSurface(null);
101        }
102    };
103
104    private final FinishedInputEventCallback mFinishedInputEventCallback =
105            new FinishedInputEventCallback() {
106        @Override
107        public void onFinishedInputEvent(Object token, boolean handled) {
108            if (DEBUG) {
109                Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")");
110            }
111            if (handled) {
112                return;
113            }
114            // TODO: Re-order unhandled events.
115            InputEvent event = (InputEvent) token;
116            if (dispatchUnhandledInputEvent(event)) {
117                return;
118            }
119            ViewRootImpl viewRootImpl = getViewRootImpl();
120            if (viewRootImpl != null) {
121                viewRootImpl.dispatchUnhandledInputEvent(event);
122            }
123        }
124    };
125
126    public TvView(Context context) {
127        this(context, null, 0);
128    }
129
130    public TvView(Context context, AttributeSet attrs) {
131        this(context, attrs, 0);
132    }
133
134    public TvView(Context context, AttributeSet attrs, int defStyleAttr) {
135        super(context, attrs, defStyleAttr);
136        mSurfaceView = new SurfaceView(context, attrs, defStyleAttr) {
137                @Override
138                protected void updateWindow(boolean force, boolean redrawNeeded) {
139                    super.updateWindow(force, redrawNeeded);
140                    relayoutSessionOverlayView();
141                }};
142        mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
143        addView(mSurfaceView);
144        mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE);
145    }
146
147    /**
148     * Sets a listener for events in this TvView.
149     *
150     * @param listener The listener to be called with events. A value of {@code null} removes any
151     *         existing listener.
152     */
153    public void setTvInputListener(TvInputListener listener) {
154        mListener = listener;
155    }
156
157    /**
158     * Sets the relative stream volume of this session to handle a change of audio focus.
159     *
160     * @param volume A volume value between 0.0f to 1.0f.
161     */
162    public void setStreamVolume(float volume) {
163        if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")");
164        mHasStreamVolume = true;
165        mStreamVolume = volume;
166        if (mSession == null) {
167            // Volume will be set once the connection has been made.
168            return;
169        }
170        mSession.setStreamVolume(volume);
171    }
172
173    /**
174     * Tunes to a given channel.
175     *
176     * @param inputId the id of TV input which will play the given channel.
177     * @param channelUri The URI of a channel.
178     */
179    public void tune(String inputId, Uri channelUri) {
180        if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")");
181        if (TextUtils.isEmpty(inputId)) {
182            throw new IllegalArgumentException("inputId cannot be null or an empty string");
183        }
184        if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) {
185            if (mSession != null) {
186                mSession.tune(channelUri);
187            } else {
188                // Session is not created yet. Replace the channel which will be set once the
189                // session is made.
190                mSessionCallback.mChannelUri = channelUri;
191            }
192        } else {
193            if (mSession != null) {
194                release();
195            }
196            // When createSession() is called multiple times before the callback is called,
197            // only the callback of the last createSession() call will be actually called back.
198            // The previous callbacks will be ignored. For the logic, mSessionCallback
199            // is newly assigned for every createSession request and compared with
200            // MySessionCreateCallback.this.
201            mSessionCallback = new MySessionCallback(inputId, channelUri);
202            mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
203        }
204    }
205
206    /**
207     * Resets this TvView.
208     * <p>
209     * This method is primarily used to un-tune the current TvView.
210     */
211    public void reset() {
212        if (mSession != null) {
213            release();
214        }
215    }
216
217    /**
218     * Enables or disables the caption in this TvView.
219     * <p>
220     * Note that this method does not take any effect unless the current TvView is tuned.
221     *
222     * @param enabled {@code true} to enable, {@code false} to disable.
223     */
224    public void setCaptionEnabled(boolean enabled) {
225        if (mSession != null) {
226            mSession.setCaptionEnabled(enabled);
227        }
228    }
229
230    /**
231     * Select a track.
232     * <p>
233     * If it is called multiple times on the same type of track (ie. Video, Audio, Text), the track
234     * selected in previous will be unselected. Note that this method does not take any effect
235     * unless the current TvView is tuned.
236     * </p>
237     *
238     * @param track the track to be selected.
239     * @see #getTracks()
240     */
241    public void selectTrack(TvTrackInfo track) {
242        if (mSession != null) {
243            mSession.selectTrack(track);
244        }
245    }
246
247    /**
248     * Unselect a track.
249     * <p>
250     * Note that this method does not take any effect unless the current TvView is tuned.
251     *
252     * @param track the track to be unselected.
253     * @see #getTracks()
254     */
255    public void unselectTrack(TvTrackInfo track) {
256        if (mSession != null) {
257            mSession.unselectTrack(track);
258        }
259    }
260
261    /**
262     * Returns a list which includes of track information. May return {@code null} if the
263     * information is not available.
264     */
265    public List<TvTrackInfo> getTracks() {
266        if (mSession == null) {
267            return null;
268        }
269        return mSession.getTracks();
270    }
271
272    /**
273     * Dispatches an unhandled input event to the next receiver.
274     * <p>
275     * Except system keys, TvView always consumes input events in the normal flow. This is called
276     * asynchronously from where the event is dispatched. It gives the host application a chance to
277     * dispatch the unhandled input events.
278     *
279     * @param event The input event.
280     * @return {@code true} if the event was handled by the view, {@code false} otherwise.
281     */
282    public boolean dispatchUnhandledInputEvent(InputEvent event) {
283        if (mOnUnhandledInputEventListener != null) {
284            if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
285                return true;
286            }
287        }
288        return onUnhandledInputEvent(event);
289    }
290
291    /**
292     * Called when an unhandled input event was also not handled by the user provided callback. This
293     * is the last chance to handle the unhandled input event in the TvView.
294     *
295     * @param event The input event.
296     * @return If you handled the event, return {@code true}. If you want to allow the event to be
297     *         handled by the next receiver, return {@code false}.
298     */
299    public boolean onUnhandledInputEvent(InputEvent event) {
300        return false;
301    }
302
303    /**
304     * Registers a callback to be invoked when an input event was not handled by the bound TV input.
305     *
306     * @param listener The callback to invoke when the unhandled input event was received.
307     */
308    public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
309        mOnUnhandledInputEventListener = listener;
310    }
311
312    @Override
313    public boolean dispatchKeyEvent(KeyEvent event) {
314        if (super.dispatchKeyEvent(event)) {
315            return true;
316        }
317        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
318        if (mSession == null) {
319            return false;
320        }
321        InputEvent copiedEvent = event.copy();
322        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
323                mHandler);
324        return ret != Session.DISPATCH_NOT_HANDLED;
325    }
326
327    @Override
328    public boolean dispatchTouchEvent(MotionEvent event) {
329        if (super.dispatchTouchEvent(event)) {
330            return true;
331        }
332        if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
333        if (mSession == null) {
334            return false;
335        }
336        InputEvent copiedEvent = event.copy();
337        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
338                mHandler);
339        return ret != Session.DISPATCH_NOT_HANDLED;
340    }
341
342    @Override
343    public boolean dispatchTrackballEvent(MotionEvent event) {
344        if (super.dispatchTrackballEvent(event)) {
345            return true;
346        }
347        if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
348        if (mSession == null) {
349            return false;
350        }
351        InputEvent copiedEvent = event.copy();
352        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
353                mHandler);
354        return ret != Session.DISPATCH_NOT_HANDLED;
355    }
356
357    @Override
358    public boolean dispatchGenericMotionEvent(MotionEvent event) {
359        if (super.dispatchGenericMotionEvent(event)) {
360            return true;
361        }
362        if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
363        if (mSession == null) {
364            return false;
365        }
366        InputEvent copiedEvent = event.copy();
367        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
368                mHandler);
369        return ret != Session.DISPATCH_NOT_HANDLED;
370    }
371
372    @Override
373    protected void onAttachedToWindow() {
374        super.onAttachedToWindow();
375        createSessionOverlayView();
376    }
377
378    @Override
379    protected void onDetachedFromWindow() {
380        removeSessionOverlayView();
381        super.onDetachedFromWindow();
382    }
383
384    @Override
385    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
386        mSurfaceView.layout(0, 0, right - left, bottom - top);
387    }
388
389    @Override
390    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
391        mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
392        int width = mSurfaceView.getMeasuredWidth();
393        int height = mSurfaceView.getMeasuredHeight();
394        int childState = mSurfaceView.getMeasuredState();
395        setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
396                resolveSizeAndState(height, heightMeasureSpec,
397                        childState << MEASURED_HEIGHT_STATE_SHIFT));
398    }
399
400    @Override
401    public void setVisibility(int visibility) {
402        super.setVisibility(visibility);
403        mSurfaceView.setVisibility(visibility);
404        if (visibility == View.VISIBLE) {
405            createSessionOverlayView();
406        } else {
407            removeSessionOverlayView();
408        }
409    }
410
411    private void release() {
412        setSessionSurface(null);
413        removeSessionOverlayView();
414        mSession.release();
415        mSession = null;
416        mSessionCallback = null;
417    }
418
419    private void setSessionSurface(Surface surface) {
420        if (mSession == null) {
421            return;
422        }
423        mSession.setSurface(surface);
424    }
425
426    private void createSessionOverlayView() {
427        if (mSession == null || !isAttachedToWindow()
428                || mOverlayViewCreated) {
429            return;
430        }
431        mOverlayViewFrame = getViewFrameOnScreen();
432        mSession.createOverlayView(this, mOverlayViewFrame);
433        mOverlayViewCreated = true;
434    }
435
436    private void removeSessionOverlayView() {
437        if (mSession == null || !mOverlayViewCreated) {
438            return;
439        }
440        mSession.removeOverlayView();
441        mOverlayViewCreated = false;
442        mOverlayViewFrame = null;
443    }
444
445    private void relayoutSessionOverlayView() {
446        if (mSession == null || !isAttachedToWindow()
447                || !mOverlayViewCreated) {
448            return;
449        }
450        Rect viewFrame = getViewFrameOnScreen();
451        if (viewFrame.equals(mOverlayViewFrame)) {
452            return;
453        }
454        mSession.relayoutOverlayView(viewFrame);
455        mOverlayViewFrame = viewFrame;
456    }
457
458    private Rect getViewFrameOnScreen() {
459        int[] location = new int[2];
460        getLocationOnScreen(location);
461        return new Rect(location[0], location[1],
462                location[0] + getWidth(), location[1] + getHeight());
463    }
464
465    private void updateVideoSize(List<TvTrackInfo> tracks) {
466        for (TvTrackInfo track : tracks) {
467            if (track.getBoolean(TvTrackInfo.KEY_IS_SELECTED)
468                    && track.getInt(TvTrackInfo.KEY_TYPE) == TvTrackInfo.VALUE_TYPE_VIDEO) {
469                int width = track.getInt(TvTrackInfo.KEY_WIDTH);
470                int height = track.getInt(TvTrackInfo.KEY_HEIGHT);
471                if (width != mVideoWidth || height != mVideoHeight) {
472                    mVideoWidth = width;
473                    mVideoHeight = height;
474                    if (mListener != null) {
475                        mListener.onVideoSizeChanged(mSessionCallback.mInputId, width, height);
476                    }
477                }
478            }
479        }
480    }
481
482    /**
483     * Interface used to receive various status updates on the {@link TvView}.
484     */
485    public abstract static class TvInputListener {
486
487        /**
488         * This is invoked when an error occurred while handling requested operation.
489         *
490         * @param inputId The ID of the TV input bound to this view.
491         * @param errorCode The error code. For the details of error code, please see
492         *         {@link TvView}.
493         */
494        public void onError(String inputId, int errorCode) {
495        }
496
497        /**
498         * This is invoked when the view is tuned to a specific channel and starts decoding video
499         * stream from there. It is also called later when the video size is changed.
500         *
501         * @param inputId The ID of the TV input bound to this view.
502         * @param width The width of the video.
503         * @param height The height of the video.
504         */
505        public void onVideoSizeChanged(String inputId, int width, int height) {
506        }
507
508        /**
509         * This is invoked when the channel of this TvView is changed by the underlying TV input
510         * with out any {@link TvView#tune(String, Uri)} request.
511         *
512         * @param inputId The ID of the TV input bound to this view.
513         * @param channelUri The URI of a channel.
514         */
515        public void onChannelRetuned(String inputId, Uri channelUri) {
516        }
517
518        /**
519         * This is called when the track information has been changed.
520         *
521         * @param inputId The ID of the TV input bound to this view.
522         * @param tracks A list which includes track information.
523         */
524        public void onTrackInfoChanged(String inputId, List<TvTrackInfo> tracks) {
525        }
526
527        /**
528         * This is called when the video is available, so the TV input starts the playback.
529         *
530         * @param inputId The ID of the TV input bound to this view.
531         */
532        public void onVideoAvailable(String inputId) {
533        }
534
535        /**
536         * This is called when the video is not available, so the TV input stops the playback.
537         *
538         * @param inputId The ID of the TV input bound to this view.
539         * @param reason The reason why the TV input stopped the playback:
540         * <ul>
541         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
542         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNE}
543         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
544         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
545         * </ul>
546         */
547        public void onVideoUnavailable(String inputId, int reason) {
548        }
549
550        /**
551         * This is called when the current program content is blocked by parental controls.
552         *
553         * @param inputId The ID of the TV input bound to this view.
554         * @param rating The content rating of the blocked program.
555         */
556        public void onContentBlocked(String inputId, TvContentRating rating) {
557        }
558
559        /**
560         * This is invoked when a custom event from the bound TV input is sent to this view.
561         *
562         * @param eventType The type of the event.
563         * @param eventArgs Optional arguments of the event.
564         * @hide
565         */
566        public void onEvent(String inputId, String eventType, Bundle eventArgs) {
567        }
568    }
569
570    /**
571     * Interface definition for a callback to be invoked when the unhandled input event is received.
572     */
573    public interface OnUnhandledInputEventListener {
574        /**
575         * Called when an input event was not handled by the bound TV input.
576         * <p>
577         * This is called asynchronously from where the event is dispatched. It gives the host
578         * application a chance to handle the unhandled input events.
579         *
580         * @param event The input event.
581         * @return If you handled the event, return {@code true}. If you want to allow the event to
582         *         be handled by the next receiver, return {@code false}.
583         */
584        boolean onUnhandledInputEvent(InputEvent event);
585    }
586
587    private class MySessionCallback extends SessionCallback {
588        final String mInputId;
589        Uri mChannelUri;
590
591        MySessionCallback(String inputId, Uri channelUri) {
592            mInputId = inputId;
593            mChannelUri = channelUri;
594        }
595
596        @Override
597        public void onSessionCreated(Session session) {
598            if (this != mSessionCallback) {
599                // This callback is obsolete.
600                if (session != null) {
601                    session.release();
602                }
603                return;
604            }
605            mSession = session;
606            if (session != null) {
607                // mSurface may not be ready yet as soon as starting an application.
608                // In the case, we don't send Session.setSurface(null) unnecessarily.
609                // setSessionSurface will be called in surfaceCreated.
610                if (mSurface != null) {
611                    setSessionSurface(mSurface);
612                }
613                createSessionOverlayView();
614                mSession.tune(mChannelUri);
615                if (mHasStreamVolume) {
616                    mSession.setStreamVolume(mStreamVolume);
617                }
618            } else {
619                if (mListener != null) {
620                    mListener.onError(mInputId, ERROR_BUSY);
621                }
622            }
623        }
624
625        @Override
626        public void onSessionReleased(Session session) {
627            if (this == mSessionCallback) {
628                mSessionCallback = null;
629            }
630            mSession = null;
631            if (mListener != null) {
632                mListener.onError(mInputId, ERROR_TV_INPUT_DISCONNECTED);
633            }
634        }
635
636        @Override
637        public void onChannelRetuned(Session session, Uri channelUri) {
638            if (DEBUG) {
639                Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")");
640            }
641            if (mListener != null) {
642                mListener.onChannelRetuned(mInputId, channelUri);
643            }
644        }
645
646        @Override
647        public void onTrackInfoChanged(Session session, List<TvTrackInfo> tracks) {
648            if (this != mSessionCallback) {
649                return;
650            }
651            if (DEBUG) {
652                Log.d(TAG, "onTrackInfoChanged()");
653            }
654            updateVideoSize(tracks);
655            if (mListener != null) {
656                mListener.onTrackInfoChanged(mInputId, tracks);
657            }
658        }
659
660        @Override
661        public void onVideoAvailable(Session session) {
662            if (DEBUG) {
663                Log.d(TAG, "onVideoAvailable()");
664            }
665            if (mListener != null) {
666                mListener.onVideoAvailable(mInputId);
667            }
668        }
669
670        @Override
671        public void onVideoUnavailable(Session session, int reason) {
672            if (DEBUG) {
673                Log.d(TAG, "onVideoUnavailable(" + reason + ")");
674            }
675            if (mListener != null) {
676                mListener.onVideoUnavailable(mInputId, reason);
677            }
678        }
679
680        @Override
681        public void onContentBlocked(Session session, TvContentRating rating) {
682            if (DEBUG) {
683                Log.d(TAG, "onContentBlocked()");
684            }
685            if (mListener != null) {
686                mListener.onContentBlocked(mInputId, rating);
687            }
688        }
689
690        @Override
691        public void onSessionEvent(TvInputManager.Session session, String eventType,
692                Bundle eventArgs) {
693            if (this != mSessionCallback) {
694                return;
695            }
696            if (mListener != null) {
697                mListener.onEvent(mInputId, eventType, eventArgs);
698            }
699        }
700    }
701}
702