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