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