TvInputManager.java revision 6057102dbb746593a7d59cf377c969b62e38c664
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media.tv;
18
19import android.graphics.Rect;
20import android.net.Uri;
21import android.os.Bundle;
22import android.os.Handler;
23import android.os.IBinder;
24import android.os.Looper;
25import android.os.Message;
26import android.os.RemoteException;
27import android.util.ArrayMap;
28import android.util.Log;
29import android.util.Pools.Pool;
30import android.util.Pools.SimplePool;
31import android.util.SparseArray;
32import android.view.InputChannel;
33import android.view.InputEvent;
34import android.view.InputEventSender;
35import android.view.Surface;
36import android.view.View;
37
38import java.util.ArrayList;
39import java.util.Iterator;
40import java.util.LinkedList;
41import java.util.List;
42import java.util.Map;
43
44/**
45 * Central system API to the overall TV input framework (TIF) architecture, which arbitrates
46 * interaction between applications and the selected TV inputs.
47 */
48public final class TvInputManager {
49    private static final String TAG = "TvInputManager";
50
51    static final int VIDEO_UNAVAILABLE_REASON_START = 0;
52    static final int VIDEO_UNAVAILABLE_REASON_END = 3;
53
54    /**
55     * A generic reason. Video is not available due to an unspecified error.
56     */
57    public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START;
58    /**
59     * Video is not available because the TV input is tuning to another channel.
60     */
61    public static final int VIDEO_UNAVAILABLE_REASON_TUNE = 1;
62    /**
63     * Video is not available due to the weak TV signal.
64     */
65    public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2;
66    /**
67     * Video is not available because the TV input stopped the playback temporarily to buffer more
68     * data.
69     */
70    public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = VIDEO_UNAVAILABLE_REASON_END;
71
72    /**
73     * The TV input is connected.
74     * <p>
75     * State for {@link #getInputState} and {@link
76     * TvInputManager.TvInputListener#onInputStateChanged}.
77     * </p>
78     */
79    public static final int INPUT_STATE_CONNECTED = 0;
80    /**
81     * The TV input is connected but in standby mode. It would take a while until it becomes
82     * fully ready.
83     * <p>
84     * State for {@link #getInputState} and {@link
85     * TvInputManager.TvInputListener#onInputStateChanged}.
86     * </p>
87     */
88    public static final int INPUT_STATE_CONNECTED_STANDBY = 1;
89    /**
90     * The TV input is disconnected.
91     * <p>
92     * State for {@link #getInputState} and {@link
93     * TvInputManager.TvInputListener#onInputStateChanged}.
94     * </p>
95     */
96    public static final int INPUT_STATE_DISCONNECTED = 2;
97
98    private final ITvInputManager mService;
99
100    private final Object mLock = new Object();
101
102    // @GuardedBy(mLock)
103    private final List<TvInputListenerRecord> mTvInputListenerRecordsList =
104            new LinkedList<TvInputListenerRecord>();
105
106    // A mapping from TV input ID to the state of corresponding input.
107    // @GuardedBy(mLock)
108    private final Map<String, Integer> mStateMap = new ArrayMap<String, Integer>();
109
110    // A mapping from the sequence number of a session to its SessionCallbackRecord.
111    private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap =
112            new SparseArray<SessionCallbackRecord>();
113
114    // A sequence number for the next session to be created. Should be protected by a lock
115    // {@code mSessionCallbackRecordMap}.
116    private int mNextSeq;
117
118    private final ITvInputClient mClient;
119
120    private final ITvInputManagerCallback mCallback;
121
122    private final int mUserId;
123
124    /**
125     * Interface used to receive the created session.
126     * @hide
127     */
128    public abstract static class SessionCallback {
129        /**
130         * This is called after {@link TvInputManager#createSession} has been processed.
131         *
132         * @param session A {@link TvInputManager.Session} instance created. This can be
133         *            {@code null} if the creation request failed.
134         */
135        public void onSessionCreated(Session session) {
136        }
137
138        /**
139         * This is called when {@link TvInputManager.Session} is released.
140         * This typically happens when the process hosting the session has crashed or been killed.
141         *
142         * @param session A {@link TvInputManager.Session} instance released.
143         */
144        public void onSessionReleased(Session session) {
145        }
146
147        /**
148         * This is called when the channel of this session is changed by the underlying TV input
149         * with out any {@link TvInputManager.Session#tune(Uri)} request.
150         *
151         * @param session A {@link TvInputManager.Session} associated with this callback
152         * @param channelUri The URI of a channel.
153         */
154        public void onChannelRetuned(Session session, Uri channelUri) {
155        }
156
157        /**
158         * This is called when the track information of the session has been changed.
159         *
160         * @param session A {@link TvInputManager.Session} associated with this callback
161         * @param tracks A list which includes track information.
162         */
163        public void onTrackInfoChanged(Session session, List<TvTrackInfo> tracks) {
164        }
165
166        /**
167         * This is called when the video is available, so the TV input starts the playback.
168         *
169         * @param session A {@link TvInputManager.Session} associated with this callback
170         */
171        public void onVideoAvailable(Session session) {
172        }
173
174        /**
175         * This is called when the video is not available, so the TV input stops the playback.
176         *
177         * @param session A {@link TvInputManager.Session} associated with this callback
178         * @param reason The reason why the TV input stopped the playback:
179         * <ul>
180         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
181         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNE}
182         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
183         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
184         * </ul>
185         */
186        public void onVideoUnavailable(Session session, int reason) {
187        }
188
189        /**
190         * This is called when the current program content is blocked by parental controls.
191         *
192         * @param session A {@link TvInputManager.Session} associated with this callback
193         * @param rating The content ration of the blocked program.
194         */
195        public void onContentBlocked(Session session, TvContentRating rating) {
196        }
197
198        /**
199         * This is called when a custom event has been sent from this session.
200         *
201         * @param session A {@link TvInputManager.Session} associated with this callback
202         * @param eventType The type of the event.
203         * @param eventArgs Optional arguments of the event.
204         * @hide
205         */
206        public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
207        }
208    }
209
210    private static final class SessionCallbackRecord {
211        private final SessionCallback mSessionCallback;
212        private final Handler mHandler;
213        private Session mSession;
214
215        public SessionCallbackRecord(SessionCallback sessionCallback,
216                Handler handler) {
217            mSessionCallback = sessionCallback;
218            mHandler = handler;
219        }
220
221        public void postSessionCreated(final Session session) {
222            mSession = session;
223            mHandler.post(new Runnable() {
224                @Override
225                public void run() {
226                    mSessionCallback.onSessionCreated(session);
227                }
228            });
229        }
230
231        public void postSessionReleased() {
232            mHandler.post(new Runnable() {
233                @Override
234                public void run() {
235                    mSessionCallback.onSessionReleased(mSession);
236                }
237            });
238        }
239
240        public void postChannelRetuned(final Uri channelUri) {
241            mHandler.post(new Runnable() {
242                @Override
243                public void run() {
244                    mSessionCallback.onChannelRetuned(mSession, channelUri);
245                }
246            });
247        }
248
249        public void postTrackInfoChanged(final List<TvTrackInfo> tracks) {
250            mHandler.post(new Runnable() {
251                @Override
252                public void run() {
253                    mSession.setTracks(tracks);
254                    mSessionCallback.onTrackInfoChanged(mSession, tracks);
255                }
256            });
257        }
258
259        public void postVideoAvailable() {
260            mHandler.post(new Runnable() {
261                @Override
262                public void run() {
263                    mSessionCallback.onVideoAvailable(mSession);
264                }
265            });
266        }
267
268        public void postVideoUnavailable(final int reason) {
269            mHandler.post(new Runnable() {
270                @Override
271                public void run() {
272                    mSessionCallback.onVideoUnavailable(mSession, reason);
273                }
274            });
275        }
276
277        public void postContentBlocked(final TvContentRating rating) {
278            mHandler.post(new Runnable() {
279                @Override
280                public void run() {
281                    mSessionCallback.onContentBlocked(mSession, rating);
282                }
283            });
284        }
285
286        public void postSessionEvent(final String eventType, final Bundle eventArgs) {
287            mHandler.post(new Runnable() {
288                @Override
289                public void run() {
290                    mSessionCallback.onSessionEvent(mSession, eventType, eventArgs);
291                }
292            });
293        }
294    }
295
296    /**
297     * Interface used to monitor status of the TV input.
298     */
299    public abstract static class TvInputListener {
300        /**
301         * This is called when the state of a given TV input is changed.
302         *
303         * @param inputId the id of the TV input.
304         * @param state state of the TV input. The value is one of the following:
305         * <ul>
306         * <li>{@link TvInputManager#INPUT_STATE_CONNECTED}
307         * <li>{@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY}
308         * <li>{@link TvInputManager#INPUT_STATE_DISCONNECTED}
309         * </ul>
310         */
311        public void onInputStateChanged(String inputId, int state) {
312        }
313    }
314
315    private static final class TvInputListenerRecord {
316        private final TvInputListener mListener;
317        private final Handler mHandler;
318
319        public TvInputListenerRecord(TvInputListener listener, Handler handler) {
320            mListener = listener;
321            mHandler = handler;
322        }
323
324        public TvInputListener getListener() {
325            return mListener;
326        }
327
328        public void postStateChanged(final String inputId, final int state) {
329            mHandler.post(new Runnable() {
330                @Override
331                public void run() {
332                    mListener.onInputStateChanged(inputId, state);
333                }
334            });
335        }
336    }
337
338    /**
339     * @hide
340     */
341    public TvInputManager(ITvInputManager service, int userId) {
342        mService = service;
343        mUserId = userId;
344        mClient = new ITvInputClient.Stub() {
345            @Override
346            public void onSessionCreated(String inputId, IBinder token, InputChannel channel,
347                    int seq) {
348                synchronized (mSessionCallbackRecordMap) {
349                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
350                    if (record == null) {
351                        Log.e(TAG, "Callback not found for " + token);
352                        return;
353                    }
354                    Session session = null;
355                    if (token != null) {
356                        session = new Session(token, channel, mService, mUserId, seq,
357                                mSessionCallbackRecordMap);
358                    }
359                    record.postSessionCreated(session);
360                }
361            }
362
363            @Override
364            public void onSessionReleased(int seq) {
365                synchronized (mSessionCallbackRecordMap) {
366                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
367                    mSessionCallbackRecordMap.delete(seq);
368                    if (record == null) {
369                        Log.e(TAG, "Callback not found for seq:" + seq);
370                        return;
371                    }
372                    record.mSession.releaseInternal();
373                    record.postSessionReleased();
374                }
375            }
376
377            @Override
378            public void onChannelRetuned(Uri channelUri, int seq) {
379                synchronized (mSessionCallbackRecordMap) {
380                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
381                    if (record == null) {
382                        Log.e(TAG, "Callback not found for seq " + seq);
383                        return;
384                    }
385                    record.postChannelRetuned(channelUri);
386                }
387            }
388
389            @Override
390            public void onTrackInfoChanged(List<TvTrackInfo> tracks, int seq) {
391                synchronized (mSessionCallbackRecordMap) {
392                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
393                    if (record == null) {
394                        Log.e(TAG, "Callback not found for seq " + seq);
395                        return;
396                    }
397                    record.postTrackInfoChanged(tracks);
398                }
399            }
400
401            @Override
402            public void onVideoAvailable(int seq) {
403                synchronized (mSessionCallbackRecordMap) {
404                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
405                    if (record == null) {
406                        Log.e(TAG, "Callback not found for seq " + seq);
407                        return;
408                    }
409                    record.postVideoAvailable();
410                }
411            }
412
413            @Override
414            public void onVideoUnavailable(int reason, int seq) {
415                synchronized (mSessionCallbackRecordMap) {
416                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
417                    if (record == null) {
418                        Log.e(TAG, "Callback not found for seq " + seq);
419                        return;
420                    }
421                    record.postVideoUnavailable(reason);
422                }
423            }
424
425            @Override
426            public void onContentBlocked(String rating, int seq) {
427                synchronized (mSessionCallbackRecordMap) {
428                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
429                    if (record == null) {
430                        Log.e(TAG, "Callback not found for seq " + seq);
431                        return;
432                    }
433                    record.postContentBlocked(TvContentRating.unflattenFromString(rating));
434                }
435            }
436
437            @Override
438            public void onSessionEvent(String eventType, Bundle eventArgs, int seq) {
439                synchronized (mSessionCallbackRecordMap) {
440                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
441                    if (record == null) {
442                        Log.e(TAG, "Callback not found for seq " + seq);
443                        return;
444                    }
445                    record.postSessionEvent(eventType, eventArgs);
446                }
447            }
448        };
449        mCallback = new ITvInputManagerCallback.Stub() {
450            @Override
451            public void onInputStateChanged(String inputId, int state) {
452                synchronized (mLock) {
453                    mStateMap.put(inputId, state);
454                    for (TvInputListenerRecord record : mTvInputListenerRecordsList) {
455                        record.postStateChanged(inputId, state);
456                    }
457                }
458            }
459        };
460        try {
461            mService.registerCallback(mCallback, mUserId);
462        } catch (RemoteException e) {
463            Log.e(TAG, "mService.registerCallback failed: " + e);
464        }
465    }
466
467    /**
468     * Returns the complete list of TV inputs on the system.
469     *
470     * @return List of {@link TvInputInfo} for each TV input that describes its meta information.
471     */
472    public List<TvInputInfo> getTvInputList() {
473        try {
474            return mService.getTvInputList(mUserId);
475        } catch (RemoteException e) {
476            throw new RuntimeException(e);
477        }
478    }
479
480    /**
481     * Returns the state of a given TV input. It retuns one of the following:
482     * <ul>
483     * <li>{@link #INPUT_STATE_CONNECTED}
484     * <li>{@link #INPUT_STATE_CONNECTED_STANDBY}
485     * <li>{@link #INPUT_STATE_DISCONNECTED}
486     * </ul>
487     *
488     * @param inputId the id of the TV input.
489     * @throws IllegalArgumentException if the argument is {@code null} or if there is no
490     *        {@link TvInputInfo} corresponding to {@code inputId}.
491     */
492    public int getInputState(String inputId) {
493        if (inputId == null) {
494            throw new IllegalArgumentException("id cannot be null");
495        }
496        synchronized (mLock) {
497            Integer state = mStateMap.get(inputId);
498            if (state == null) {
499                throw new IllegalArgumentException("Unrecognized input ID: " + inputId);
500            }
501            return state.intValue();
502        }
503    }
504
505    /**
506     * Registers a {@link TvInputListener}.
507     *
508     * @param listener a listener used to monitor status of the TV inputs.
509     * @param handler a {@link Handler} that the status change will be delivered to.
510     * @throws IllegalArgumentException if any of the arguments is {@code null}.
511     */
512    public void registerListener(TvInputListener listener, Handler handler) {
513        if (listener == null) {
514            throw new IllegalArgumentException("listener cannot be null");
515        }
516        if (handler == null) {
517            throw new IllegalArgumentException("handler cannot be null");
518        }
519        synchronized (mLock) {
520            mTvInputListenerRecordsList.add(new TvInputListenerRecord(listener, handler));
521        }
522    }
523
524    /**
525     * Unregisters the existing {@link TvInputListener}.
526     *
527     * @param listener the existing listener to remove.
528     * @throws IllegalArgumentException if any of the arguments is {@code null}.
529     */
530    public void unregisterListener(final TvInputListener listener) {
531        if (listener == null) {
532            throw new IllegalArgumentException("listener cannot be null");
533        }
534        synchronized (mLock) {
535            for (Iterator<TvInputListenerRecord> it = mTvInputListenerRecordsList.iterator();
536                    it.hasNext(); ) {
537                TvInputListenerRecord record = it.next();
538                if (record.getListener() == listener) {
539                    it.remove();
540                    break;
541                }
542            }
543        }
544    }
545
546    /**
547     * Creates a {@link Session} for a given TV input.
548     * <p>
549     * The number of sessions that can be created at the same time is limited by the capability of
550     * the given TV input.
551     * </p>
552     *
553     * @param inputId the id of the TV input.
554     * @param callback a callback used to receive the created session.
555     * @param handler a {@link Handler} that the session creation will be delivered to.
556     * @throws IllegalArgumentException if any of the arguments is {@code null}.
557     * @hide
558     */
559    public void createSession(String inputId, final SessionCallback callback,
560            Handler handler) {
561        if (inputId == null) {
562            throw new IllegalArgumentException("id cannot be null");
563        }
564        if (callback == null) {
565            throw new IllegalArgumentException("callback cannot be null");
566        }
567        if (handler == null) {
568            throw new IllegalArgumentException("handler cannot be null");
569        }
570        SessionCallbackRecord record = new SessionCallbackRecord(callback, handler);
571        synchronized (mSessionCallbackRecordMap) {
572            int seq = mNextSeq++;
573            mSessionCallbackRecordMap.put(seq, record);
574            try {
575                mService.createSession(mClient, inputId, seq, mUserId);
576            } catch (RemoteException e) {
577                throw new RuntimeException(e);
578            }
579        }
580    }
581
582    /**
583     * The Session provides the per-session functionality of TV inputs.
584     * @hide
585     */
586    public static final class Session {
587        static final int DISPATCH_IN_PROGRESS = -1;
588        static final int DISPATCH_NOT_HANDLED = 0;
589        static final int DISPATCH_HANDLED = 1;
590
591        private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
592
593        private final ITvInputManager mService;
594        private final int mUserId;
595        private final int mSeq;
596
597        // For scheduling input event handling on the main thread. This also serves as a lock to
598        // protect pending input events and the input channel.
599        private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
600
601        private final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20);
602        private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20);
603        private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
604
605        private IBinder mToken;
606        private TvInputEventSender mSender;
607        private InputChannel mChannel;
608        private List<TvTrackInfo> mTracks;
609
610        /** @hide */
611        private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
612                int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
613            mToken = token;
614            mChannel = channel;
615            mService = service;
616            mUserId = userId;
617            mSeq = seq;
618            mSessionCallbackRecordMap = sessionCallbackRecordMap;
619        }
620
621        /**
622         * Releases this session.
623         */
624        public void release() {
625            if (mToken == null) {
626                Log.w(TAG, "The session has been already released");
627                return;
628            }
629            try {
630                mService.releaseSession(mToken, mUserId);
631            } catch (RemoteException e) {
632                throw new RuntimeException(e);
633            }
634
635            releaseInternal();
636        }
637
638        /**
639         * Sets the {@link android.view.Surface} for this session.
640         *
641         * @param surface A {@link android.view.Surface} used to render video.
642         * @hide
643         */
644        public void setSurface(Surface surface) {
645            if (mToken == null) {
646                Log.w(TAG, "The session has been already released");
647                return;
648            }
649            // surface can be null.
650            try {
651                mService.setSurface(mToken, surface, mUserId);
652            } catch (RemoteException e) {
653                throw new RuntimeException(e);
654            }
655        }
656
657        /**
658         * Sets the relative stream volume of this session to handle a change of audio focus.
659         *
660         * @param volume A volume value between 0.0f to 1.0f.
661         * @throws IllegalArgumentException if the volume value is out of range.
662         */
663        public void setStreamVolume(float volume) {
664            if (mToken == null) {
665                Log.w(TAG, "The session has been already released");
666                return;
667            }
668            try {
669                if (volume < 0.0f || volume > 1.0f) {
670                    throw new IllegalArgumentException("volume should be between 0.0f and 1.0f");
671                }
672                mService.setVolume(mToken, volume, mUserId);
673            } catch (RemoteException e) {
674                throw new RuntimeException(e);
675            }
676        }
677
678        /**
679         * Tunes to a given channel.
680         *
681         * @param channelUri The URI of a channel.
682         * @throws IllegalArgumentException if the argument is {@code null}.
683         */
684        public void tune(Uri channelUri) {
685            if (channelUri == null) {
686                throw new IllegalArgumentException("channelUri cannot be null");
687            }
688            if (mToken == null) {
689                Log.w(TAG, "The session has been already released");
690                return;
691            }
692            mTracks = null;
693            try {
694                mService.tune(mToken, channelUri, mUserId);
695            } catch (RemoteException e) {
696                throw new RuntimeException(e);
697            }
698        }
699
700        /**
701         * Enables or disables the caption for this session.
702         *
703         * @param enabled {@code true} to enable, {@code false} to disable.
704         */
705        public void setCaptionEnabled(boolean enabled) {
706            if (mToken == null) {
707                Log.w(TAG, "The session has been already released");
708                return;
709            }
710            try {
711                mService.setCaptionEnabled(mToken, enabled, mUserId);
712            } catch (RemoteException e) {
713                throw new RuntimeException(e);
714            }
715        }
716
717        /**
718         * Select a track.
719         *
720         * @param track the track to be selected.
721         * @see #getTracks()
722         */
723        public void selectTrack(TvTrackInfo track) {
724            if (track == null) {
725                throw new IllegalArgumentException("track cannot be null");
726            }
727            if (mToken == null) {
728                Log.w(TAG, "The session has been already released");
729                return;
730            }
731            try {
732                mService.selectTrack(mToken, track, mUserId);
733            } catch (RemoteException e) {
734                throw new RuntimeException(e);
735            }
736        }
737
738        /**
739         * Unselect a track.
740         *
741         * @param track the track to be selected.
742         * @see #getTracks()
743         */
744        public void unselectTrack(TvTrackInfo track) {
745            if (track == null) {
746                throw new IllegalArgumentException("track cannot be null");
747            }
748            if (mToken == null) {
749                Log.w(TAG, "The session has been already released");
750                return;
751            }
752            try {
753                mService.unselectTrack(mToken, track, mUserId);
754            } catch (RemoteException e) {
755                throw new RuntimeException(e);
756            }
757        }
758
759        /**
760         * Returns a list which includes track information. May return {@code null} if the
761         * information is not available.
762         * @see #selectTrack(TvTrackInfo)
763         * @see #unselectTrack(TvTrackInfo)
764         */
765        public List<TvTrackInfo> getTracks() {
766            if (mTracks == null) {
767                return null;
768            }
769            return new ArrayList<TvTrackInfo>(mTracks);
770        }
771
772        private void setTracks(List<TvTrackInfo> tracks) {
773            mTracks = tracks;
774        }
775
776        /**
777         * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView}
778         * should be called whenever the layout of its containing view is changed.
779         * {@link #removeOverlayView()} should be called to remove the overlay view.
780         * Since a session can have only one overlay view, this method should be called only once
781         * or it can be called again after calling {@link #removeOverlayView()}.
782         *
783         * @param view A view playing TV.
784         * @param frame A position of the overlay view.
785         * @throws IllegalArgumentException if any of the arguments is {@code null}.
786         * @throws IllegalStateException if {@code view} is not attached to a window.
787         */
788        void createOverlayView(View view, Rect frame) {
789            if (view == null) {
790                throw new IllegalArgumentException("view cannot be null");
791            }
792            if (frame == null) {
793                throw new IllegalArgumentException("frame cannot be null");
794            }
795            if (view.getWindowToken() == null) {
796                throw new IllegalStateException("view must be attached to a window");
797            }
798            if (mToken == null) {
799                Log.w(TAG, "The session has been already released");
800                return;
801            }
802            try {
803                mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId);
804            } catch (RemoteException e) {
805                throw new RuntimeException(e);
806            }
807        }
808
809        /**
810         * Relayouts the current overlay view.
811         *
812         * @param frame A new position of the overlay view.
813         * @throws IllegalArgumentException if the arguments is {@code null}.
814         */
815        void relayoutOverlayView(Rect frame) {
816            if (frame == null) {
817                throw new IllegalArgumentException("frame cannot be null");
818            }
819            if (mToken == null) {
820                Log.w(TAG, "The session has been already released");
821                return;
822            }
823            try {
824                mService.relayoutOverlayView(mToken, frame, mUserId);
825            } catch (RemoteException e) {
826                throw new RuntimeException(e);
827            }
828        }
829
830        /**
831         * Removes the current overlay view.
832         */
833        void removeOverlayView() {
834            if (mToken == null) {
835                Log.w(TAG, "The session has been already released");
836                return;
837            }
838            try {
839                mService.removeOverlayView(mToken, mUserId);
840            } catch (RemoteException e) {
841                throw new RuntimeException(e);
842            }
843        }
844
845        /**
846         * Dispatches an input event to this session.
847         *
848         * @param event {@link InputEvent} to dispatch.
849         * @param token A token used to identify the input event later in the callback.
850         * @param callback A callback used to receive the dispatch result.
851         * @param handler {@link Handler} that the dispatch result will be delivered to.
852         * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
853         *         {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
854         *         {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
855         *         be invoked later.
856         * @throws IllegalArgumentException if any of the necessary arguments is {@code null}.
857         * @hide
858         */
859        public int dispatchInputEvent(InputEvent event, Object token,
860                FinishedInputEventCallback callback, Handler handler) {
861            if (event == null) {
862                throw new IllegalArgumentException("event cannot be null");
863            }
864            if (callback != null && handler == null) {
865                throw new IllegalArgumentException("handler cannot be null");
866            }
867            synchronized (mHandler) {
868                if (mChannel == null) {
869                    return DISPATCH_NOT_HANDLED;
870                }
871                PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
872                if (Looper.myLooper() == Looper.getMainLooper()) {
873                    // Already running on the main thread so we can send the event immediately.
874                    return sendInputEventOnMainLooperLocked(p);
875                }
876
877                // Post the event to the main thread.
878                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
879                msg.setAsynchronous(true);
880                mHandler.sendMessage(msg);
881                return DISPATCH_IN_PROGRESS;
882            }
883        }
884
885        /**
886         * Callback that is invoked when an input event that was dispatched to this session has been
887         * finished.
888         *
889         * @hide
890         */
891        public interface FinishedInputEventCallback {
892            /**
893             * Called when the dispatched input event is finished.
894             *
895             * @param token a token passed to {@link #dispatchInputEvent}.
896             * @param handled {@code true} if the dispatched input event was handled properly.
897             *            {@code false} otherwise.
898             */
899            public void onFinishedInputEvent(Object token, boolean handled);
900        }
901
902        // Must be called on the main looper
903        private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
904            synchronized (mHandler) {
905                int result = sendInputEventOnMainLooperLocked(p);
906                if (result == DISPATCH_IN_PROGRESS) {
907                    return;
908                }
909            }
910
911            invokeFinishedInputEventCallback(p, false);
912        }
913
914        private int sendInputEventOnMainLooperLocked(PendingEvent p) {
915            if (mChannel != null) {
916                if (mSender == null) {
917                    mSender = new TvInputEventSender(mChannel, mHandler.getLooper());
918                }
919
920                final InputEvent event = p.mEvent;
921                final int seq = event.getSequenceNumber();
922                if (mSender.sendInputEvent(seq, event)) {
923                    mPendingEvents.put(seq, p);
924                    Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
925                    msg.setAsynchronous(true);
926                    mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
927                    return DISPATCH_IN_PROGRESS;
928                }
929
930                Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
931                        + event);
932            }
933            return DISPATCH_NOT_HANDLED;
934        }
935
936        void finishedInputEvent(int seq, boolean handled, boolean timeout) {
937            final PendingEvent p;
938            synchronized (mHandler) {
939                int index = mPendingEvents.indexOfKey(seq);
940                if (index < 0) {
941                    return; // spurious, event already finished or timed out
942                }
943
944                p = mPendingEvents.valueAt(index);
945                mPendingEvents.removeAt(index);
946
947                if (timeout) {
948                    Log.w(TAG, "Timeout waiting for seesion to handle input event after "
949                            + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
950                } else {
951                    mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
952                }
953            }
954
955            invokeFinishedInputEventCallback(p, handled);
956        }
957
958        // Assumes the event has already been removed from the queue.
959        void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
960            p.mHandled = handled;
961            if (p.mHandler.getLooper().isCurrentThread()) {
962                // Already running on the callback handler thread so we can send the callback
963                // immediately.
964                p.run();
965            } else {
966                // Post the event to the callback handler thread.
967                // In this case, the callback will be responsible for recycling the event.
968                Message msg = Message.obtain(p.mHandler, p);
969                msg.setAsynchronous(true);
970                msg.sendToTarget();
971            }
972        }
973
974        private void flushPendingEventsLocked() {
975            mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
976
977            final int count = mPendingEvents.size();
978            for (int i = 0; i < count; i++) {
979                int seq = mPendingEvents.keyAt(i);
980                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
981                msg.setAsynchronous(true);
982                msg.sendToTarget();
983            }
984        }
985
986        private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
987                FinishedInputEventCallback callback, Handler handler) {
988            PendingEvent p = mPendingEventPool.acquire();
989            if (p == null) {
990                p = new PendingEvent();
991            }
992            p.mEvent = event;
993            p.mToken = token;
994            p.mCallback = callback;
995            p.mHandler = handler;
996            return p;
997        }
998
999        private void recyclePendingEventLocked(PendingEvent p) {
1000            p.recycle();
1001            mPendingEventPool.release(p);
1002        }
1003
1004        private void releaseInternal() {
1005            mToken = null;
1006            synchronized (mHandler) {
1007                if (mChannel != null) {
1008                    if (mSender != null) {
1009                        flushPendingEventsLocked();
1010                        mSender.dispose();
1011                        mSender = null;
1012                    }
1013                    mChannel.dispose();
1014                    mChannel = null;
1015                }
1016            }
1017            synchronized (mSessionCallbackRecordMap) {
1018                mSessionCallbackRecordMap.remove(mSeq);
1019            }
1020        }
1021
1022        private final class InputEventHandler extends Handler {
1023            public static final int MSG_SEND_INPUT_EVENT = 1;
1024            public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
1025            public static final int MSG_FLUSH_INPUT_EVENT = 3;
1026
1027            InputEventHandler(Looper looper) {
1028                super(looper, null, true);
1029            }
1030
1031            @Override
1032            public void handleMessage(Message msg) {
1033                switch (msg.what) {
1034                    case MSG_SEND_INPUT_EVENT: {
1035                        sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
1036                        return;
1037                    }
1038                    case MSG_TIMEOUT_INPUT_EVENT: {
1039                        finishedInputEvent(msg.arg1, false, true);
1040                        return;
1041                    }
1042                    case MSG_FLUSH_INPUT_EVENT: {
1043                        finishedInputEvent(msg.arg1, false, false);
1044                        return;
1045                    }
1046                }
1047            }
1048        }
1049
1050        private final class TvInputEventSender extends InputEventSender {
1051            public TvInputEventSender(InputChannel inputChannel, Looper looper) {
1052                super(inputChannel, looper);
1053            }
1054
1055            @Override
1056            public void onInputEventFinished(int seq, boolean handled) {
1057                finishedInputEvent(seq, handled, false);
1058            }
1059        }
1060
1061        private final class PendingEvent implements Runnable {
1062            public InputEvent mEvent;
1063            public Object mToken;
1064            public FinishedInputEventCallback mCallback;
1065            public Handler mHandler;
1066            public boolean mHandled;
1067
1068            public void recycle() {
1069                mEvent = null;
1070                mToken = null;
1071                mCallback = null;
1072                mHandler = null;
1073                mHandled = false;
1074            }
1075
1076            @Override
1077            public void run() {
1078                mCallback.onFinishedInputEvent(mToken, mHandled);
1079
1080                synchronized (mHandler) {
1081                    recyclePendingEventLocked(this);
1082                }
1083            }
1084        }
1085    }
1086}
1087