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