OSDN Git Service

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