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