TvView.java revision 336cdf20dd44ee93b5173be73e26e966a2609eb0
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.ViewRootImpl;
37
38/**
39 * View playing TV
40 */
41public class TvView extends SurfaceView {
42    private static final String TAG = "TvView";
43    // STOPSHIP: Turn debugging off.
44    private static final boolean DEBUG = true;
45
46    /**
47     * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the requested TV
48     * input is busy and unable to handle the request.
49     */
50    public static final int ERROR_BUSY = 0;
51
52    /**
53     * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the underlying TV
54     * input has been disconnected.
55     */
56    public static final int ERROR_TV_INPUT_DISCONNECTED = 1;
57
58    private final Handler mHandler = new Handler();
59    private TvInputManager.Session mSession;
60    private Surface mSurface;
61    private boolean mOverlayViewCreated;
62    private Rect mOverlayViewFrame;
63    private final TvInputManager mTvInputManager;
64    private MySessionCallback mSessionCallback;
65    private TvInputListener mListener;
66    private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
67    private boolean mHasStreamVolume;
68    private float mStreamVolume;
69
70    private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
71        @Override
72        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
73            Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width
74                    + ", height=" + height + ")");
75            if (holder.getSurface() == mSurface) {
76                return;
77            }
78            mSurface = holder.getSurface();
79            setSessionSurface(mSurface);
80        }
81
82        @Override
83        public void surfaceCreated(SurfaceHolder holder) {
84            mSurface = holder.getSurface();
85            setSessionSurface(mSurface);
86        }
87
88        @Override
89        public void surfaceDestroyed(SurfaceHolder holder) {
90            mSurface = null;
91            setSessionSurface(null);
92        }
93    };
94
95    private final FinishedInputEventCallback mFinishedInputEventCallback =
96            new FinishedInputEventCallback() {
97        @Override
98        public void onFinishedInputEvent(Object token, boolean handled) {
99            if (DEBUG) {
100                Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")");
101            }
102            if (handled) {
103                return;
104            }
105            // TODO: Re-order unhandled events.
106            InputEvent event = (InputEvent) token;
107            if (dispatchUnhandledInputEvent(event)) {
108                return;
109            }
110            ViewRootImpl viewRootImpl = getViewRootImpl();
111            if (viewRootImpl != null) {
112                viewRootImpl.dispatchUnhandledInputEvent(event);
113            }
114        }
115    };
116
117    public TvView(Context context) {
118        this(context, null, 0);
119    }
120
121    public TvView(Context context, AttributeSet attrs) {
122        this(context, attrs, 0);
123    }
124
125    public TvView(Context context, AttributeSet attrs, int defStyleAttr) {
126        super(context, attrs, defStyleAttr);
127        getHolder().addCallback(mSurfaceHolderCallback);
128        mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE);
129    }
130
131    /**
132     * Sets a listener for events in this TvView.
133     *
134     * @param listener The listener to be called with events. A value of {@code null} removes any
135     *         existing listener.
136     */
137    public void setTvInputListener(TvInputListener listener) {
138        mListener = listener;
139    }
140
141    /**
142     * Sets the relative stream volume of this session to handle a change of audio focus.
143     *
144     * @param volume A volume value between 0.0f to 1.0f.
145     */
146    public void setStreamVolume(float volume) {
147        if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")");
148        mHasStreamVolume = true;
149        mStreamVolume = volume;
150        if (mSession == null) {
151            // Volume will be set once the connection has been made.
152            return;
153        }
154        mSession.setStreamVolume(volume);
155    }
156
157    /**
158     * Tunes to a given channel.
159     *
160     * @param inputId the id of TV input which will play the given channel.
161     * @param channelUri The URI of a channel.
162     */
163    public void tune(String inputId, Uri channelUri) {
164        if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")");
165        if (TextUtils.isEmpty(inputId)) {
166            throw new IllegalArgumentException("inputId cannot be null or an empty string");
167        }
168        if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) {
169            if (mSession != null) {
170                mSession.tune(channelUri);
171            } else {
172                // Session is not created yet. Replace the channel which will be set once the
173                // session is made.
174                mSessionCallback.mChannelUri = channelUri;
175            }
176        } else {
177            if (mSession != null) {
178                release();
179            }
180            // When createSession() is called multiple times before the callback is called,
181            // only the callback of the last createSession() call will be actually called back.
182            // The previous callbacks will be ignored. For the logic, mSessionCallback
183            // is newly assigned for every createSession request and compared with
184            // MySessionCreateCallback.this.
185            mSessionCallback = new MySessionCallback(inputId, channelUri);
186            mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
187        }
188    }
189
190    /**
191     * Resets this TvView.
192     * <p>
193     * This method is primarily used to un-tune the current TvView.
194     */
195    public void reset() {
196        if (mSession != null) {
197            release();
198        }
199    }
200
201    /**
202     * Dispatches an unhandled input event to the next receiver.
203     * <p>
204     * Except system keys, TvView always consumes input events in the normal flow. This is called
205     * asynchronously from where the event is dispatched. It gives the host application a chance to
206     * dispatch the unhandled input events.
207     *
208     * @param event The input event.
209     * @return {@code true} if the event was handled by the view, {@code false} otherwise.
210     */
211    public boolean dispatchUnhandledInputEvent(InputEvent event) {
212        if (mOnUnhandledInputEventListener != null) {
213            if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
214                return true;
215            }
216        }
217        return onUnhandledInputEvent(event);
218    }
219
220    /**
221     * Called when an unhandled input event was also not handled by the user provided callback. This
222     * is the last chance to handle the unhandled input event in the TvView.
223     *
224     * @param event The input event.
225     * @return If you handled the event, return {@code true}. If you want to allow the event to be
226     *         handled by the next receiver, return {@code false}.
227     */
228    public boolean onUnhandledInputEvent(InputEvent event) {
229        return false;
230    }
231
232    /**
233     * Registers a callback to be invoked when an input event was not handled by the bound TV input.
234     *
235     * @param listener The callback to invoke when the unhandled input event was received.
236     */
237    public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
238        mOnUnhandledInputEventListener = listener;
239    }
240
241    @Override
242    public boolean dispatchKeyEvent(KeyEvent event) {
243        if (super.dispatchKeyEvent(event)) {
244            return true;
245        }
246        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
247        if (mSession == null) {
248            return false;
249        }
250        InputEvent copiedEvent = event.copy();
251        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
252                mHandler);
253        return ret != Session.DISPATCH_NOT_HANDLED;
254    }
255
256    @Override
257    public boolean dispatchTouchEvent(MotionEvent event) {
258        if (super.dispatchTouchEvent(event)) {
259            return true;
260        }
261        if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
262        if (mSession == null) {
263            return false;
264        }
265        InputEvent copiedEvent = event.copy();
266        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
267                mHandler);
268        return ret != Session.DISPATCH_NOT_HANDLED;
269    }
270
271    @Override
272    public boolean dispatchTrackballEvent(MotionEvent event) {
273        if (super.dispatchTrackballEvent(event)) {
274            return true;
275        }
276        if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
277        if (mSession == null) {
278            return false;
279        }
280        InputEvent copiedEvent = event.copy();
281        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
282                mHandler);
283        return ret != Session.DISPATCH_NOT_HANDLED;
284    }
285
286    @Override
287    public boolean dispatchGenericMotionEvent(MotionEvent event) {
288        if (super.dispatchGenericMotionEvent(event)) {
289            return true;
290        }
291        if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
292        if (mSession == null) {
293            return false;
294        }
295        InputEvent copiedEvent = event.copy();
296        int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
297                mHandler);
298        return ret != Session.DISPATCH_NOT_HANDLED;
299    }
300
301    @Override
302    protected void onAttachedToWindow() {
303        super.onAttachedToWindow();
304        createSessionOverlayView();
305    }
306
307    @Override
308    protected void onDetachedFromWindow() {
309        removeSessionOverlayView();
310        super.onDetachedFromWindow();
311    }
312
313    /** @hide */
314    @Override
315    protected void updateWindow(boolean force, boolean redrawNeeded) {
316        super.updateWindow(force, redrawNeeded);
317        relayoutSessionOverlayView();
318    }
319
320    private void release() {
321        setSessionSurface(null);
322        removeSessionOverlayView();
323        mSession.release();
324        mSession = null;
325        mSessionCallback = null;
326    }
327
328    private void setSessionSurface(Surface surface) {
329        if (mSession == null) {
330            return;
331        }
332        mSession.setSurface(surface);
333    }
334
335    private void createSessionOverlayView() {
336        if (mSession == null || !isAttachedToWindow()
337                || mOverlayViewCreated) {
338            return;
339        }
340        mOverlayViewFrame = getViewFrameOnScreen();
341        mSession.createOverlayView(this, mOverlayViewFrame);
342        mOverlayViewCreated = true;
343    }
344
345    private void removeSessionOverlayView() {
346        if (mSession == null || !mOverlayViewCreated) {
347            return;
348        }
349        mSession.removeOverlayView();
350        mOverlayViewCreated = false;
351        mOverlayViewFrame = null;
352    }
353
354    private void relayoutSessionOverlayView() {
355        if (mSession == null || !isAttachedToWindow()
356                || !mOverlayViewCreated) {
357            return;
358        }
359        Rect viewFrame = getViewFrameOnScreen();
360        if (viewFrame.equals(mOverlayViewFrame)) {
361            return;
362        }
363        mSession.relayoutOverlayView(viewFrame);
364        mOverlayViewFrame = viewFrame;
365    }
366
367    private Rect getViewFrameOnScreen() {
368        int[] location = new int[2];
369        getLocationOnScreen(location);
370        return new Rect(location[0], location[1],
371                location[0] + getWidth(), location[1] + getHeight());
372    }
373
374    /**
375     * Interface used to receive various status updates on the {@link TvView}.
376     */
377    public abstract static class TvInputListener {
378
379        /**
380         * This is invoked when an error occurred while handling requested operation.
381         *
382         * @param inputId The ID of the TV input bound to this view.
383         * @param errorCode The error code. For the details of error code, please see
384         *         {@link TvView}.
385         */
386        public void onError(String inputId, int errorCode) {
387        }
388
389        /**
390         * This is invoked when the view is tuned to a specific channel and starts decoding video
391         * stream from there. It is also called later when the video format is changed.
392         *
393         * @param inputId The ID of the TV input bound to this view.
394         * @param width The width of the video.
395         * @param height The height of the video.
396         * @param interlaced {@code true} if the video is interlaced, {@code false} if the video is
397         *            progressive.
398         * @hide
399         */
400        public void onVideoStreamChanged(String inputId, int width, int height,
401                boolean interlaced) {
402        }
403
404        /**
405         * This is invoked when the view is tuned to a specific channel and starts decoding audio
406         * stream from there. It is also called later when the audio format is changed.
407         *
408         * @param inputId The ID of the TV input bound to this view.
409         * @param channelCount The number of channels in the audio stream.
410         * @hide
411         */
412        public void onAudioStreamChanged(String inputId, int channelCount) {
413        }
414
415        /**
416         * This is invoked when the view is tuned to a specific channel and starts decoding data
417         * stream that includes subtitle information from the channel. It is also called later when
418         * the information disappears or appears.
419         *
420         * @param inputId The ID of the TV input bound to this view.
421         * @param hasClosedCaption {@code true} if the stream contains closed caption, {@code false}
422         *            otherwise.
423         * @hide
424         */
425        public void onClosedCaptionStreamChanged(String inputId, boolean hasClosedCaption) {
426        }
427
428        /**
429         * This is invoked when a custom event from the bound TV input is sent to this view.
430         *
431         * @param eventType The type of the event.
432         * @param eventArgs Optional arguments of the event.
433         * @hide
434         */
435        public void onEvent(String inputId, String eventType, Bundle eventArgs) {
436        }
437    }
438
439    /**
440     * Interface definition for a callback to be invoked when the unhandled input event is received.
441     */
442    public interface OnUnhandledInputEventListener {
443        /**
444         * Called when an input event was not handled by the bound TV input.
445         * <p>
446         * This is called asynchronously from where the event is dispatched. It gives the host
447         * application a chance to handle the unhandled input events.
448         *
449         * @param event The input event.
450         * @return If you handled the event, return {@code true}. If you want to allow the event to
451         *         be handled by the next receiver, return {@code false}.
452         */
453        boolean onUnhandledInputEvent(InputEvent event);
454    }
455
456    private class MySessionCallback extends SessionCallback {
457        final String mInputId;
458        Uri mChannelUri;
459
460        MySessionCallback(String inputId, Uri channelUri) {
461            mInputId = inputId;
462            mChannelUri = channelUri;
463        }
464
465        @Override
466        public void onSessionCreated(Session session) {
467            if (this != mSessionCallback) {
468                // This callback is obsolete.
469                if (session != null) {
470                    session.release();
471                }
472                return;
473            }
474            mSession = session;
475            if (session != null) {
476                // mSurface may not be ready yet as soon as starting an application.
477                // In the case, we don't send Session.setSurface(null) unnecessarily.
478                // setSessionSurface will be called in surfaceCreated.
479                if (mSurface != null) {
480                    setSessionSurface(mSurface);
481                }
482                createSessionOverlayView();
483                mSession.tune(mChannelUri);
484                if (mHasStreamVolume) {
485                    mSession.setStreamVolume(mStreamVolume);
486                }
487            } else {
488                if (mListener != null) {
489                    mListener.onError(mInputId, ERROR_BUSY);
490                }
491            }
492        }
493
494        @Override
495        public void onSessionReleased(Session session) {
496            mSession = null;
497            mSessionCallback = null;
498            if (mListener != null) {
499                mListener.onError(mInputId, ERROR_TV_INPUT_DISCONNECTED);
500            }
501        }
502
503        @Override
504        public void onVideoStreamChanged(Session session, int width, int height,
505                boolean interlaced) {
506            if (DEBUG) {
507                Log.d(TAG, "onVideoSizeChanged(" + width + ", " + height + ")");
508            }
509            if (mListener != null) {
510                mListener.onVideoStreamChanged(mInputId, width, height, interlaced);
511            }
512        }
513
514        @Override
515        public void onAudioStreamChanged(Session session, int channelCount) {
516            if (DEBUG) {
517                Log.d(TAG, "onAudioStreamChanged(" + channelCount + ")");
518            }
519            if (mListener != null) {
520                mListener.onAudioStreamChanged(mInputId, channelCount);
521            }
522        }
523
524        @Override
525        public void onClosedCaptionStreamChanged(Session session, boolean hasClosedCaption) {
526            if (DEBUG) {
527                Log.d(TAG, "onClosedCaptionStreamChanged(" + hasClosedCaption + ")");
528            }
529            if (mListener != null) {
530                mListener.onClosedCaptionStreamChanged(mInputId, hasClosedCaption);
531            }
532        }
533
534        @Override
535        public void onSessionEvent(TvInputManager.Session session, String eventType,
536                Bundle eventArgs) {
537            if (mListener != null) {
538                mListener.onEvent(mInputId, eventType, eventArgs);
539            }
540        }
541    }
542}
543