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