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