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