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