TvInputManager.java revision e377ea5de67aaca36c86ac8971ce0a9126c5af20
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media.tv;
18
19import android.annotation.SystemApi;
20import android.graphics.Rect;
21import android.net.Uri;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.IBinder;
25import android.os.Looper;
26import android.os.Message;
27import android.os.RemoteException;
28import android.util.ArrayMap;
29import android.util.Log;
30import android.util.Pools.Pool;
31import android.util.Pools.SimplePool;
32import android.util.SparseArray;
33import android.view.InputChannel;
34import android.view.InputEvent;
35import android.view.InputEventSender;
36import android.view.Surface;
37import android.view.View;
38
39import java.util.ArrayList;
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    static final int VIDEO_UNAVAILABLE_REASON_START = 0;
53    static final int VIDEO_UNAVAILABLE_REASON_END = 3;
54
55    /**
56     * A generic reason. Video is not available due to an unspecified error.
57     */
58    public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START;
59    /**
60     * Video is not available because the TV input is in the middle of tuning to a new channel.
61     */
62    public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1;
63    /**
64     * Video is not available due to the weak TV signal.
65     */
66    public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2;
67    /**
68     * Video is not available because the TV input stopped the playback temporarily to buffer more
69     * data.
70     */
71    public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = VIDEO_UNAVAILABLE_REASON_END;
72
73    /**
74     * The TV input is connected.
75     * <p>
76     * State for {@link #getInputState} and {@link
77     * TvInputManager.TvInputListener#onInputStateChanged}.
78     * </p>
79     */
80    public static final int INPUT_STATE_CONNECTED = 0;
81    /**
82     * The TV input is connected but in standby mode. It would take a while until it becomes
83     * fully ready.
84     * <p>
85     * State for {@link #getInputState} and {@link
86     * TvInputManager.TvInputListener#onInputStateChanged}.
87     * </p>
88     */
89    public static final int INPUT_STATE_CONNECTED_STANDBY = 1;
90    /**
91     * The TV input is disconnected.
92     * <p>
93     * State for {@link #getInputState} and {@link
94     * TvInputManager.TvInputListener#onInputStateChanged}.
95     * </p>
96     */
97    public static final int INPUT_STATE_DISCONNECTED = 2;
98
99    /**
100     * Broadcast intent action when the user blocked content ratings change. For use with the
101     * {@link #isRatingBlocked}.
102     */
103    public static final String ACTION_BLOCKED_RATINGS_CHANGED =
104            "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
105
106    /**
107     * Broadcast intent action when the parental controls enabled state changes. For use with the
108     * {@link #isParentalControlsEnabled}.
109     */
110    public static final String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED =
111            "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
112
113    private final ITvInputManager mService;
114
115    private final Object mLock = new Object();
116
117    // @GuardedBy(mLock)
118    private final List<TvInputListenerRecord> mTvInputListenerRecordsList =
119            new LinkedList<TvInputListenerRecord>();
120
121    // A mapping from TV input ID to the state of corresponding input.
122    // @GuardedBy(mLock)
123    private final Map<String, Integer> mStateMap = new ArrayMap<String, Integer>();
124
125    // A mapping from the sequence number of a session to its SessionCallbackRecord.
126    private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap =
127            new SparseArray<SessionCallbackRecord>();
128
129    // A sequence number for the next session to be created. Should be protected by a lock
130    // {@code mSessionCallbackRecordMap}.
131    private int mNextSeq;
132
133    private final ITvInputClient mClient;
134
135    private final ITvInputManagerCallback mCallback;
136
137    private final int mUserId;
138
139    /**
140     * Interface used to receive the created session.
141     * @hide
142     */
143    @SystemApi
144    public abstract static class SessionCallback {
145        /**
146         * This is called after {@link TvInputManager#createSession} has been processed.
147         *
148         * @param session A {@link TvInputManager.Session} instance created. This can be
149         *            {@code null} if the creation request failed.
150         */
151        public void onSessionCreated(Session session) {
152        }
153
154        /**
155         * This is called when {@link TvInputManager.Session} is released.
156         * This typically happens when the process hosting the session has crashed or been killed.
157         *
158         * @param session A {@link TvInputManager.Session} instance released.
159         */
160        public void onSessionReleased(Session session) {
161        }
162
163        /**
164         * This is called when the channel of this session is changed by the underlying TV input
165         * with out any {@link TvInputManager.Session#tune(Uri)} request.
166         *
167         * @param session A {@link TvInputManager.Session} associated with this callback.
168         * @param channelUri The URI of a channel.
169         */
170        public void onChannelRetuned(Session session, Uri channelUri) {
171        }
172
173        /**
174         * This is called when the track information of the session has been changed.
175         *
176         * @param session A {@link TvInputManager.Session} associated with this callback.
177         * @param tracks A list which includes track information.
178         */
179        public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
180        }
181
182        /**
183         * This is called when a track for a given type is selected.
184         *
185         * @param session A {@link TvInputManager.Session} associated with this callback
186         * @param type The type of the selected track. The type can be
187         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
188         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
189         * @param trackId The ID of the selected track. When {@code null} the currently selected
190         *            track for a given type should be unselected.
191         */
192        public void onTrackSelected(Session session, int type, String trackId) {
193        }
194
195        /**
196         * This is called when the video is available, so the TV input starts the playback.
197         *
198         * @param session A {@link TvInputManager.Session} associated with this callback.
199         */
200        public void onVideoAvailable(Session session) {
201        }
202
203        /**
204         * This is called when the video is not available, so the TV input stops the playback.
205         *
206         * @param session A {@link TvInputManager.Session} associated with this callback
207         * @param reason The reason why the TV input stopped the playback:
208         * <ul>
209         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
210         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
211         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
212         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
213         * </ul>
214         */
215        public void onVideoUnavailable(Session session, int reason) {
216        }
217
218        /**
219         * This is called when the current program content turns out to be allowed to watch since
220         * its content rating is not blocked by parental controls.
221         *
222         * @param session A {@link TvInputManager.Session} associated with this callback
223         */
224        public void onContentAllowed(Session session) {
225        }
226
227        /**
228         * This is called when the current program content turns out to be not allowed to watch
229         * since its content rating is blocked by parental controls.
230         *
231         * @param session A {@link TvInputManager.Session} associated with this callback
232         * @param rating The content ration of the blocked program.
233         */
234        public void onContentBlocked(Session session, TvContentRating rating) {
235        }
236
237        /**
238         * This is called when a custom event has been sent from this session.
239         *
240         * @param session A {@link TvInputManager.Session} associated with this callback
241         * @param eventType The type of the event.
242         * @param eventArgs Optional arguments of the event.
243         * @hide
244         */
245        @SystemApi
246        public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
247        }
248    }
249
250    private static final class SessionCallbackRecord {
251        private final SessionCallback mSessionCallback;
252        private final Handler mHandler;
253        private Session mSession;
254
255        public SessionCallbackRecord(SessionCallback sessionCallback,
256                Handler handler) {
257            mSessionCallback = sessionCallback;
258            mHandler = handler;
259        }
260
261        public void postSessionCreated(final Session session) {
262            mSession = session;
263            mHandler.post(new Runnable() {
264                @Override
265                public void run() {
266                    mSessionCallback.onSessionCreated(session);
267                }
268            });
269        }
270
271        public void postSessionReleased() {
272            mHandler.post(new Runnable() {
273                @Override
274                public void run() {
275                    mSessionCallback.onSessionReleased(mSession);
276                }
277            });
278        }
279
280        public void postChannelRetuned(final Uri channelUri) {
281            mHandler.post(new Runnable() {
282                @Override
283                public void run() {
284                    mSessionCallback.onChannelRetuned(mSession, channelUri);
285                }
286            });
287        }
288
289        public void postTracksChanged(final List<TvTrackInfo> tracks) {
290            mHandler.post(new Runnable() {
291                @Override
292                public void run() {
293                    mSession.mAudioTracks.clear();
294                    mSession.mVideoTracks.clear();
295                    mSession.mSubtitleTracks.clear();
296                    for (TvTrackInfo track : tracks) {
297                        if (track.getType() == TvTrackInfo.TYPE_AUDIO) {
298                            mSession.mAudioTracks.add(track);
299                        } else if (track.getType() == TvTrackInfo.TYPE_VIDEO) {
300                            mSession.mVideoTracks.add(track);
301                        } else if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
302                            mSession.mSubtitleTracks.add(track);
303                        } else {
304                            // Silently ignore.
305                        }
306                    }
307                    mSessionCallback.onTracksChanged(mSession, tracks);
308                }
309            });
310        }
311
312        public void postTrackSelected(final int type, final String trackId) {
313            mHandler.post(new Runnable() {
314                @Override
315                public void run() {
316                    if (type == TvTrackInfo.TYPE_AUDIO) {
317                        mSession.mSelectedAudioTrackId = trackId;
318                    } else if (type == TvTrackInfo.TYPE_VIDEO) {
319                        mSession.mSelectedVideoTrackId = trackId;
320                    } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
321                        mSession.mSelectedSubtitleTrackId = trackId;
322                    } else {
323                        // Silently ignore.
324                        return;
325                    }
326                    mSessionCallback.onTrackSelected(mSession, type, trackId);
327                }
328            });
329        }
330
331        public void postVideoAvailable() {
332            mHandler.post(new Runnable() {
333                @Override
334                public void run() {
335                    mSessionCallback.onVideoAvailable(mSession);
336                }
337            });
338        }
339
340        public void postVideoUnavailable(final int reason) {
341            mHandler.post(new Runnable() {
342                @Override
343                public void run() {
344                    mSessionCallback.onVideoUnavailable(mSession, reason);
345                }
346            });
347        }
348
349        public void postContentAllowed() {
350            mHandler.post(new Runnable() {
351                @Override
352                public void run() {
353                    mSessionCallback.onContentAllowed(mSession);
354                }
355            });
356        }
357
358        public void postContentBlocked(final TvContentRating rating) {
359            mHandler.post(new Runnable() {
360                @Override
361                public void run() {
362                    mSessionCallback.onContentBlocked(mSession, rating);
363                }
364            });
365        }
366
367        public void postSessionEvent(final String eventType, final Bundle eventArgs) {
368            mHandler.post(new Runnable() {
369                @Override
370                public void run() {
371                    mSessionCallback.onSessionEvent(mSession, eventType, eventArgs);
372                }
373            });
374        }
375    }
376
377    /**
378     * Interface used to monitor status of the TV input.
379     */
380    public abstract static class TvInputListener {
381        /**
382         * This is called when the state of a given TV input is changed.
383         *
384         * @param inputId The id of the TV input.
385         * @param state State of the TV input. The value is one of the following:
386         * <ul>
387         * <li>{@link TvInputManager#INPUT_STATE_CONNECTED}
388         * <li>{@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY}
389         * <li>{@link TvInputManager#INPUT_STATE_DISCONNECTED}
390         * </ul>
391         */
392        public void onInputStateChanged(String inputId, int state) {
393        }
394
395        /**
396         * This is called when a TV input is added.
397         *
398         * @param inputId The id of the TV input.
399         */
400        public void onInputAdded(String inputId) {
401        }
402
403        /**
404         * This is called when a TV input is removed.
405         *
406         * @param inputId The id of the TV input.
407         */
408        public void onInputRemoved(String inputId) {
409        }
410    }
411
412    private static final class TvInputListenerRecord {
413        private final TvInputListener mListener;
414        private final Handler mHandler;
415
416        public TvInputListenerRecord(TvInputListener listener, Handler handler) {
417            mListener = listener;
418            mHandler = handler;
419        }
420
421        public TvInputListener getListener() {
422            return mListener;
423        }
424
425        public void postInputStateChanged(final String inputId, final int state) {
426            mHandler.post(new Runnable() {
427                @Override
428                public void run() {
429                    mListener.onInputStateChanged(inputId, state);
430                }
431            });
432        }
433
434        public void postInputAdded(final String inputId) {
435            mHandler.post(new Runnable() {
436                @Override
437                public void run() {
438                    mListener.onInputAdded(inputId);
439                }
440            });
441        }
442
443        public void postInputRemoved(final String inputId) {
444            mHandler.post(new Runnable() {
445                @Override
446                public void run() {
447                    mListener.onInputRemoved(inputId);
448                }
449            });
450        }
451    }
452
453    /**
454     * @hide
455     */
456    public TvInputManager(ITvInputManager service, int userId) {
457        mService = service;
458        mUserId = userId;
459        mClient = new ITvInputClient.Stub() {
460            @Override
461            public void onSessionCreated(String inputId, IBinder token, InputChannel channel,
462                    int seq) {
463                synchronized (mSessionCallbackRecordMap) {
464                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
465                    if (record == null) {
466                        Log.e(TAG, "Callback not found for " + token);
467                        return;
468                    }
469                    Session session = null;
470                    if (token != null) {
471                        session = new Session(token, channel, mService, mUserId, seq,
472                                mSessionCallbackRecordMap);
473                    }
474                    record.postSessionCreated(session);
475                }
476            }
477
478            @Override
479            public void onSessionReleased(int seq) {
480                synchronized (mSessionCallbackRecordMap) {
481                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
482                    mSessionCallbackRecordMap.delete(seq);
483                    if (record == null) {
484                        Log.e(TAG, "Callback not found for seq:" + seq);
485                        return;
486                    }
487                    record.mSession.releaseInternal();
488                    record.postSessionReleased();
489                }
490            }
491
492            @Override
493            public void onChannelRetuned(Uri channelUri, int seq) {
494                synchronized (mSessionCallbackRecordMap) {
495                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
496                    if (record == null) {
497                        Log.e(TAG, "Callback not found for seq " + seq);
498                        return;
499                    }
500                    record.postChannelRetuned(channelUri);
501                }
502            }
503
504            @Override
505            public void onTracksChanged(List<TvTrackInfo> tracks, int seq) {
506                synchronized (mSessionCallbackRecordMap) {
507                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
508                    if (record == null) {
509                        Log.e(TAG, "Callback not found for seq " + seq);
510                        return;
511                    }
512                    record.postTracksChanged(tracks);
513                }
514            }
515
516            @Override
517            public void onTrackSelected(int type, String trackId, int seq) {
518                synchronized (mSessionCallbackRecordMap) {
519                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
520                    if (record == null) {
521                        Log.e(TAG, "Callback not found for seq " + seq);
522                        return;
523                    }
524                    record.postTrackSelected(type, trackId);
525                }
526            }
527
528            @Override
529            public void onVideoAvailable(int seq) {
530                synchronized (mSessionCallbackRecordMap) {
531                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
532                    if (record == null) {
533                        Log.e(TAG, "Callback not found for seq " + seq);
534                        return;
535                    }
536                    record.postVideoAvailable();
537                }
538            }
539
540            @Override
541            public void onVideoUnavailable(int reason, int seq) {
542                synchronized (mSessionCallbackRecordMap) {
543                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
544                    if (record == null) {
545                        Log.e(TAG, "Callback not found for seq " + seq);
546                        return;
547                    }
548                    record.postVideoUnavailable(reason);
549                }
550            }
551
552            @Override
553            public void onContentAllowed(int seq) {
554                synchronized (mSessionCallbackRecordMap) {
555                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
556                    if (record == null) {
557                        Log.e(TAG, "Callback not found for seq " + seq);
558                        return;
559                    }
560                    record.postContentAllowed();
561                }
562            }
563
564            @Override
565            public void onContentBlocked(String rating, int seq) {
566                synchronized (mSessionCallbackRecordMap) {
567                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
568                    if (record == null) {
569                        Log.e(TAG, "Callback not found for seq " + seq);
570                        return;
571                    }
572                    record.postContentBlocked(TvContentRating.unflattenFromString(rating));
573                }
574            }
575
576            @Override
577            public void onSessionEvent(String eventType, Bundle eventArgs, int seq) {
578                synchronized (mSessionCallbackRecordMap) {
579                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
580                    if (record == null) {
581                        Log.e(TAG, "Callback not found for seq " + seq);
582                        return;
583                    }
584                    record.postSessionEvent(eventType, eventArgs);
585                }
586            }
587        };
588        mCallback = new ITvInputManagerCallback.Stub() {
589            @Override
590            public void onInputStateChanged(String inputId, int state) {
591                synchronized (mLock) {
592                    mStateMap.put(inputId, state);
593                    for (TvInputListenerRecord record : mTvInputListenerRecordsList) {
594                        record.postInputStateChanged(inputId, state);
595                    }
596                }
597            }
598
599            @Override
600            public void onInputAdded(String inputId) {
601                synchronized (mLock) {
602                    mStateMap.put(inputId, INPUT_STATE_CONNECTED);
603                    for (TvInputListenerRecord record : mTvInputListenerRecordsList) {
604                        record.postInputAdded(inputId);
605                    }
606                }
607            }
608
609            @Override
610            public void onInputRemoved(String inputId) {
611                synchronized (mLock) {
612                    mStateMap.remove(inputId);
613                    for (TvInputListenerRecord record : mTvInputListenerRecordsList) {
614                        record.postInputRemoved(inputId);
615                    }
616                }
617            }
618        };
619        try {
620            mService.registerCallback(mCallback, mUserId);
621        } catch (RemoteException e) {
622            Log.e(TAG, "mService.registerCallback failed: " + e);
623        }
624    }
625
626    /**
627     * Returns the complete list of TV inputs on the system.
628     *
629     * @return List of {@link TvInputInfo} for each TV input that describes its meta information.
630     */
631    public List<TvInputInfo> getTvInputList() {
632        try {
633            return mService.getTvInputList(mUserId);
634        } catch (RemoteException e) {
635            throw new RuntimeException(e);
636        }
637    }
638
639    /**
640     * Returns the {@link TvInputInfo} for a given TV input.
641     *
642     * @param inputId The ID of the TV input.
643     * @return the {@link TvInputInfo} for a given TV input. {@code null} if not found.
644     */
645    public TvInputInfo getTvInputInfo(String inputId) {
646        if (inputId == null) {
647            throw new IllegalArgumentException("inputId cannot be null");
648        }
649        try {
650            return mService.getTvInputInfo(inputId, mUserId);
651        } catch (RemoteException e) {
652            throw new RuntimeException(e);
653        }
654    }
655
656    /**
657     * Returns the state of a given TV input. It retuns one of the following:
658     * <ul>
659     * <li>{@link #INPUT_STATE_CONNECTED}
660     * <li>{@link #INPUT_STATE_CONNECTED_STANDBY}
661     * <li>{@link #INPUT_STATE_DISCONNECTED}
662     * </ul>
663     *
664     * @param inputId The id of the TV input.
665     * @throws IllegalArgumentException if the argument is {@code null} or if there is no
666     *        {@link TvInputInfo} corresponding to {@code inputId}.
667     */
668    public int getInputState(String inputId) {
669        if (inputId == null) {
670            throw new IllegalArgumentException("inputId cannot be null");
671        }
672        synchronized (mLock) {
673            Integer state = mStateMap.get(inputId);
674            if (state == null) {
675                throw new IllegalArgumentException("Unrecognized input ID: " + inputId);
676            }
677            return state.intValue();
678        }
679    }
680
681    /**
682     * Registers a {@link TvInputListener}.
683     *
684     * @param listener A listener used to monitor status of the TV inputs.
685     * @param handler A {@link Handler} that the status change will be delivered to.
686     * @throws IllegalArgumentException if any of the arguments is {@code null}.
687     */
688    public void registerListener(TvInputListener listener, Handler handler) {
689        if (listener == null) {
690            throw new IllegalArgumentException("callback cannot be null");
691        }
692        if (handler == null) {
693            throw new IllegalArgumentException("handler cannot be null");
694        }
695        synchronized (mLock) {
696            mTvInputListenerRecordsList.add(new TvInputListenerRecord(listener, handler));
697        }
698    }
699
700    /**
701     * Unregisters the existing {@link TvInputListener}.
702     *
703     * @param listener The existing listener to remove.
704     * @throws IllegalArgumentException if any of the arguments is {@code null}.
705     */
706    public void unregisterListener(final TvInputListener listener) {
707        if (listener == null) {
708            throw new IllegalArgumentException("callback cannot be null");
709        }
710        synchronized (mLock) {
711            for (Iterator<TvInputListenerRecord> it = mTvInputListenerRecordsList.iterator();
712                    it.hasNext(); ) {
713                TvInputListenerRecord record = it.next();
714                if (record.getListener() == listener) {
715                    it.remove();
716                    break;
717                }
718            }
719        }
720    }
721
722    /**
723     * Returns the user's parental controls enabled state.
724     *
725     * @return {@code true} if the user enabled the parental controls, {@code false} otherwise.
726     */
727    public boolean isParentalControlsEnabled() {
728        try {
729            return mService.isParentalControlsEnabled(mUserId);
730        } catch (RemoteException e) {
731            throw new RuntimeException(e);
732        }
733    }
734
735    /**
736     * Sets the user's parental controls enabled state.
737     *
738     * @param enabled The user's parental controls enabled state. {@code true} if the user enabled
739     *            the parental controls, {@code false} otherwise.
740     * @see #isParentalControlsEnabled
741     * @hide
742     */
743    @SystemApi
744    public void setParentalControlsEnabled(boolean enabled) {
745        try {
746            mService.setParentalControlsEnabled(enabled, mUserId);
747        } catch (RemoteException e) {
748            throw new RuntimeException(e);
749        }
750    }
751
752    /**
753     * Checks whether a given TV content rating is blocked by the user.
754     *
755     * @param rating The TV content rating to check.
756     * @return {@code true} if the given TV content rating is blocked, {@code false} otherwise.
757     */
758    public boolean isRatingBlocked(TvContentRating rating) {
759        if (rating == null) {
760            throw new IllegalArgumentException("rating cannot be null");
761        }
762        try {
763            return mService.isRatingBlocked(rating.flattenToString(), mUserId);
764        } catch (RemoteException e) {
765            throw new RuntimeException(e);
766        }
767    }
768
769    /**
770     * Returns the list of blocked content ratings.
771     *
772     * @return the list of content ratings blocked by the user.
773     * @hide
774     */
775    @SystemApi
776    public List<TvContentRating> getBlockedRatings() {
777        try {
778            List<TvContentRating> ratings = new ArrayList<TvContentRating>();
779            for (String rating : mService.getBlockedRatings(mUserId)) {
780                ratings.add(TvContentRating.unflattenFromString(rating));
781            }
782            return ratings;
783        } catch (RemoteException e) {
784            throw new RuntimeException(e);
785        }
786    }
787
788    /**
789     * Adds a user blocked content rating.
790     *
791     * @param rating The content rating to block.
792     * @see #isRatingBlocked
793     * @see #removeBlockedRating
794     * @hide
795     */
796    @SystemApi
797    public void addBlockedRating(TvContentRating rating) {
798        if (rating == null) {
799            throw new IllegalArgumentException("rating cannot be null");
800        }
801        try {
802            mService.addBlockedRating(rating.flattenToString(), mUserId);
803        } catch (RemoteException e) {
804            throw new RuntimeException(e);
805        }
806    }
807
808    /**
809     * Removes a user blocked content rating.
810     *
811     * @param rating The content rating to unblock.
812     * @see #isRatingBlocked
813     * @see #addBlockedRating
814     * @hide
815     */
816    @SystemApi
817    public void removeBlockedRating(TvContentRating rating) {
818        if (rating == null) {
819            throw new IllegalArgumentException("rating cannot be null");
820        }
821        try {
822            mService.removeBlockedRating(rating.flattenToString(), mUserId);
823        } catch (RemoteException e) {
824            throw new RuntimeException(e);
825        }
826    }
827
828    /**
829     * Returns the list of xml resource uris for TV content rating systems.
830     * @hide
831     */
832    @SystemApi
833    public List<Uri> getTvContentRatingSystemXmls() {
834        try {
835            return mService.getTvContentRatingSystemXmls(mUserId);
836        } catch (RemoteException e) {
837            throw new RuntimeException(e);
838        }
839    }
840
841    /**
842     * Creates a {@link Session} for a given TV input.
843     * <p>
844     * The number of sessions that can be created at the same time is limited by the capability of
845     * the given TV input.
846     * </p>
847     *
848     * @param inputId The id of the TV input.
849     * @param callback A callback used to receive the created session.
850     * @param handler A {@link Handler} that the session creation will be delivered to.
851     * @throws IllegalArgumentException if any of the arguments is {@code null}.
852     * @hide
853     */
854    @SystemApi
855    public void createSession(String inputId, final SessionCallback callback,
856            Handler handler) {
857        if (inputId == null) {
858            throw new IllegalArgumentException("id cannot be null");
859        }
860        if (callback == null) {
861            throw new IllegalArgumentException("callback cannot be null");
862        }
863        if (handler == null) {
864            throw new IllegalArgumentException("handler cannot be null");
865        }
866        SessionCallbackRecord record = new SessionCallbackRecord(callback, handler);
867        synchronized (mSessionCallbackRecordMap) {
868            int seq = mNextSeq++;
869            mSessionCallbackRecordMap.put(seq, record);
870            try {
871                mService.createSession(mClient, inputId, seq, mUserId);
872            } catch (RemoteException e) {
873                throw new RuntimeException(e);
874            }
875        }
876    }
877
878    /**
879     * Returns the TvStreamConfig list of the given TV input.
880     *
881     * @param inputId the id of the TV input.
882     * @return List of {@link TvStreamConfig} which is available for capturing
883     *   of the given TV input.
884     * @hide
885     */
886    @SystemApi
887    public List<TvStreamConfig> getAvailableTvStreamConfigList(String inputId) {
888        try {
889            return mService.getAvailableTvStreamConfigList(inputId, mUserId);
890        } catch (RemoteException e) {
891            throw new RuntimeException(e);
892        }
893    }
894
895    /**
896     * Take a snapshot of the given TV input into the provided Surface.
897     *
898     * @param inputId the id of the TV input.
899     * @param surface the {@link Surface} to which the snapshot is captured.
900     * @param config the {@link TvStreamConfig} which is used for capturing.
901     * @return true when the {@link Surface} is ready to be captured.
902     * @hide
903     */
904    @SystemApi
905    public boolean captureFrame(String inputId, Surface surface, TvStreamConfig config) {
906        try {
907            return mService.captureFrame(inputId, surface, config, mUserId);
908        } catch (RemoteException e) {
909            throw new RuntimeException(e);
910        }
911    }
912
913    /**
914     * The Session provides the per-session functionality of TV inputs.
915     * @hide
916     */
917    @SystemApi
918    public static final class Session {
919        static final int DISPATCH_IN_PROGRESS = -1;
920        static final int DISPATCH_NOT_HANDLED = 0;
921        static final int DISPATCH_HANDLED = 1;
922
923        private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
924
925        private final ITvInputManager mService;
926        private final int mUserId;
927        private final int mSeq;
928
929        // For scheduling input event handling on the main thread. This also serves as a lock to
930        // protect pending input events and the input channel.
931        private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
932
933        private final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20);
934        private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20);
935        private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
936
937        private IBinder mToken;
938        private TvInputEventSender mSender;
939        private InputChannel mChannel;
940        private final List<TvTrackInfo> mAudioTracks = new ArrayList<TvTrackInfo>();
941        private final List<TvTrackInfo> mVideoTracks = new ArrayList<TvTrackInfo>();
942        private final List<TvTrackInfo> mSubtitleTracks = new ArrayList<TvTrackInfo>();
943        private String mSelectedAudioTrackId;
944        private String mSelectedVideoTrackId;
945        private String mSelectedSubtitleTrackId;
946
947        private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
948                int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
949            mToken = token;
950            mChannel = channel;
951            mService = service;
952            mUserId = userId;
953            mSeq = seq;
954            mSessionCallbackRecordMap = sessionCallbackRecordMap;
955        }
956
957        /**
958         * Releases this session.
959         */
960        public void release() {
961            if (mToken == null) {
962                Log.w(TAG, "The session has been already released");
963                return;
964            }
965            try {
966                mService.releaseSession(mToken, mUserId);
967            } catch (RemoteException e) {
968                throw new RuntimeException(e);
969            }
970
971            releaseInternal();
972        }
973
974        /**
975         * Sets this as main session. See {@link TvView#setMainTvView} for about meaning of "main".
976         * @hide
977         */
978        public void setMainSession() {
979            if (mToken == null) {
980                Log.w(TAG, "The session has been already released");
981                return;
982            }
983            try {
984                mService.setMainSession(mToken, mUserId);
985            } catch (RemoteException e) {
986                throw new RuntimeException(e);
987            }
988        }
989
990        /**
991         * Sets the {@link android.view.Surface} for this session.
992         *
993         * @param surface A {@link android.view.Surface} used to render video.
994         */
995        public void setSurface(Surface surface) {
996            if (mToken == null) {
997                Log.w(TAG, "The session has been already released");
998                return;
999            }
1000            // surface can be null.
1001            try {
1002                mService.setSurface(mToken, surface, mUserId);
1003            } catch (RemoteException e) {
1004                throw new RuntimeException(e);
1005            }
1006        }
1007
1008        /**
1009         * Notifies of any structural changes (format or size) of the {@link Surface}
1010         * passed by {@link #setSurface}.
1011         *
1012         * @param format The new PixelFormat of the {@link Surface}.
1013         * @param width The new width of the {@link Surface}.
1014         * @param height The new height of the {@link Surface}.
1015         * @hide
1016         */
1017        @SystemApi
1018        public void dispatchSurfaceChanged(int format, int width, int height) {
1019            if (mToken == null) {
1020                Log.w(TAG, "The session has been already released");
1021                return;
1022            }
1023            try {
1024                mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
1025            } catch (RemoteException e) {
1026                throw new RuntimeException(e);
1027            }
1028        }
1029
1030        /**
1031         * Sets the relative stream volume of this session to handle a change of audio focus.
1032         *
1033         * @param volume A volume value between 0.0f to 1.0f.
1034         * @throws IllegalArgumentException if the volume value is out of range.
1035         */
1036        public void setStreamVolume(float volume) {
1037            if (mToken == null) {
1038                Log.w(TAG, "The session has been already released");
1039                return;
1040            }
1041            try {
1042                if (volume < 0.0f || volume > 1.0f) {
1043                    throw new IllegalArgumentException("volume should be between 0.0f and 1.0f");
1044                }
1045                mService.setVolume(mToken, volume, mUserId);
1046            } catch (RemoteException e) {
1047                throw new RuntimeException(e);
1048            }
1049        }
1050
1051        /**
1052         * Tunes to a given channel.
1053         *
1054         * @param channelUri The URI of a channel.
1055         * @throws IllegalArgumentException if the argument is {@code null}.
1056         */
1057        public void tune(Uri channelUri) {
1058            tune(channelUri, null);
1059        }
1060
1061        /**
1062         * Tunes to a given channel.
1063         *
1064         * @param channelUri The URI of a channel.
1065         * @param params A set of extra parameters which might be handled with this tune event.
1066         * @throws IllegalArgumentException if {@code channelUri} is {@code null}.
1067         * @hide
1068         */
1069        @SystemApi
1070        public void tune(Uri channelUri, Bundle params) {
1071            if (channelUri == null) {
1072                throw new IllegalArgumentException("channelUri cannot be null");
1073            }
1074            if (mToken == null) {
1075                Log.w(TAG, "The session has been already released");
1076                return;
1077            }
1078            mAudioTracks.clear();
1079            mVideoTracks.clear();
1080            mSubtitleTracks.clear();
1081            mSelectedAudioTrackId = null;
1082            mSelectedVideoTrackId = null;
1083            mSelectedSubtitleTrackId = null;
1084            try {
1085                mService.tune(mToken, channelUri, params, mUserId);
1086            } catch (RemoteException e) {
1087                throw new RuntimeException(e);
1088            }
1089        }
1090
1091        /**
1092         * Enables or disables the caption for this session.
1093         *
1094         * @param enabled {@code true} to enable, {@code false} to disable.
1095         */
1096        public void setCaptionEnabled(boolean enabled) {
1097            if (mToken == null) {
1098                Log.w(TAG, "The session has been already released");
1099                return;
1100            }
1101            try {
1102                mService.setCaptionEnabled(mToken, enabled, mUserId);
1103            } catch (RemoteException e) {
1104                throw new RuntimeException(e);
1105            }
1106        }
1107
1108        /**
1109         * Selects a track.
1110         *
1111         * @param type The type of the track to select. The type can be
1112         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
1113         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
1114         * @param trackId The ID of the track to select. When {@code null}, the currently selected
1115         *            track of the given type will be unselected.
1116         * @see #getTracks()
1117         */
1118        public void selectTrack(int type, String trackId) {
1119            if (type == TvTrackInfo.TYPE_AUDIO) {
1120                if (trackId != null && !mAudioTracks.contains(trackId)) {
1121                    Log.w(TAG, "Invalid audio trackId: " + trackId);
1122                }
1123            } else if (type == TvTrackInfo.TYPE_VIDEO) {
1124                if (trackId != null && !mVideoTracks.contains(trackId)) {
1125                    Log.w(TAG, "Invalid video trackId: " + trackId);
1126                }
1127            } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
1128                if (trackId != null && !mSubtitleTracks.contains(trackId)) {
1129                    Log.w(TAG, "Invalid subtitle trackId: " + trackId);
1130                }
1131            } else {
1132                throw new IllegalArgumentException("invalid type: " + type);
1133            }
1134            if (mToken == null) {
1135                Log.w(TAG, "The session has been already released");
1136                return;
1137            }
1138            try {
1139                mService.selectTrack(mToken, type, trackId, mUserId);
1140            } catch (RemoteException e) {
1141                throw new RuntimeException(e);
1142            }
1143        }
1144
1145        /**
1146         * Returns the list of tracks for a given type. Returns {@code null} if the information is
1147         * not available.
1148         *
1149         * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
1150         *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
1151         * @return the list of tracks for the given type.
1152         */
1153        public List<TvTrackInfo> getTracks(int type) {
1154            if (type == TvTrackInfo.TYPE_AUDIO) {
1155                if (mAudioTracks == null) {
1156                    return null;
1157                }
1158                return mAudioTracks;
1159            } else if (type == TvTrackInfo.TYPE_VIDEO) {
1160                if (mVideoTracks == null) {
1161                    return null;
1162                }
1163                return mVideoTracks;
1164            } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
1165                if (mSubtitleTracks == null) {
1166                    return null;
1167                }
1168                return mSubtitleTracks;
1169            }
1170            throw new IllegalArgumentException("invalid type: " + type);
1171        }
1172
1173        /**
1174         * Returns the selected track for a given type. Returns {@code null} if the information is
1175         * not available or any of the tracks for the given type is not selected.
1176         *
1177         * @return the ID of the selected track.
1178         * @see #selectTrack
1179         */
1180        public String getSelectedTrack(int type) {
1181            if (type == TvTrackInfo.TYPE_AUDIO) {
1182                return mSelectedAudioTrackId;
1183            } else if (type == TvTrackInfo.TYPE_VIDEO) {
1184                return mSelectedVideoTrackId;
1185            } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
1186                return mSelectedSubtitleTrackId;
1187            }
1188            throw new IllegalArgumentException("invalid type: " + type);
1189        }
1190
1191        /**
1192         * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
1193         * TvInputService.Session.appPrivateCommand()} on the current TvView.
1194         *
1195         * @param action Name of the command to be performed. This <em>must</em> be a scoped name,
1196         *            i.e. prefixed with a package name you own, so that different developers will
1197         *            not create conflicting commands.
1198         * @param data Any data to include with the command.
1199         * @hide
1200         */
1201        @SystemApi
1202        public void sendAppPrivateCommand(String action, Bundle data) {
1203            if (mToken == null) {
1204                Log.w(TAG, "The session has been already released");
1205                return;
1206            }
1207            try {
1208                mService.sendAppPrivateCommand(mToken, action, data, mUserId);
1209            } catch (RemoteException e) {
1210                throw new RuntimeException(e);
1211            }
1212        }
1213
1214        /**
1215         * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView}
1216         * should be called whenever the layout of its containing view is changed.
1217         * {@link #removeOverlayView()} should be called to remove the overlay view.
1218         * Since a session can have only one overlay view, this method should be called only once
1219         * or it can be called again after calling {@link #removeOverlayView()}.
1220         *
1221         * @param view A view playing TV.
1222         * @param frame A position of the overlay view.
1223         * @throws IllegalArgumentException if any of the arguments is {@code null}.
1224         * @throws IllegalStateException if {@code view} is not attached to a window.
1225         */
1226        void createOverlayView(View view, Rect frame) {
1227            if (view == null) {
1228                throw new IllegalArgumentException("view cannot be null");
1229            }
1230            if (frame == null) {
1231                throw new IllegalArgumentException("frame cannot be null");
1232            }
1233            if (view.getWindowToken() == null) {
1234                throw new IllegalStateException("view must be attached to a window");
1235            }
1236            if (mToken == null) {
1237                Log.w(TAG, "The session has been already released");
1238                return;
1239            }
1240            try {
1241                mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId);
1242            } catch (RemoteException e) {
1243                throw new RuntimeException(e);
1244            }
1245        }
1246
1247        /**
1248         * Relayouts the current overlay view.
1249         *
1250         * @param frame A new position of the overlay view.
1251         * @throws IllegalArgumentException if the arguments is {@code null}.
1252         */
1253        void relayoutOverlayView(Rect frame) {
1254            if (frame == null) {
1255                throw new IllegalArgumentException("frame cannot be null");
1256            }
1257            if (mToken == null) {
1258                Log.w(TAG, "The session has been already released");
1259                return;
1260            }
1261            try {
1262                mService.relayoutOverlayView(mToken, frame, mUserId);
1263            } catch (RemoteException e) {
1264                throw new RuntimeException(e);
1265            }
1266        }
1267
1268        /**
1269         * Removes the current overlay view.
1270         */
1271        void removeOverlayView() {
1272            if (mToken == null) {
1273                Log.w(TAG, "The session has been already released");
1274                return;
1275            }
1276            try {
1277                mService.removeOverlayView(mToken, mUserId);
1278            } catch (RemoteException e) {
1279                throw new RuntimeException(e);
1280            }
1281        }
1282
1283        /**
1284         * Requests to unblock content blocked by parental controls.
1285         */
1286        void requestUnblockContent(TvContentRating unblockedRating) {
1287            if (mToken == null) {
1288                Log.w(TAG, "The session has been already released");
1289                return;
1290            }
1291            try {
1292                mService.requestUnblockContent(mToken, unblockedRating.flattenToString(), mUserId);
1293            } catch (RemoteException e) {
1294                throw new RuntimeException(e);
1295            }
1296        }
1297
1298        /**
1299         * Dispatches an input event to this session.
1300         *
1301         * @param event An {@link InputEvent} to dispatch.
1302         * @param token A token used to identify the input event later in the callback.
1303         * @param callback A callback used to receive the dispatch result.
1304         * @param handler A {@link Handler} that the dispatch result will be delivered to.
1305         * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
1306         *         {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
1307         *         {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
1308         *         be invoked later.
1309         * @throws IllegalArgumentException if any of the necessary arguments is {@code null}.
1310         * @hide
1311         */
1312        public int dispatchInputEvent(InputEvent event, Object token,
1313                FinishedInputEventCallback callback, Handler handler) {
1314            if (event == null) {
1315                throw new IllegalArgumentException("event cannot be null");
1316            }
1317            if (callback != null && handler == null) {
1318                throw new IllegalArgumentException("handler cannot be null");
1319            }
1320            synchronized (mHandler) {
1321                if (mChannel == null) {
1322                    return DISPATCH_NOT_HANDLED;
1323                }
1324                PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
1325                if (Looper.myLooper() == Looper.getMainLooper()) {
1326                    // Already running on the main thread so we can send the event immediately.
1327                    return sendInputEventOnMainLooperLocked(p);
1328                }
1329
1330                // Post the event to the main thread.
1331                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
1332                msg.setAsynchronous(true);
1333                mHandler.sendMessage(msg);
1334                return DISPATCH_IN_PROGRESS;
1335            }
1336        }
1337
1338        /**
1339         * Callback that is invoked when an input event that was dispatched to this session has been
1340         * finished.
1341         *
1342         * @hide
1343         */
1344        public interface FinishedInputEventCallback {
1345            /**
1346             * Called when the dispatched input event is finished.
1347             *
1348             * @param token A token passed to {@link #dispatchInputEvent}.
1349             * @param handled {@code true} if the dispatched input event was handled properly.
1350             *            {@code false} otherwise.
1351             */
1352            public void onFinishedInputEvent(Object token, boolean handled);
1353        }
1354
1355        // Must be called on the main looper
1356        private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
1357            synchronized (mHandler) {
1358                int result = sendInputEventOnMainLooperLocked(p);
1359                if (result == DISPATCH_IN_PROGRESS) {
1360                    return;
1361                }
1362            }
1363
1364            invokeFinishedInputEventCallback(p, false);
1365        }
1366
1367        private int sendInputEventOnMainLooperLocked(PendingEvent p) {
1368            if (mChannel != null) {
1369                if (mSender == null) {
1370                    mSender = new TvInputEventSender(mChannel, mHandler.getLooper());
1371                }
1372
1373                final InputEvent event = p.mEvent;
1374                final int seq = event.getSequenceNumber();
1375                if (mSender.sendInputEvent(seq, event)) {
1376                    mPendingEvents.put(seq, p);
1377                    Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
1378                    msg.setAsynchronous(true);
1379                    mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
1380                    return DISPATCH_IN_PROGRESS;
1381                }
1382
1383                Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
1384                        + event);
1385            }
1386            return DISPATCH_NOT_HANDLED;
1387        }
1388
1389        void finishedInputEvent(int seq, boolean handled, boolean timeout) {
1390            final PendingEvent p;
1391            synchronized (mHandler) {
1392                int index = mPendingEvents.indexOfKey(seq);
1393                if (index < 0) {
1394                    return; // spurious, event already finished or timed out
1395                }
1396
1397                p = mPendingEvents.valueAt(index);
1398                mPendingEvents.removeAt(index);
1399
1400                if (timeout) {
1401                    Log.w(TAG, "Timeout waiting for seesion to handle input event after "
1402                            + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
1403                } else {
1404                    mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
1405                }
1406            }
1407
1408            invokeFinishedInputEventCallback(p, handled);
1409        }
1410
1411        // Assumes the event has already been removed from the queue.
1412        void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
1413            p.mHandled = handled;
1414            if (p.mHandler.getLooper().isCurrentThread()) {
1415                // Already running on the callback handler thread so we can send the callback
1416                // immediately.
1417                p.run();
1418            } else {
1419                // Post the event to the callback handler thread.
1420                // In this case, the callback will be responsible for recycling the event.
1421                Message msg = Message.obtain(p.mHandler, p);
1422                msg.setAsynchronous(true);
1423                msg.sendToTarget();
1424            }
1425        }
1426
1427        private void flushPendingEventsLocked() {
1428            mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
1429
1430            final int count = mPendingEvents.size();
1431            for (int i = 0; i < count; i++) {
1432                int seq = mPendingEvents.keyAt(i);
1433                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
1434                msg.setAsynchronous(true);
1435                msg.sendToTarget();
1436            }
1437        }
1438
1439        private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
1440                FinishedInputEventCallback callback, Handler handler) {
1441            PendingEvent p = mPendingEventPool.acquire();
1442            if (p == null) {
1443                p = new PendingEvent();
1444            }
1445            p.mEvent = event;
1446            p.mToken = token;
1447            p.mCallback = callback;
1448            p.mHandler = handler;
1449            return p;
1450        }
1451
1452        private void recyclePendingEventLocked(PendingEvent p) {
1453            p.recycle();
1454            mPendingEventPool.release(p);
1455        }
1456
1457        IBinder getToken() {
1458            return mToken;
1459        }
1460
1461        private void releaseInternal() {
1462            mToken = null;
1463            synchronized (mHandler) {
1464                if (mChannel != null) {
1465                    if (mSender != null) {
1466                        flushPendingEventsLocked();
1467                        mSender.dispose();
1468                        mSender = null;
1469                    }
1470                    mChannel.dispose();
1471                    mChannel = null;
1472                }
1473            }
1474            synchronized (mSessionCallbackRecordMap) {
1475                mSessionCallbackRecordMap.remove(mSeq);
1476            }
1477        }
1478
1479        private final class InputEventHandler extends Handler {
1480            public static final int MSG_SEND_INPUT_EVENT = 1;
1481            public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
1482            public static final int MSG_FLUSH_INPUT_EVENT = 3;
1483
1484            InputEventHandler(Looper looper) {
1485                super(looper, null, true);
1486            }
1487
1488            @Override
1489            public void handleMessage(Message msg) {
1490                switch (msg.what) {
1491                    case MSG_SEND_INPUT_EVENT: {
1492                        sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
1493                        return;
1494                    }
1495                    case MSG_TIMEOUT_INPUT_EVENT: {
1496                        finishedInputEvent(msg.arg1, false, true);
1497                        return;
1498                    }
1499                    case MSG_FLUSH_INPUT_EVENT: {
1500                        finishedInputEvent(msg.arg1, false, false);
1501                        return;
1502                    }
1503                }
1504            }
1505        }
1506
1507        private final class TvInputEventSender extends InputEventSender {
1508            public TvInputEventSender(InputChannel inputChannel, Looper looper) {
1509                super(inputChannel, looper);
1510            }
1511
1512            @Override
1513            public void onInputEventFinished(int seq, boolean handled) {
1514                finishedInputEvent(seq, handled, false);
1515            }
1516        }
1517
1518        private final class PendingEvent implements Runnable {
1519            public InputEvent mEvent;
1520            public Object mToken;
1521            public FinishedInputEventCallback mCallback;
1522            public Handler mHandler;
1523            public boolean mHandled;
1524
1525            public void recycle() {
1526                mEvent = null;
1527                mToken = null;
1528                mCallback = null;
1529                mHandler = null;
1530                mHandled = false;
1531            }
1532
1533            @Override
1534            public void run() {
1535                mCallback.onFinishedInputEvent(mToken, mHandled);
1536
1537                synchronized (mHandler) {
1538                    recyclePendingEventLocked(this);
1539                }
1540            }
1541        }
1542    }
1543}
1544