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