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.KeyEvent;
37import android.view.Surface;
38import android.view.View;
39
40import java.util.ArrayList;
41import java.util.Iterator;
42import java.util.LinkedList;
43import java.util.List;
44import java.util.Map;
45
46/**
47 * Central system API to the overall TV input framework (TIF) architecture, which arbitrates
48 * interaction between applications and the selected TV inputs.
49 */
50public final class TvInputManager {
51    private static final String TAG = "TvInputManager";
52
53    static final int VIDEO_UNAVAILABLE_REASON_START = 0;
54    static final int VIDEO_UNAVAILABLE_REASON_END = 3;
55
56    /**
57     * A generic reason. Video is not available due to an unspecified error.
58     */
59    public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START;
60    /**
61     * Video is not available because the TV input is in the middle of tuning to a new channel.
62     */
63    public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1;
64    /**
65     * Video is not available due to the weak TV signal.
66     */
67    public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2;
68    /**
69     * Video is not available because the TV input stopped the playback temporarily to buffer more
70     * data.
71     */
72    public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = VIDEO_UNAVAILABLE_REASON_END;
73
74    /**
75     * The TV input is in unknown state.
76     * <p>
77     * State for denoting unknown TV input state. The typical use case is when a requested TV
78     * input is removed from the device or it is not registered. Used in
79     * {@code ITvInputManager.getTvInputState()}.
80     * </p>
81     * @hide
82     */
83    public static final int INPUT_STATE_UNKNOWN = -1;
84
85    /**
86     * The TV input is connected.
87     * <p>
88     * State for {@link #getInputState} and {@link
89     * TvInputManager.TvInputCallback#onInputStateChanged}.
90     * </p>
91     */
92    public static final int INPUT_STATE_CONNECTED = 0;
93    /**
94     * The TV input is connected but in standby mode. It would take a while until it becomes
95     * fully ready.
96     * <p>
97     * State for {@link #getInputState} and {@link
98     * TvInputManager.TvInputCallback#onInputStateChanged}.
99     * </p>
100     */
101    public static final int INPUT_STATE_CONNECTED_STANDBY = 1;
102    /**
103     * The TV input is disconnected.
104     * <p>
105     * State for {@link #getInputState} and {@link
106     * TvInputManager.TvInputCallback#onInputStateChanged}.
107     * </p>
108     */
109    public static final int INPUT_STATE_DISCONNECTED = 2;
110
111    /**
112     * Broadcast intent action when the user blocked content ratings change. For use with the
113     * {@link #isRatingBlocked}.
114     */
115    public static final String ACTION_BLOCKED_RATINGS_CHANGED =
116            "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
117
118    /**
119     * Broadcast intent action when the parental controls enabled state changes. For use with the
120     * {@link #isParentalControlsEnabled}.
121     */
122    public static final String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED =
123            "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
124
125    /**
126     * Broadcast intent action used to query available content rating systems.
127     * <p>
128     * The TV input manager service locates available content rating systems by querying broadcast
129     * receivers that are registered for this action. An application can offer additional content
130     * rating systems to the user by declaring a suitable broadcast receiver in its manifest.
131     * </p><p>
132     * Here is an example broadcast receiver declaration that an application might include in its
133     * AndroidManifest.xml to advertise custom content rating systems. The meta-data specifies a
134     * resource that contains a description of each content rating system that is provided by the
135     * application.
136     * <p><pre class="prettyprint">
137     * {@literal
138     * <receiver android:name=".TvInputReceiver">
139     *     <intent-filter>
140     *         <action android:name=
141     *                 "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
142     *     </intent-filter>
143     *     <meta-data
144     *             android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS"
145     *             android:resource="@xml/tv_content_rating_systems" />
146     * </receiver>}</pre></p>
147     * In the above example, the <code>@xml/tv_content_rating_systems</code> resource refers to an
148     * XML resource whose root element is <code>&lt;rating-system-definitions&gt;</code> that
149     * contains zero or more <code>&lt;rating-system-definition&gt;</code> elements. Each <code>
150     * &lt;rating-system-definition&gt;</code> element specifies the ratings, sub-ratings and rating
151     * orders of a particular content rating system.
152     * </p>
153     *
154     * @see TvContentRating
155     */
156    public static final String ACTION_QUERY_CONTENT_RATING_SYSTEMS =
157            "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
158
159    /**
160     * Content rating systems metadata associated with {@link #ACTION_QUERY_CONTENT_RATING_SYSTEMS}.
161     * <p>
162     * Specifies the resource ID of an XML resource that describes the content rating systems that
163     * are provided by the application.
164     * </p>
165     */
166    public static final String META_DATA_CONTENT_RATING_SYSTEMS =
167            "android.media.tv.metadata.CONTENT_RATING_SYSTEMS";
168
169    private final ITvInputManager mService;
170
171    private final Object mLock = new Object();
172
173    // @GuardedBy("mLock")
174    private final List<TvInputCallbackRecord> mCallbackRecords =
175            new LinkedList<TvInputCallbackRecord>();
176
177    // A mapping from TV input ID to the state of corresponding input.
178    // @GuardedBy("mLock")
179    private final Map<String, Integer> mStateMap = new ArrayMap<String, Integer>();
180
181    // A mapping from the sequence number of a session to its SessionCallbackRecord.
182    private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap =
183            new SparseArray<SessionCallbackRecord>();
184
185    // A sequence number for the next session to be created. Should be protected by a lock
186    // {@code mSessionCallbackRecordMap}.
187    private int mNextSeq;
188
189    private final ITvInputClient mClient;
190
191    private final ITvInputManagerCallback mManagerCallback;
192
193    private final int mUserId;
194
195    /**
196     * Interface used to receive the created session.
197     * @hide
198     */
199    @SystemApi
200    public abstract static class SessionCallback {
201        /**
202         * This is called after {@link TvInputManager#createSession} has been processed.
203         *
204         * @param session A {@link TvInputManager.Session} instance created. This can be
205         *            {@code null} if the creation request failed.
206         */
207        public void onSessionCreated(Session session) {
208        }
209
210        /**
211         * This is called when {@link TvInputManager.Session} is released.
212         * This typically happens when the process hosting the session has crashed or been killed.
213         *
214         * @param session A {@link TvInputManager.Session} instance released.
215         */
216        public void onSessionReleased(Session session) {
217        }
218
219        /**
220         * This is called when the channel of this session is changed by the underlying TV input
221         * without any {@link TvInputManager.Session#tune(Uri)} request.
222         *
223         * @param session A {@link TvInputManager.Session} associated with this callback.
224         * @param channelUri The URI of a channel.
225         */
226        public void onChannelRetuned(Session session, Uri channelUri) {
227        }
228
229        /**
230         * This is called when the track information of the session has been changed.
231         *
232         * @param session A {@link TvInputManager.Session} associated with this callback.
233         * @param tracks A list which includes track information.
234         */
235        public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
236        }
237
238        /**
239         * This is called when a track for a given type is selected.
240         *
241         * @param session A {@link TvInputManager.Session} associated with this callback.
242         * @param type The type of the selected track. The type can be
243         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
244         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
245         * @param trackId The ID of the selected track. When {@code null} the currently selected
246         *            track for a given type should be unselected.
247         */
248        public void onTrackSelected(Session session, int type, String trackId) {
249        }
250
251        /**
252         * This is invoked when the video size has been changed. It is also called when the first
253         * time video size information becomes available after the session is tuned to a specific
254         * channel.
255         *
256         * @param session A {@link TvInputManager.Session} associated with this callback.
257         * @param width The width of the video.
258         * @param height The height of the video.
259         */
260        public void onVideoSizeChanged(Session session, int width, int height) {
261        }
262
263        /**
264         * This is called when the video is available, so the TV input starts the playback.
265         *
266         * @param session A {@link TvInputManager.Session} associated with this callback.
267         */
268        public void onVideoAvailable(Session session) {
269        }
270
271        /**
272         * This is called when the video is not available, so the TV input stops the playback.
273         *
274         * @param session A {@link TvInputManager.Session} associated with this callback
275         * @param reason The reason why the TV input stopped the playback:
276         * <ul>
277         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
278         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
279         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
280         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
281         * </ul>
282         */
283        public void onVideoUnavailable(Session session, int reason) {
284        }
285
286        /**
287         * This is called when the current program content turns out to be allowed to watch since
288         * its content rating is not blocked by parental controls.
289         *
290         * @param session A {@link TvInputManager.Session} associated with this callback
291         */
292        public void onContentAllowed(Session session) {
293        }
294
295        /**
296         * This is called when the current program content turns out to be not allowed to watch
297         * since its content rating is blocked by parental controls.
298         *
299         * @param session A {@link TvInputManager.Session} associated with this callback
300         * @param rating The content ration of the blocked program.
301         */
302        public void onContentBlocked(Session session, TvContentRating rating) {
303        }
304
305        /**
306         * This is called when {@link TvInputService.Session#layoutSurface} is called to change the
307         * layout of surface.
308         *
309         * @param session A {@link TvInputManager.Session} associated with this callback
310         * @param left Left position.
311         * @param top Top position.
312         * @param right Right position.
313         * @param bottom Bottom position.
314         * @hide
315         */
316        @SystemApi
317        public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
318        }
319
320        /**
321         * This is called when a custom event has been sent from this session.
322         *
323         * @param session A {@link TvInputManager.Session} associated with this callback
324         * @param eventType The type of the event.
325         * @param eventArgs Optional arguments of the event.
326         * @hide
327         */
328        @SystemApi
329        public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
330        }
331    }
332
333    private static final class SessionCallbackRecord {
334        private final SessionCallback mSessionCallback;
335        private final Handler mHandler;
336        private Session mSession;
337
338        SessionCallbackRecord(SessionCallback sessionCallback,
339                Handler handler) {
340            mSessionCallback = sessionCallback;
341            mHandler = handler;
342        }
343
344        void postSessionCreated(final Session session) {
345            mSession = session;
346            mHandler.post(new Runnable() {
347                @Override
348                public void run() {
349                    mSessionCallback.onSessionCreated(session);
350                }
351            });
352        }
353
354        void postSessionReleased() {
355            mHandler.post(new Runnable() {
356                @Override
357                public void run() {
358                    mSessionCallback.onSessionReleased(mSession);
359                }
360            });
361        }
362
363        void postChannelRetuned(final Uri channelUri) {
364            mHandler.post(new Runnable() {
365                @Override
366                public void run() {
367                    mSessionCallback.onChannelRetuned(mSession, channelUri);
368                }
369            });
370        }
371
372        void postTracksChanged(final List<TvTrackInfo> tracks) {
373            mHandler.post(new Runnable() {
374                @Override
375                public void run() {
376                    mSessionCallback.onTracksChanged(mSession, tracks);
377                }
378            });
379        }
380
381        void postTrackSelected(final int type, final String trackId) {
382            mHandler.post(new Runnable() {
383                @Override
384                public void run() {
385                    mSessionCallback.onTrackSelected(mSession, type, trackId);
386                }
387            });
388        }
389
390        void postVideoSizeChanged(final int width, final int height) {
391            mHandler.post(new Runnable() {
392                @Override
393                public void run() {
394                    mSessionCallback.onVideoSizeChanged(mSession, width, height);
395                }
396            });
397        }
398
399        void postVideoAvailable() {
400            mHandler.post(new Runnable() {
401                @Override
402                public void run() {
403                    mSessionCallback.onVideoAvailable(mSession);
404                }
405            });
406        }
407
408        void postVideoUnavailable(final int reason) {
409            mHandler.post(new Runnable() {
410                @Override
411                public void run() {
412                    mSessionCallback.onVideoUnavailable(mSession, reason);
413                }
414            });
415        }
416
417        void postContentAllowed() {
418            mHandler.post(new Runnable() {
419                @Override
420                public void run() {
421                    mSessionCallback.onContentAllowed(mSession);
422                }
423            });
424        }
425
426        void postContentBlocked(final TvContentRating rating) {
427            mHandler.post(new Runnable() {
428                @Override
429                public void run() {
430                    mSessionCallback.onContentBlocked(mSession, rating);
431                }
432            });
433        }
434
435        void postLayoutSurface(final int left, final int top, final int right,
436                final int bottom) {
437            mHandler.post(new Runnable() {
438                @Override
439                public void run() {
440                    mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom);
441                }
442            });
443        }
444
445        void postSessionEvent(final String eventType, final Bundle eventArgs) {
446            mHandler.post(new Runnable() {
447                @Override
448                public void run() {
449                    mSessionCallback.onSessionEvent(mSession, eventType, eventArgs);
450                }
451            });
452        }
453    }
454
455    /**
456     * Callback used to monitor status of the TV input.
457     */
458    public abstract static class TvInputCallback {
459        /**
460         * This is called when the state of a given TV input is changed.
461         *
462         * @param inputId The id of the TV input.
463         * @param state State of the TV input. The value is one of the following:
464         * <ul>
465         * <li>{@link TvInputManager#INPUT_STATE_CONNECTED}
466         * <li>{@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY}
467         * <li>{@link TvInputManager#INPUT_STATE_DISCONNECTED}
468         * </ul>
469         */
470        public void onInputStateChanged(String inputId, int state) {
471        }
472
473        /**
474         * This is called when a TV input is added.
475         *
476         * @param inputId The id of the TV input.
477         */
478        public void onInputAdded(String inputId) {
479        }
480
481        /**
482         * This is called when a TV input is removed.
483         *
484         * @param inputId The id of the TV input.
485         */
486        public void onInputRemoved(String inputId) {
487        }
488
489        /**
490         * This is called when a TV input is updated. The update of TV input happens when it is
491         * reinstalled or the media on which the newer version of TV input exists is
492         * available/unavailable.
493         *
494         * @param inputId The id of the TV input.
495         * @hide
496         */
497        @SystemApi
498        public void onInputUpdated(String inputId) {
499        }
500    }
501
502    private static final class TvInputCallbackRecord {
503        private final TvInputCallback mCallback;
504        private final Handler mHandler;
505
506        public TvInputCallbackRecord(TvInputCallback callback, Handler handler) {
507            mCallback = callback;
508            mHandler = handler;
509        }
510
511        public TvInputCallback getCallback() {
512            return mCallback;
513        }
514
515        public void postInputStateChanged(final String inputId, final int state) {
516            mHandler.post(new Runnable() {
517                @Override
518                public void run() {
519                    mCallback.onInputStateChanged(inputId, state);
520                }
521            });
522        }
523
524        public void postInputAdded(final String inputId) {
525            mHandler.post(new Runnable() {
526                @Override
527                public void run() {
528                    mCallback.onInputAdded(inputId);
529                }
530            });
531        }
532
533        public void postInputRemoved(final String inputId) {
534            mHandler.post(new Runnable() {
535                @Override
536                public void run() {
537                    mCallback.onInputRemoved(inputId);
538                }
539            });
540        }
541
542        public void postInputUpdated(final String inputId) {
543            mHandler.post(new Runnable() {
544                @Override
545                public void run() {
546                    mCallback.onInputUpdated(inputId);
547                }
548            });
549        }
550    }
551
552    /**
553     * Interface used to receive events from Hardware objects.
554     * @hide
555     */
556    @SystemApi
557    public abstract static class HardwareCallback {
558        public abstract void onReleased();
559        public abstract void onStreamConfigChanged(TvStreamConfig[] configs);
560    }
561
562    /**
563     * @hide
564     */
565    public TvInputManager(ITvInputManager service, int userId) {
566        mService = service;
567        mUserId = userId;
568        mClient = new ITvInputClient.Stub() {
569            @Override
570            public void onSessionCreated(String inputId, IBinder token, InputChannel channel,
571                    int seq) {
572                synchronized (mSessionCallbackRecordMap) {
573                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
574                    if (record == null) {
575                        Log.e(TAG, "Callback not found for " + token);
576                        return;
577                    }
578                    Session session = null;
579                    if (token != null) {
580                        session = new Session(token, channel, mService, mUserId, seq,
581                                mSessionCallbackRecordMap);
582                    }
583                    record.postSessionCreated(session);
584                }
585            }
586
587            @Override
588            public void onSessionReleased(int seq) {
589                synchronized (mSessionCallbackRecordMap) {
590                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
591                    mSessionCallbackRecordMap.delete(seq);
592                    if (record == null) {
593                        Log.e(TAG, "Callback not found for seq:" + seq);
594                        return;
595                    }
596                    record.mSession.releaseInternal();
597                    record.postSessionReleased();
598                }
599            }
600
601            @Override
602            public void onChannelRetuned(Uri channelUri, int seq) {
603                synchronized (mSessionCallbackRecordMap) {
604                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
605                    if (record == null) {
606                        Log.e(TAG, "Callback not found for seq " + seq);
607                        return;
608                    }
609                    record.postChannelRetuned(channelUri);
610                }
611            }
612
613            @Override
614            public void onTracksChanged(List<TvTrackInfo> tracks, int seq) {
615                synchronized (mSessionCallbackRecordMap) {
616                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
617                    if (record == null) {
618                        Log.e(TAG, "Callback not found for seq " + seq);
619                        return;
620                    }
621                    if (record.mSession.updateTracks(tracks)) {
622                        record.postTracksChanged(tracks);
623                        postVideoSizeChangedIfNeededLocked(record);
624                    }
625                }
626            }
627
628            @Override
629            public void onTrackSelected(int type, String trackId, int seq) {
630                synchronized (mSessionCallbackRecordMap) {
631                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
632                    if (record == null) {
633                        Log.e(TAG, "Callback not found for seq " + seq);
634                        return;
635                    }
636                    if (record.mSession.updateTrackSelection(type, trackId)) {
637                        record.postTrackSelected(type, trackId);
638                        postVideoSizeChangedIfNeededLocked(record);
639                    }
640                }
641            }
642
643            private void postVideoSizeChangedIfNeededLocked(SessionCallbackRecord record) {
644                TvTrackInfo track = record.mSession.getVideoTrackToNotify();
645                if (track != null) {
646                    record.postVideoSizeChanged(track.getVideoWidth(), track.getVideoHeight());
647                }
648            }
649
650            @Override
651            public void onVideoAvailable(int seq) {
652                synchronized (mSessionCallbackRecordMap) {
653                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
654                    if (record == null) {
655                        Log.e(TAG, "Callback not found for seq " + seq);
656                        return;
657                    }
658                    record.postVideoAvailable();
659                }
660            }
661
662            @Override
663            public void onVideoUnavailable(int reason, int seq) {
664                synchronized (mSessionCallbackRecordMap) {
665                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
666                    if (record == null) {
667                        Log.e(TAG, "Callback not found for seq " + seq);
668                        return;
669                    }
670                    record.postVideoUnavailable(reason);
671                }
672            }
673
674            @Override
675            public void onContentAllowed(int seq) {
676                synchronized (mSessionCallbackRecordMap) {
677                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
678                    if (record == null) {
679                        Log.e(TAG, "Callback not found for seq " + seq);
680                        return;
681                    }
682                    record.postContentAllowed();
683                }
684            }
685
686            @Override
687            public void onContentBlocked(String rating, int seq) {
688                synchronized (mSessionCallbackRecordMap) {
689                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
690                    if (record == null) {
691                        Log.e(TAG, "Callback not found for seq " + seq);
692                        return;
693                    }
694                    record.postContentBlocked(TvContentRating.unflattenFromString(rating));
695                }
696            }
697
698            @Override
699            public void onLayoutSurface(int left, int top, int right, int bottom, int seq) {
700                synchronized (mSessionCallbackRecordMap) {
701                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
702                    if (record == null) {
703                        Log.e(TAG, "Callback not found for seq " + seq);
704                        return;
705                    }
706                    record.postLayoutSurface(left, top, right, bottom);
707                }
708            }
709
710            @Override
711            public void onSessionEvent(String eventType, Bundle eventArgs, int seq) {
712                synchronized (mSessionCallbackRecordMap) {
713                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
714                    if (record == null) {
715                        Log.e(TAG, "Callback not found for seq " + seq);
716                        return;
717                    }
718                    record.postSessionEvent(eventType, eventArgs);
719                }
720            }
721        };
722        mManagerCallback = new ITvInputManagerCallback.Stub() {
723            @Override
724            public void onInputStateChanged(String inputId, int state) {
725                synchronized (mLock) {
726                    mStateMap.put(inputId, state);
727                    for (TvInputCallbackRecord record : mCallbackRecords) {
728                        record.postInputStateChanged(inputId, state);
729                    }
730                }
731            }
732
733            @Override
734            public void onInputAdded(String inputId) {
735                synchronized (mLock) {
736                    mStateMap.put(inputId, INPUT_STATE_CONNECTED);
737                    for (TvInputCallbackRecord record : mCallbackRecords) {
738                        record.postInputAdded(inputId);
739                    }
740                }
741            }
742
743            @Override
744            public void onInputRemoved(String inputId) {
745                synchronized (mLock) {
746                    mStateMap.remove(inputId);
747                    for (TvInputCallbackRecord record : mCallbackRecords) {
748                        record.postInputRemoved(inputId);
749                    }
750                }
751            }
752
753            @Override
754            public void onInputUpdated(String inputId) {
755                synchronized (mLock) {
756                    for (TvInputCallbackRecord record : mCallbackRecords) {
757                        record.postInputUpdated(inputId);
758                    }
759                }
760            }
761        };
762        try {
763            if (mService != null) {
764                mService.registerCallback(mManagerCallback, mUserId);
765                List<TvInputInfo> infos = mService.getTvInputList(mUserId);
766                synchronized (mLock) {
767                    for (TvInputInfo info : infos) {
768                        String inputId = info.getId();
769                        int state = mService.getTvInputState(inputId, mUserId);
770                        if (state != INPUT_STATE_UNKNOWN) {
771                            mStateMap.put(inputId, state);
772                        }
773                    }
774                }
775            }
776        } catch (RemoteException e) {
777            Log.e(TAG, "TvInputManager initialization failed: " + e);
778        }
779    }
780
781    /**
782     * Returns the complete list of TV inputs on the system.
783     *
784     * @return List of {@link TvInputInfo} for each TV input that describes its meta information.
785     */
786    public List<TvInputInfo> getTvInputList() {
787        try {
788            return mService.getTvInputList(mUserId);
789        } catch (RemoteException e) {
790            throw new RuntimeException(e);
791        }
792    }
793
794    /**
795     * Returns the {@link TvInputInfo} for a given TV input.
796     *
797     * @param inputId The ID of the TV input.
798     * @return the {@link TvInputInfo} for a given TV input. {@code null} if not found.
799     */
800    public TvInputInfo getTvInputInfo(String inputId) {
801        if (inputId == null) {
802            throw new IllegalArgumentException("inputId cannot be null");
803        }
804        try {
805            return mService.getTvInputInfo(inputId, mUserId);
806        } catch (RemoteException e) {
807            throw new RuntimeException(e);
808        }
809    }
810
811    /**
812     * Returns the state of a given TV input. It returns one of the following:
813     * <ul>
814     * <li>{@link #INPUT_STATE_CONNECTED}
815     * <li>{@link #INPUT_STATE_CONNECTED_STANDBY}
816     * <li>{@link #INPUT_STATE_DISCONNECTED}
817     * </ul>
818     *
819     * @param inputId The id of the TV input.
820     * @throws IllegalArgumentException if the argument is {@code null} or if there is no
821     *        {@link TvInputInfo} corresponding to {@code inputId}.
822     */
823    public int getInputState(String inputId) {
824        if (inputId == null) {
825            throw new IllegalArgumentException("inputId cannot be null");
826        }
827        synchronized (mLock) {
828            Integer state = mStateMap.get(inputId);
829            if (state == null) {
830                throw new IllegalArgumentException("Unrecognized input ID: " + inputId);
831            }
832            return state.intValue();
833        }
834    }
835
836    /**
837     * Registers a {@link TvInputCallback}.
838     *
839     * @param callback A callback used to monitor status of the TV inputs.
840     * @param handler A {@link Handler} that the status change will be delivered to.
841     * @throws IllegalArgumentException if any of the arguments is {@code null}.
842     */
843    public void registerCallback(TvInputCallback callback, Handler handler) {
844        if (callback == null) {
845            throw new IllegalArgumentException("callback cannot be null");
846        }
847        if (handler == null) {
848            throw new IllegalArgumentException("handler cannot be null");
849        }
850        synchronized (mLock) {
851            mCallbackRecords.add(new TvInputCallbackRecord(callback, handler));
852        }
853    }
854
855    /**
856     * Unregisters the existing {@link TvInputCallback}.
857     *
858     * @param callback The existing callback to remove.
859     * @throws IllegalArgumentException if any of the arguments is {@code null}.
860     */
861    public void unregisterCallback(final TvInputCallback callback) {
862        if (callback == null) {
863            throw new IllegalArgumentException("callback cannot be null");
864        }
865        synchronized (mLock) {
866            for (Iterator<TvInputCallbackRecord> it = mCallbackRecords.iterator();
867                    it.hasNext(); ) {
868                TvInputCallbackRecord record = it.next();
869                if (record.getCallback() == callback) {
870                    it.remove();
871                    break;
872                }
873            }
874        }
875    }
876
877    /**
878     * Returns the user's parental controls enabled state.
879     *
880     * @return {@code true} if the user enabled the parental controls, {@code false} otherwise.
881     */
882    public boolean isParentalControlsEnabled() {
883        try {
884            return mService.isParentalControlsEnabled(mUserId);
885        } catch (RemoteException e) {
886            throw new RuntimeException(e);
887        }
888    }
889
890    /**
891     * Sets the user's parental controls enabled state.
892     *
893     * @param enabled The user's parental controls enabled state. {@code true} if the user enabled
894     *            the parental controls, {@code false} otherwise.
895     * @see #isParentalControlsEnabled
896     * @hide
897     */
898    @SystemApi
899    public void setParentalControlsEnabled(boolean enabled) {
900        try {
901            mService.setParentalControlsEnabled(enabled, mUserId);
902        } catch (RemoteException e) {
903            throw new RuntimeException(e);
904        }
905    }
906
907    /**
908     * Checks whether a given TV content rating is blocked by the user.
909     *
910     * @param rating The TV content rating to check.
911     * @return {@code true} if the given TV content rating is blocked, {@code false} otherwise.
912     */
913    public boolean isRatingBlocked(TvContentRating rating) {
914        if (rating == null) {
915            throw new IllegalArgumentException("rating cannot be null");
916        }
917        try {
918            return mService.isRatingBlocked(rating.flattenToString(), mUserId);
919        } catch (RemoteException e) {
920            throw new RuntimeException(e);
921        }
922    }
923
924    /**
925     * Returns the list of blocked content ratings.
926     *
927     * @return the list of content ratings blocked by the user.
928     * @hide
929     */
930    @SystemApi
931    public List<TvContentRating> getBlockedRatings() {
932        try {
933            List<TvContentRating> ratings = new ArrayList<TvContentRating>();
934            for (String rating : mService.getBlockedRatings(mUserId)) {
935                ratings.add(TvContentRating.unflattenFromString(rating));
936            }
937            return ratings;
938        } catch (RemoteException e) {
939            throw new RuntimeException(e);
940        }
941    }
942
943    /**
944     * Adds a user blocked content rating.
945     *
946     * @param rating The content rating to block.
947     * @see #isRatingBlocked
948     * @see #removeBlockedRating
949     * @hide
950     */
951    @SystemApi
952    public void addBlockedRating(TvContentRating rating) {
953        if (rating == null) {
954            throw new IllegalArgumentException("rating cannot be null");
955        }
956        try {
957            mService.addBlockedRating(rating.flattenToString(), mUserId);
958        } catch (RemoteException e) {
959            throw new RuntimeException(e);
960        }
961    }
962
963    /**
964     * Removes a user blocked content rating.
965     *
966     * @param rating The content rating to unblock.
967     * @see #isRatingBlocked
968     * @see #addBlockedRating
969     * @hide
970     */
971    @SystemApi
972    public void removeBlockedRating(TvContentRating rating) {
973        if (rating == null) {
974            throw new IllegalArgumentException("rating cannot be null");
975        }
976        try {
977            mService.removeBlockedRating(rating.flattenToString(), mUserId);
978        } catch (RemoteException e) {
979            throw new RuntimeException(e);
980        }
981    }
982
983    /**
984     * Returns the list of all TV content rating systems defined.
985     * @hide
986     */
987    @SystemApi
988    public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() {
989        try {
990            return mService.getTvContentRatingSystemList(mUserId);
991        } catch (RemoteException e) {
992            throw new RuntimeException(e);
993        }
994    }
995
996    /**
997     * Creates a {@link Session} for a given TV input.
998     * <p>
999     * The number of sessions that can be created at the same time is limited by the capability of
1000     * the given TV input.
1001     * </p>
1002     *
1003     * @param inputId The id of the TV input.
1004     * @param callback A callback used to receive the created session.
1005     * @param handler A {@link Handler} that the session creation will be delivered to.
1006     * @throws IllegalArgumentException if any of the arguments is {@code null}.
1007     * @hide
1008     */
1009    @SystemApi
1010    public void createSession(String inputId, final SessionCallback callback,
1011            Handler handler) {
1012        if (inputId == null) {
1013            throw new IllegalArgumentException("id cannot be null");
1014        }
1015        if (callback == null) {
1016            throw new IllegalArgumentException("callback cannot be null");
1017        }
1018        if (handler == null) {
1019            throw new IllegalArgumentException("handler cannot be null");
1020        }
1021        SessionCallbackRecord record = new SessionCallbackRecord(callback, handler);
1022        synchronized (mSessionCallbackRecordMap) {
1023            int seq = mNextSeq++;
1024            mSessionCallbackRecordMap.put(seq, record);
1025            try {
1026                mService.createSession(mClient, inputId, seq, mUserId);
1027            } catch (RemoteException e) {
1028                throw new RuntimeException(e);
1029            }
1030        }
1031    }
1032
1033    /**
1034     * Returns the TvStreamConfig list of the given TV input.
1035     *
1036     * If you are using {@link Hardware} object from {@link
1037     * #acquireTvInputHardware}, you should get the list of available streams
1038     * from {@link HardwareCallback#onStreamConfigChanged} method, not from
1039     * here. This method is designed to be used with {@link #captureFrame} in
1040     * capture scenarios specifically and not suitable for any other use.
1041     *
1042     * @param inputId the id of the TV input.
1043     * @return List of {@link TvStreamConfig} which is available for capturing
1044     *   of the given TV input.
1045     * @hide
1046     */
1047    @SystemApi
1048    public List<TvStreamConfig> getAvailableTvStreamConfigList(String inputId) {
1049        try {
1050            return mService.getAvailableTvStreamConfigList(inputId, mUserId);
1051        } catch (RemoteException e) {
1052            throw new RuntimeException(e);
1053        }
1054    }
1055
1056    /**
1057     * Take a snapshot of the given TV input into the provided Surface.
1058     *
1059     * @param inputId the id of the TV input.
1060     * @param surface the {@link Surface} to which the snapshot is captured.
1061     * @param config the {@link TvStreamConfig} which is used for capturing.
1062     * @return true when the {@link Surface} is ready to be captured.
1063     * @hide
1064     */
1065    @SystemApi
1066    public boolean captureFrame(String inputId, Surface surface, TvStreamConfig config) {
1067        try {
1068            return mService.captureFrame(inputId, surface, config, mUserId);
1069        } catch (RemoteException e) {
1070            throw new RuntimeException(e);
1071        }
1072    }
1073
1074    /**
1075     * Returns true if there is only a single TV input session.
1076     *
1077     * @hide
1078     */
1079    @SystemApi
1080    public boolean isSingleSessionActive() {
1081        try {
1082            return mService.isSingleSessionActive(mUserId);
1083        } catch (RemoteException e) {
1084            throw new RuntimeException(e);
1085        }
1086    }
1087
1088    /**
1089     * Returns a list of TvInputHardwareInfo objects representing available hardware.
1090     *
1091     * @hide
1092     */
1093    @SystemApi
1094    public List<TvInputHardwareInfo> getHardwareList() {
1095        try {
1096            return mService.getHardwareList();
1097        } catch (RemoteException e) {
1098            throw new RuntimeException(e);
1099        }
1100    }
1101
1102    /**
1103     * Returns acquired TvInputManager.Hardware object for given deviceId.
1104     *
1105     * If there are other Hardware object acquired for the same deviceId, calling this method will
1106     * preempt the previously acquired object and report {@link HardwareCallback#onReleased} to the
1107     * old object.
1108     *
1109     * @hide
1110     */
1111    @SystemApi
1112    public Hardware acquireTvInputHardware(int deviceId, final HardwareCallback callback,
1113            TvInputInfo info) {
1114        try {
1115            return new Hardware(
1116                    mService.acquireTvInputHardware(deviceId, new ITvInputHardwareCallback.Stub() {
1117                @Override
1118                public void onReleased() {
1119                    callback.onReleased();
1120                }
1121
1122                @Override
1123                public void onStreamConfigChanged(TvStreamConfig[] configs) {
1124                    callback.onStreamConfigChanged(configs);
1125                }
1126            }, info, mUserId));
1127        } catch (RemoteException e) {
1128            throw new RuntimeException(e);
1129        }
1130    }
1131
1132    /**
1133     * Releases previously acquired hardware object.
1134     *
1135     * @hide
1136     */
1137    @SystemApi
1138    public void releaseTvInputHardware(int deviceId, Hardware hardware) {
1139        try {
1140            mService.releaseTvInputHardware(deviceId, hardware.getInterface(), mUserId);
1141        } catch (RemoteException e) {
1142            throw new RuntimeException(e);
1143        }
1144    }
1145
1146    /**
1147     * The Session provides the per-session functionality of TV inputs.
1148     * @hide
1149     */
1150    @SystemApi
1151    public static final class Session {
1152        static final int DISPATCH_IN_PROGRESS = -1;
1153        static final int DISPATCH_NOT_HANDLED = 0;
1154        static final int DISPATCH_HANDLED = 1;
1155
1156        private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
1157
1158        private final ITvInputManager mService;
1159        private final int mUserId;
1160        private final int mSeq;
1161
1162        // For scheduling input event handling on the main thread. This also serves as a lock to
1163        // protect pending input events and the input channel.
1164        private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
1165
1166        private final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20);
1167        private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20);
1168        private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
1169
1170        private IBinder mToken;
1171        private TvInputEventSender mSender;
1172        private InputChannel mChannel;
1173
1174        private final Object mTrackLock = new Object();
1175        // @GuardedBy("mTrackLock")
1176        private final List<TvTrackInfo> mAudioTracks = new ArrayList<TvTrackInfo>();
1177        // @GuardedBy("mTrackLock")
1178        private final List<TvTrackInfo> mVideoTracks = new ArrayList<TvTrackInfo>();
1179        // @GuardedBy("mTrackLock")
1180        private final List<TvTrackInfo> mSubtitleTracks = new ArrayList<TvTrackInfo>();
1181        // @GuardedBy("mTrackLock")
1182        private String mSelectedAudioTrackId;
1183        // @GuardedBy("mTrackLock")
1184        private String mSelectedVideoTrackId;
1185        // @GuardedBy("mTrackLock")
1186        private String mSelectedSubtitleTrackId;
1187        // @GuardedBy("mTrackLock")
1188        private int mVideoWidth;
1189        // @GuardedBy("mTrackLock")
1190        private int mVideoHeight;
1191
1192        private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
1193                int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
1194            mToken = token;
1195            mChannel = channel;
1196            mService = service;
1197            mUserId = userId;
1198            mSeq = seq;
1199            mSessionCallbackRecordMap = sessionCallbackRecordMap;
1200        }
1201
1202        /**
1203         * Releases this session.
1204         */
1205        public void release() {
1206            if (mToken == null) {
1207                Log.w(TAG, "The session has been already released");
1208                return;
1209            }
1210            try {
1211                mService.releaseSession(mToken, mUserId);
1212            } catch (RemoteException e) {
1213                throw new RuntimeException(e);
1214            }
1215
1216            releaseInternal();
1217        }
1218
1219        /**
1220         * Sets this as the main session. The main session is a session whose corresponding TV
1221         * input determines the HDMI-CEC active source device.
1222         *
1223         * @see TvView#setMain
1224         */
1225        void setMain() {
1226            if (mToken == null) {
1227                Log.w(TAG, "The session has been already released");
1228                return;
1229            }
1230            try {
1231                mService.setMainSession(mToken, mUserId);
1232            } catch (RemoteException e) {
1233                throw new RuntimeException(e);
1234            }
1235        }
1236
1237        /**
1238         * Sets the {@link android.view.Surface} for this session.
1239         *
1240         * @param surface A {@link android.view.Surface} used to render video.
1241         */
1242        public void setSurface(Surface surface) {
1243            if (mToken == null) {
1244                Log.w(TAG, "The session has been already released");
1245                return;
1246            }
1247            // surface can be null.
1248            try {
1249                mService.setSurface(mToken, surface, mUserId);
1250            } catch (RemoteException e) {
1251                throw new RuntimeException(e);
1252            }
1253        }
1254
1255        /**
1256         * Notifies of any structural changes (format or size) of the {@link Surface}
1257         * passed by {@link #setSurface}.
1258         *
1259         * @param format The new PixelFormat of the {@link Surface}.
1260         * @param width The new width of the {@link Surface}.
1261         * @param height The new height of the {@link Surface}.
1262         * @hide
1263         */
1264        @SystemApi
1265        public void dispatchSurfaceChanged(int format, int width, int height) {
1266            if (mToken == null) {
1267                Log.w(TAG, "The session has been already released");
1268                return;
1269            }
1270            try {
1271                mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
1272            } catch (RemoteException e) {
1273                throw new RuntimeException(e);
1274            }
1275        }
1276
1277        /**
1278         * Sets the relative stream volume of this session to handle a change of audio focus.
1279         *
1280         * @param volume A volume value between 0.0f to 1.0f.
1281         * @throws IllegalArgumentException if the volume value is out of range.
1282         */
1283        public void setStreamVolume(float volume) {
1284            if (mToken == null) {
1285                Log.w(TAG, "The session has been already released");
1286                return;
1287            }
1288            try {
1289                if (volume < 0.0f || volume > 1.0f) {
1290                    throw new IllegalArgumentException("volume should be between 0.0f and 1.0f");
1291                }
1292                mService.setVolume(mToken, volume, mUserId);
1293            } catch (RemoteException e) {
1294                throw new RuntimeException(e);
1295            }
1296        }
1297
1298        /**
1299         * Tunes to a given channel.
1300         *
1301         * @param channelUri The URI of a channel.
1302         * @throws IllegalArgumentException if the argument is {@code null}.
1303         */
1304        public void tune(Uri channelUri) {
1305            tune(channelUri, null);
1306        }
1307
1308        /**
1309         * Tunes to a given channel.
1310         *
1311         * @param channelUri The URI of a channel.
1312         * @param params A set of extra parameters which might be handled with this tune event.
1313         * @throws IllegalArgumentException if {@code channelUri} is {@code null}.
1314         * @hide
1315         */
1316        @SystemApi
1317        public void tune(Uri channelUri, Bundle params) {
1318            if (channelUri == null) {
1319                throw new IllegalArgumentException("channelUri cannot be null");
1320            }
1321            if (mToken == null) {
1322                Log.w(TAG, "The session has been already released");
1323                return;
1324            }
1325            synchronized (mTrackLock) {
1326                mAudioTracks.clear();
1327                mVideoTracks.clear();
1328                mSubtitleTracks.clear();
1329                mSelectedAudioTrackId = null;
1330                mSelectedVideoTrackId = null;
1331                mSelectedSubtitleTrackId = null;
1332                mVideoWidth = 0;
1333                mVideoHeight = 0;
1334            }
1335            try {
1336                mService.tune(mToken, channelUri, params, mUserId);
1337            } catch (RemoteException e) {
1338                throw new RuntimeException(e);
1339            }
1340        }
1341
1342        /**
1343         * Enables or disables the caption for this session.
1344         *
1345         * @param enabled {@code true} to enable, {@code false} to disable.
1346         */
1347        public void setCaptionEnabled(boolean enabled) {
1348            if (mToken == null) {
1349                Log.w(TAG, "The session has been already released");
1350                return;
1351            }
1352            try {
1353                mService.setCaptionEnabled(mToken, enabled, mUserId);
1354            } catch (RemoteException e) {
1355                throw new RuntimeException(e);
1356            }
1357        }
1358
1359        /**
1360         * Selects a track.
1361         *
1362         * @param type The type of the track to select. The type can be
1363         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
1364         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
1365         * @param trackId The ID of the track to select. When {@code null}, the currently selected
1366         *            track of the given type will be unselected.
1367         * @see #getTracks
1368         */
1369        public void selectTrack(int type, String trackId) {
1370            synchronized (mTrackLock) {
1371                if (type == TvTrackInfo.TYPE_AUDIO) {
1372                    if (trackId != null && !containsTrack(mAudioTracks, trackId)) {
1373                        Log.w(TAG, "Invalid audio trackId: " + trackId);
1374                        return;
1375                    }
1376                } else if (type == TvTrackInfo.TYPE_VIDEO) {
1377                    if (trackId != null && !containsTrack(mVideoTracks, trackId)) {
1378                        Log.w(TAG, "Invalid video trackId: " + trackId);
1379                        return;
1380                    }
1381                } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
1382                    if (trackId != null && !containsTrack(mSubtitleTracks, trackId)) {
1383                        Log.w(TAG, "Invalid subtitle trackId: " + trackId);
1384                        return;
1385                    }
1386                } else {
1387                    throw new IllegalArgumentException("invalid type: " + type);
1388                }
1389            }
1390            if (mToken == null) {
1391                Log.w(TAG, "The session has been already released");
1392                return;
1393            }
1394            try {
1395                mService.selectTrack(mToken, type, trackId, mUserId);
1396            } catch (RemoteException e) {
1397                throw new RuntimeException(e);
1398            }
1399        }
1400
1401        private boolean containsTrack(List<TvTrackInfo> tracks, String trackId) {
1402            for (TvTrackInfo track : tracks) {
1403                if (track.getId().equals(trackId)) {
1404                    return true;
1405                }
1406            }
1407            return false;
1408        }
1409
1410        /**
1411         * Returns the list of tracks for a given type. Returns {@code null} if the information is
1412         * not available.
1413         *
1414         * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
1415         *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
1416         * @return the list of tracks for the given type.
1417         */
1418        public List<TvTrackInfo> getTracks(int type) {
1419            synchronized (mTrackLock) {
1420                if (type == TvTrackInfo.TYPE_AUDIO) {
1421                    if (mAudioTracks == null) {
1422                        return null;
1423                    }
1424                    return new ArrayList<TvTrackInfo>(mAudioTracks);
1425                } else if (type == TvTrackInfo.TYPE_VIDEO) {
1426                    if (mVideoTracks == null) {
1427                        return null;
1428                    }
1429                    return new ArrayList<TvTrackInfo>(mVideoTracks);
1430                } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
1431                    if (mSubtitleTracks == null) {
1432                        return null;
1433                    }
1434                    return new ArrayList<TvTrackInfo>(mSubtitleTracks);
1435                }
1436            }
1437            throw new IllegalArgumentException("invalid type: " + type);
1438        }
1439
1440        /**
1441         * Returns the selected track for a given type. Returns {@code null} if the information is
1442         * not available or any of the tracks for the given type is not selected.
1443         *
1444         * @return the ID of the selected track.
1445         * @see #selectTrack
1446         */
1447        public String getSelectedTrack(int type) {
1448            synchronized (mTrackLock) {
1449                if (type == TvTrackInfo.TYPE_AUDIO) {
1450                    return mSelectedAudioTrackId;
1451                } else if (type == TvTrackInfo.TYPE_VIDEO) {
1452                    return mSelectedVideoTrackId;
1453                } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
1454                    return mSelectedSubtitleTrackId;
1455                }
1456            }
1457            throw new IllegalArgumentException("invalid type: " + type);
1458        }
1459
1460        /**
1461         * Responds to onTracksChanged() and updates the internal track information. Returns true if
1462         * there is an update.
1463         */
1464        boolean updateTracks(List<TvTrackInfo> tracks) {
1465            synchronized (mTrackLock) {
1466                mAudioTracks.clear();
1467                mVideoTracks.clear();
1468                mSubtitleTracks.clear();
1469                for (TvTrackInfo track : tracks) {
1470                    if (track.getType() == TvTrackInfo.TYPE_AUDIO) {
1471                        mAudioTracks.add(track);
1472                    } else if (track.getType() == TvTrackInfo.TYPE_VIDEO) {
1473                        mVideoTracks.add(track);
1474                    } else if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
1475                        mSubtitleTracks.add(track);
1476                    }
1477                }
1478                return !mAudioTracks.isEmpty() || !mVideoTracks.isEmpty()
1479                        || !mSubtitleTracks.isEmpty();
1480            }
1481        }
1482
1483        /**
1484         * Responds to onTrackSelected() and updates the internal track selection information.
1485         * Returns true if there is an update.
1486         */
1487        boolean updateTrackSelection(int type, String trackId) {
1488            synchronized (mTrackLock) {
1489                if (type == TvTrackInfo.TYPE_AUDIO && trackId != mSelectedAudioTrackId) {
1490                    mSelectedAudioTrackId = trackId;
1491                    return true;
1492                } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != mSelectedVideoTrackId) {
1493                    mSelectedVideoTrackId = trackId;
1494                    return true;
1495                } else if (type == TvTrackInfo.TYPE_SUBTITLE
1496                        && trackId != mSelectedSubtitleTrackId) {
1497                    mSelectedSubtitleTrackId = trackId;
1498                    return true;
1499                }
1500            }
1501            return false;
1502        }
1503
1504        /**
1505         * Returns the new/updated video track that contains new video size information. Returns
1506         * null if there is no video track to notify. Subsequent calls of this method results in a
1507         * non-null video track returned only by the first call and null returned by following
1508         * calls. The caller should immediately notify of the video size change upon receiving the
1509         * track.
1510         */
1511        TvTrackInfo getVideoTrackToNotify() {
1512            synchronized (mTrackLock) {
1513                if (!mVideoTracks.isEmpty() && mSelectedVideoTrackId != null) {
1514                    for (TvTrackInfo track : mVideoTracks) {
1515                        if (track.getId().equals(mSelectedVideoTrackId)) {
1516                            int videoWidth = track.getVideoWidth();
1517                            int videoHeight = track.getVideoHeight();
1518                            if (mVideoWidth != videoWidth || mVideoHeight != videoHeight) {
1519                                mVideoWidth = videoWidth;
1520                                mVideoHeight = videoHeight;
1521                                return track;
1522                            }
1523                        }
1524                    }
1525                }
1526            }
1527            return null;
1528        }
1529
1530        /**
1531         * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
1532         * TvInputService.Session.appPrivateCommand()} on the current TvView.
1533         *
1534         * @param action Name of the command to be performed. This <em>must</em> be a scoped name,
1535         *            i.e. prefixed with a package name you own, so that different developers will
1536         *            not create conflicting commands.
1537         * @param data Any data to include with the command.
1538         * @hide
1539         */
1540        @SystemApi
1541        public void sendAppPrivateCommand(String action, Bundle data) {
1542            if (mToken == null) {
1543                Log.w(TAG, "The session has been already released");
1544                return;
1545            }
1546            try {
1547                mService.sendAppPrivateCommand(mToken, action, data, mUserId);
1548            } catch (RemoteException e) {
1549                throw new RuntimeException(e);
1550            }
1551        }
1552
1553        /**
1554         * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView}
1555         * should be called whenever the layout of its containing view is changed.
1556         * {@link #removeOverlayView()} should be called to remove the overlay view.
1557         * Since a session can have only one overlay view, this method should be called only once
1558         * or it can be called again after calling {@link #removeOverlayView()}.
1559         *
1560         * @param view A view playing TV.
1561         * @param frame A position of the overlay view.
1562         * @throws IllegalArgumentException if any of the arguments is {@code null}.
1563         * @throws IllegalStateException if {@code view} is not attached to a window.
1564         */
1565        void createOverlayView(View view, Rect frame) {
1566            if (view == null) {
1567                throw new IllegalArgumentException("view cannot be null");
1568            }
1569            if (frame == null) {
1570                throw new IllegalArgumentException("frame cannot be null");
1571            }
1572            if (view.getWindowToken() == null) {
1573                throw new IllegalStateException("view must be attached to a window");
1574            }
1575            if (mToken == null) {
1576                Log.w(TAG, "The session has been already released");
1577                return;
1578            }
1579            try {
1580                mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId);
1581            } catch (RemoteException e) {
1582                throw new RuntimeException(e);
1583            }
1584        }
1585
1586        /**
1587         * Relayouts the current overlay view.
1588         *
1589         * @param frame A new position of the overlay view.
1590         * @throws IllegalArgumentException if the arguments is {@code null}.
1591         */
1592        void relayoutOverlayView(Rect frame) {
1593            if (frame == null) {
1594                throw new IllegalArgumentException("frame cannot be null");
1595            }
1596            if (mToken == null) {
1597                Log.w(TAG, "The session has been already released");
1598                return;
1599            }
1600            try {
1601                mService.relayoutOverlayView(mToken, frame, mUserId);
1602            } catch (RemoteException e) {
1603                throw new RuntimeException(e);
1604            }
1605        }
1606
1607        /**
1608         * Removes the current overlay view.
1609         */
1610        void removeOverlayView() {
1611            if (mToken == null) {
1612                Log.w(TAG, "The session has been already released");
1613                return;
1614            }
1615            try {
1616                mService.removeOverlayView(mToken, mUserId);
1617            } catch (RemoteException e) {
1618                throw new RuntimeException(e);
1619            }
1620        }
1621
1622        /**
1623         * Requests to unblock content blocked by parental controls.
1624         */
1625        void requestUnblockContent(TvContentRating unblockedRating) {
1626            if (mToken == null) {
1627                Log.w(TAG, "The session has been already released");
1628                return;
1629            }
1630            if (unblockedRating == null) {
1631                throw new IllegalArgumentException("unblockedRating cannot be null");
1632            }
1633            try {
1634                mService.requestUnblockContent(mToken, unblockedRating.flattenToString(), mUserId);
1635            } catch (RemoteException e) {
1636                throw new RuntimeException(e);
1637            }
1638        }
1639
1640        /**
1641         * Dispatches an input event to this session.
1642         *
1643         * @param event An {@link InputEvent} to dispatch.
1644         * @param token A token used to identify the input event later in the callback.
1645         * @param callback A callback used to receive the dispatch result.
1646         * @param handler A {@link Handler} that the dispatch result will be delivered to.
1647         * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
1648         *         {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
1649         *         {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
1650         *         be invoked later.
1651         * @throws IllegalArgumentException if any of the necessary arguments is {@code null}.
1652         * @hide
1653         */
1654        public int dispatchInputEvent(InputEvent event, Object token,
1655                FinishedInputEventCallback callback, Handler handler) {
1656            if (event == null) {
1657                throw new IllegalArgumentException("event cannot be null");
1658            }
1659            if (callback != null && handler == null) {
1660                throw new IllegalArgumentException("handler cannot be null");
1661            }
1662            synchronized (mHandler) {
1663                if (mChannel == null) {
1664                    return DISPATCH_NOT_HANDLED;
1665                }
1666                PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
1667                if (Looper.myLooper() == Looper.getMainLooper()) {
1668                    // Already running on the main thread so we can send the event immediately.
1669                    return sendInputEventOnMainLooperLocked(p);
1670                }
1671
1672                // Post the event to the main thread.
1673                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
1674                msg.setAsynchronous(true);
1675                mHandler.sendMessage(msg);
1676                return DISPATCH_IN_PROGRESS;
1677            }
1678        }
1679
1680        /**
1681         * Callback that is invoked when an input event that was dispatched to this session has been
1682         * finished.
1683         *
1684         * @hide
1685         */
1686        public interface FinishedInputEventCallback {
1687            /**
1688             * Called when the dispatched input event is finished.
1689             *
1690             * @param token A token passed to {@link #dispatchInputEvent}.
1691             * @param handled {@code true} if the dispatched input event was handled properly.
1692             *            {@code false} otherwise.
1693             */
1694            public void onFinishedInputEvent(Object token, boolean handled);
1695        }
1696
1697        // Must be called on the main looper
1698        private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
1699            synchronized (mHandler) {
1700                int result = sendInputEventOnMainLooperLocked(p);
1701                if (result == DISPATCH_IN_PROGRESS) {
1702                    return;
1703                }
1704            }
1705
1706            invokeFinishedInputEventCallback(p, false);
1707        }
1708
1709        private int sendInputEventOnMainLooperLocked(PendingEvent p) {
1710            if (mChannel != null) {
1711                if (mSender == null) {
1712                    mSender = new TvInputEventSender(mChannel, mHandler.getLooper());
1713                }
1714
1715                final InputEvent event = p.mEvent;
1716                final int seq = event.getSequenceNumber();
1717                if (mSender.sendInputEvent(seq, event)) {
1718                    mPendingEvents.put(seq, p);
1719                    Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
1720                    msg.setAsynchronous(true);
1721                    mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
1722                    return DISPATCH_IN_PROGRESS;
1723                }
1724
1725                Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
1726                        + event);
1727            }
1728            return DISPATCH_NOT_HANDLED;
1729        }
1730
1731        void finishedInputEvent(int seq, boolean handled, boolean timeout) {
1732            final PendingEvent p;
1733            synchronized (mHandler) {
1734                int index = mPendingEvents.indexOfKey(seq);
1735                if (index < 0) {
1736                    return; // spurious, event already finished or timed out
1737                }
1738
1739                p = mPendingEvents.valueAt(index);
1740                mPendingEvents.removeAt(index);
1741
1742                if (timeout) {
1743                    Log.w(TAG, "Timeout waiting for seesion to handle input event after "
1744                            + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
1745                } else {
1746                    mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
1747                }
1748            }
1749
1750            invokeFinishedInputEventCallback(p, handled);
1751        }
1752
1753        // Assumes the event has already been removed from the queue.
1754        void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
1755            p.mHandled = handled;
1756            if (p.mEventHandler.getLooper().isCurrentThread()) {
1757                // Already running on the callback handler thread so we can send the callback
1758                // immediately.
1759                p.run();
1760            } else {
1761                // Post the event to the callback handler thread.
1762                // In this case, the callback will be responsible for recycling the event.
1763                Message msg = Message.obtain(p.mEventHandler, p);
1764                msg.setAsynchronous(true);
1765                msg.sendToTarget();
1766            }
1767        }
1768
1769        private void flushPendingEventsLocked() {
1770            mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
1771
1772            final int count = mPendingEvents.size();
1773            for (int i = 0; i < count; i++) {
1774                int seq = mPendingEvents.keyAt(i);
1775                Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
1776                msg.setAsynchronous(true);
1777                msg.sendToTarget();
1778            }
1779        }
1780
1781        private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
1782                FinishedInputEventCallback callback, Handler handler) {
1783            PendingEvent p = mPendingEventPool.acquire();
1784            if (p == null) {
1785                p = new PendingEvent();
1786            }
1787            p.mEvent = event;
1788            p.mEventToken = token;
1789            p.mCallback = callback;
1790            p.mEventHandler = handler;
1791            return p;
1792        }
1793
1794        private void recyclePendingEventLocked(PendingEvent p) {
1795            p.recycle();
1796            mPendingEventPool.release(p);
1797        }
1798
1799        IBinder getToken() {
1800            return mToken;
1801        }
1802
1803        private void releaseInternal() {
1804            mToken = null;
1805            synchronized (mHandler) {
1806                if (mChannel != null) {
1807                    if (mSender != null) {
1808                        flushPendingEventsLocked();
1809                        mSender.dispose();
1810                        mSender = null;
1811                    }
1812                    mChannel.dispose();
1813                    mChannel = null;
1814                }
1815            }
1816            synchronized (mSessionCallbackRecordMap) {
1817                mSessionCallbackRecordMap.remove(mSeq);
1818            }
1819        }
1820
1821        private final class InputEventHandler extends Handler {
1822            public static final int MSG_SEND_INPUT_EVENT = 1;
1823            public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
1824            public static final int MSG_FLUSH_INPUT_EVENT = 3;
1825
1826            InputEventHandler(Looper looper) {
1827                super(looper, null, true);
1828            }
1829
1830            @Override
1831            public void handleMessage(Message msg) {
1832                switch (msg.what) {
1833                    case MSG_SEND_INPUT_EVENT: {
1834                        sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
1835                        return;
1836                    }
1837                    case MSG_TIMEOUT_INPUT_EVENT: {
1838                        finishedInputEvent(msg.arg1, false, true);
1839                        return;
1840                    }
1841                    case MSG_FLUSH_INPUT_EVENT: {
1842                        finishedInputEvent(msg.arg1, false, false);
1843                        return;
1844                    }
1845                }
1846            }
1847        }
1848
1849        private final class TvInputEventSender extends InputEventSender {
1850            public TvInputEventSender(InputChannel inputChannel, Looper looper) {
1851                super(inputChannel, looper);
1852            }
1853
1854            @Override
1855            public void onInputEventFinished(int seq, boolean handled) {
1856                finishedInputEvent(seq, handled, false);
1857            }
1858        }
1859
1860        private final class PendingEvent implements Runnable {
1861            public InputEvent mEvent;
1862            public Object mEventToken;
1863            public FinishedInputEventCallback mCallback;
1864            public Handler mEventHandler;
1865            public boolean mHandled;
1866
1867            public void recycle() {
1868                mEvent = null;
1869                mEventToken = null;
1870                mCallback = null;
1871                mEventHandler = null;
1872                mHandled = false;
1873            }
1874
1875            @Override
1876            public void run() {
1877                mCallback.onFinishedInputEvent(mEventToken, mHandled);
1878
1879                synchronized (mEventHandler) {
1880                    recyclePendingEventLocked(this);
1881                }
1882            }
1883        }
1884    }
1885
1886    /**
1887     * The Hardware provides the per-hardware functionality of TV hardware.
1888     *
1889     * TV hardware is physical hardware attached to the Android device; for example, HDMI ports,
1890     * Component/Composite ports, etc. Specifically, logical devices such as HDMI CEC logical
1891     * devices don't fall into this category.
1892     *
1893     * @hide
1894     */
1895    @SystemApi
1896    public final static class Hardware {
1897        private final ITvInputHardware mInterface;
1898
1899        private Hardware(ITvInputHardware hardwareInterface) {
1900            mInterface = hardwareInterface;
1901        }
1902
1903        private ITvInputHardware getInterface() {
1904            return mInterface;
1905        }
1906
1907        public boolean setSurface(Surface surface, TvStreamConfig config) {
1908            try {
1909                return mInterface.setSurface(surface, config);
1910            } catch (RemoteException e) {
1911                throw new RuntimeException(e);
1912            }
1913        }
1914
1915        public void setStreamVolume(float volume) {
1916            try {
1917                mInterface.setStreamVolume(volume);
1918            } catch (RemoteException e) {
1919                throw new RuntimeException(e);
1920            }
1921        }
1922
1923        public boolean dispatchKeyEventToHdmi(KeyEvent event) {
1924            try {
1925                return mInterface.dispatchKeyEventToHdmi(event);
1926            } catch (RemoteException e) {
1927                throw new RuntimeException(e);
1928            }
1929        }
1930
1931        public void overrideAudioSink(int audioType, String audioAddress, int samplingRate,
1932                int channelMask, int format) {
1933            try {
1934                mInterface.overrideAudioSink(audioType, audioAddress, samplingRate, channelMask,
1935                        format);
1936            } catch (RemoteException e) {
1937                throw new RuntimeException(e);
1938            }
1939        }
1940    }
1941}
1942