TvInputService.java revision ff04ae757a5542d2d5633e75b7adacc4fce1ce7e
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.SuppressLint;
20import android.annotation.SystemApi;
21import android.app.Service;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.hardware.hdmi.HdmiCecDeviceInfo;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.Looper;
32import android.os.Message;
33import android.os.RemoteCallbackList;
34import android.os.RemoteException;
35import android.text.TextUtils;
36import android.util.Log;
37import android.view.Gravity;
38import android.view.InputChannel;
39import android.view.InputDevice;
40import android.view.InputEvent;
41import android.view.InputEventReceiver;
42import android.view.KeyEvent;
43import android.view.MotionEvent;
44import android.view.Surface;
45import android.view.SurfaceView;
46import android.view.View;
47import android.view.WindowManager;
48import android.view.accessibility.CaptioningManager;
49
50import com.android.internal.annotations.VisibleForTesting;
51import com.android.internal.os.SomeArgs;
52
53import java.util.HashSet;
54import java.util.List;
55import java.util.Set;
56
57/**
58 * The TvInputService class represents a TV input or source such as HDMI or built-in tuner which
59 * provides pass-through video or broadcast TV programs.
60 * <p>
61 * Applications will not normally use this service themselves, instead relying on the standard
62 * interaction provided by {@link TvView}. Those implementing TV input services should normally do
63 * so by deriving from this class and providing their own session implementation based on
64 * {@link TvInputService.Session}. All TV input services must require that clients hold the
65 * {@link android.Manifest.permission#BIND_TV_INPUT} in order to interact with the service; if this
66 * permission is not specified in the manifest, the system will refuse to bind to that TV input
67 * service.
68 * </p>
69 */
70public abstract class TvInputService extends Service {
71    // STOPSHIP: Turn debugging off.
72    private static final boolean DEBUG = true;
73    private static final String TAG = "TvInputService";
74
75    /**
76     * This is the interface name that a service implementing a TV input should say that it support
77     * -- that is, this is the action it uses for its intent filter. To be supported, the service
78     * must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that
79     * other applications cannot abuse it.
80     */
81    public static final String SERVICE_INTERFACE = "android.media.tv.TvInputService";
82
83    /**
84     * Name under which a TvInputService component publishes information about itself.
85     * This meta-data must reference an XML resource containing an
86     * <code>&lt;{@link android.R.styleable#TvInputService tv-input}&gt;</code>
87     * tag.
88     */
89    public static final String SERVICE_META_DATA = "android.media.tv.input";
90
91    /**
92     * Handler instance to handle request from TV Input Manager Service. Should be run in the main
93     * looper to be synchronously run with {@code Session.mHandler}.
94     */
95    private final Handler mServiceHandler = new ServiceHandler(getMainLooper());
96    private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks =
97            new RemoteCallbackList<ITvInputServiceCallback>();
98
99    @Override
100    public final IBinder onBind(Intent intent) {
101        return new ITvInputService.Stub() {
102            @Override
103            public void registerCallback(ITvInputServiceCallback cb) {
104                if (cb != null) {
105                    mCallbacks.register(cb);
106                }
107            }
108
109            @Override
110            public void unregisterCallback(ITvInputServiceCallback cb) {
111                if (cb != null) {
112                    mCallbacks.unregister(cb);
113                }
114            }
115
116            @Override
117            public void createSession(InputChannel channel, ITvInputSessionCallback cb,
118                    String inputId) {
119                if (channel == null) {
120                    Log.w(TAG, "Creating session without input channel");
121                }
122                if (cb == null) {
123                    return;
124                }
125                SomeArgs args = SomeArgs.obtain();
126                args.arg1 = channel;
127                args.arg2 = cb;
128                args.arg3 = inputId;
129                mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget();
130            }
131
132            @Override
133            public void notifyHardwareAdded(TvInputHardwareInfo hardwareInfo) {
134                mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HARDWARE_TV_INPUT,
135                        hardwareInfo).sendToTarget();
136            }
137
138            @Override
139            public void notifyHardwareRemoved(TvInputHardwareInfo hardwareInfo) {
140                mServiceHandler.obtainMessage(ServiceHandler.DO_REMOVE_HARDWARE_TV_INPUT,
141                        hardwareInfo).sendToTarget();
142            }
143
144            @Override
145            public void notifyHdmiCecDeviceAdded(HdmiCecDeviceInfo cecDeviceInfo) {
146                mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HDMI_CEC_TV_INPUT,
147                        cecDeviceInfo).sendToTarget();
148            }
149
150            @Override
151            public void notifyHdmiCecDeviceRemoved(HdmiCecDeviceInfo cecDeviceInfo) {
152                mServiceHandler.obtainMessage(ServiceHandler.DO_REMOVE_HDMI_CEC_TV_INPUT,
153                        cecDeviceInfo).sendToTarget();
154            }
155        };
156    }
157
158    /**
159     * Get the number of callbacks that are registered.
160     * @hide
161     */
162    @VisibleForTesting
163    public final int getRegisteredCallbackCount() {
164        return mCallbacks.getRegisteredCallbackCount();
165    }
166
167    /**
168     * Returns a concrete implementation of {@link Session}.
169     * <p>
170     * May return {@code null} if this TV input service fails to create a session for some reason.
171     * If TV input represents an external device connected to a hardware TV input,
172     * {@link HardwareSession} should be returned.
173     * </p>
174     * @param inputId The ID of the TV input associated with the session.
175     */
176    public abstract Session onCreateSession(String inputId);
177
178    /**
179     * Returns a new {@link TvInputInfo} object if this service is responsible for
180     * {@code hardwareInfo}; otherwise, return {@code null}. Override to modify default behavior of
181     * ignoring all hardware input.
182     *
183     * @param hardwareInfo {@link TvInputHardwareInfo} object just added.
184     * @hide
185     */
186    @SystemApi
187    public TvInputInfo onHardwareAdded(TvInputHardwareInfo hardwareInfo) {
188        return null;
189    }
190
191    /**
192     * Returns the input ID for {@code deviceId} if it is handled by this service;
193     * otherwise, return {@code null}. Override to modify default behavior of ignoring all hardware
194     * input.
195     *
196     * @param hardwareInfo {@link TvInputHardwareInfo} object just removed.
197     * @hide
198     */
199    @SystemApi
200    public String onHardwareRemoved(TvInputHardwareInfo hardwareInfo) {
201        return null;
202    }
203
204    /**
205     * Returns a new {@link TvInputInfo} object if this service is responsible for
206     * {@code cecDeviceInfo}; otherwise, return {@code null}. Override to modify default behavior
207     * of ignoring all HDMI CEC logical input device.
208     *
209     * @param cecDeviceInfo {@link HdmiCecDeviceInfo} object just added.
210     * @hide
211     */
212    @SystemApi
213    public TvInputInfo onHdmiCecDeviceAdded(HdmiCecDeviceInfo cecDeviceInfo) {
214        return null;
215    }
216
217    /**
218     * Returns the input ID for {@code logicalAddress} if it is handled by this service;
219     * otherwise, return {@code null}. Override to modify default behavior of ignoring all HDMI CEC
220     * logical input device.
221     *
222     * @param cecDeviceInfo {@link HdmiCecDeviceInfo} object just removed.
223     * @hide
224     */
225    @SystemApi
226    public String onHdmiCecDeviceRemoved(HdmiCecDeviceInfo cecDeviceInfo) {
227        return null;
228    }
229
230    /**
231     * Base class for derived classes to implement to provide a TV input session.
232     */
233    public abstract static class Session implements KeyEvent.Callback {
234        private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
235        private final WindowManager mWindowManager;
236        final Handler mHandler;
237        private WindowManager.LayoutParams mWindowParams;
238        private Surface mSurface;
239        private View mOverlayView;
240        private boolean mOverlayViewEnabled;
241        private IBinder mWindowToken;
242        private Rect mOverlayFrame;
243        private ITvInputSessionCallback mSessionCallback;
244
245        /**
246         * Creates a new Session.
247         *
248         * @param context The context of the application
249         */
250        public Session(Context context) {
251            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
252            mHandler = new Handler(context.getMainLooper());
253        }
254
255        /**
256         * Enables or disables the overlay view. By default, the overlay view is disabled. Must be
257         * called explicitly after the session is created to enable the overlay view.
258         *
259         * @param enable {@code true} if you want to enable the overlay view. {@code false}
260         *            otherwise.
261         */
262        public void setOverlayViewEnabled(final boolean enable) {
263            mHandler.post(new Runnable() {
264                @Override
265                public void run() {
266                    if (enable == mOverlayViewEnabled) {
267                        return;
268                    }
269                    mOverlayViewEnabled = enable;
270                    if (enable) {
271                        if (mWindowToken != null) {
272                            createOverlayView(mWindowToken, mOverlayFrame);
273                        }
274                    } else {
275                        removeOverlayView(false);
276                    }
277                }
278            });
279        }
280
281        /**
282         * Dispatches an event to the application using this session.
283         *
284         * @param eventType The type of the event.
285         * @param eventArgs Optional arguments of the event.
286         * @hide
287         */
288        public void notifySessionEvent(final String eventType, final Bundle eventArgs) {
289            if (eventType == null) {
290                throw new IllegalArgumentException("eventType should not be null.");
291            }
292            mHandler.post(new Runnable() {
293                @Override
294                public void run() {
295                    try {
296                        if (DEBUG) Log.d(TAG, "notifySessionEvent(" + eventType + ")");
297                        mSessionCallback.onSessionEvent(eventType, eventArgs);
298                    } catch (RemoteException e) {
299                        Log.w(TAG, "error in sending event (event=" + eventType + ")");
300                    }
301                }
302            });
303        }
304
305        /**
306         * Notifies the channel of the session is retuned by TV input.
307         *
308         * @param channelUri The URI of a channel.
309         */
310        public void notifyChannelRetuned(final Uri channelUri) {
311            mHandler.post(new Runnable() {
312                @Override
313                public void run() {
314                    try {
315                        if (DEBUG) Log.d(TAG, "notifyChannelRetuned");
316                        mSessionCallback.onChannelRetuned(channelUri);
317                    } catch (RemoteException e) {
318                        Log.w(TAG, "error in notifyChannelRetuned");
319                    }
320                }
321            });
322        }
323
324        /**
325         * Sends the change on the track information. This is expected to be called whenever a track
326         * is added/removed and the metadata of a track is modified.
327         *
328         * @param tracks A list which includes track information.
329         * @throws IllegalArgumentException if {@code tracks} contains redundant tracks.
330         */
331        public void notifyTracksChanged(final List<TvTrackInfo> tracks) {
332            Set<String> trackIdSet = new HashSet<String>();
333            for (TvTrackInfo track : tracks) {
334                String trackId = track.getId();
335                if (trackIdSet.contains(trackId)) {
336                    throw new IllegalArgumentException("redundant track ID: " + trackId);
337                }
338                trackIdSet.add(trackId);
339            }
340            trackIdSet.clear();
341
342            // TODO: Validate the track list.
343            mHandler.post(new Runnable() {
344                @Override
345                public void run() {
346                    try {
347                        if (DEBUG) Log.d(TAG, "notifyTracksChanged");
348                        mSessionCallback.onTracksChanged(tracks);
349                    } catch (RemoteException e) {
350                        Log.w(TAG, "error in notifyTracksChanged");
351                    }
352                }
353            });
354        }
355
356        /**
357         * Sends the ID of the selected track for a given track type. This is expected to be called
358         * whenever there is a change on track selection.
359         *
360         * @param type The type of the selected track. The type can be
361         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
362         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
363         * @param trackId The ID of the selected track.
364         * @see #onSelectTrack
365         */
366        public void notifyTrackSelected(final int type, final String trackId) {
367            mHandler.post(new Runnable() {
368                @Override
369                public void run() {
370                    try {
371                        if (DEBUG) Log.d(TAG, "notifyTrackSelected");
372                        mSessionCallback.onTrackSelected(type, trackId);
373                    } catch (RemoteException e) {
374                        Log.w(TAG, "error in notifyTrackSelected");
375                    }
376                }
377            });
378        }
379
380        /**
381         * Informs the application that video is available and the playback of the TV stream has
382         * been started.
383         */
384        public void notifyVideoAvailable() {
385            mHandler.post(new Runnable() {
386                @Override
387                public void run() {
388                    try {
389                        if (DEBUG) Log.d(TAG, "notifyVideoAvailable");
390                        mSessionCallback.onVideoAvailable();
391                    } catch (RemoteException e) {
392                        Log.w(TAG, "error in notifyVideoAvailable");
393                    }
394                }
395            });
396        }
397
398        /**
399         * Informs the application that video is not available, so the TV input cannot continue
400         * playing the TV stream.
401         *
402         * @param reason The reason that the TV input stopped the playback:
403         * <ul>
404         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
405         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
406         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
407         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
408         * </ul>
409         */
410        public void notifyVideoUnavailable(final int reason) {
411            if (reason < TvInputManager.VIDEO_UNAVAILABLE_REASON_START
412                    || reason > TvInputManager.VIDEO_UNAVAILABLE_REASON_END) {
413                throw new IllegalArgumentException("Unknown reason: " + reason);
414            }
415            mHandler.post(new Runnable() {
416                @Override
417                public void run() {
418                    try {
419                        if (DEBUG) Log.d(TAG, "notifyVideoUnavailable");
420                        mSessionCallback.onVideoUnavailable(reason);
421                    } catch (RemoteException e) {
422                        Log.w(TAG, "error in notifyVideoUnavailable");
423                    }
424                }
425            });
426        }
427
428        /**
429         * Informs the application that the user is allowed to watch the current program content.
430         * <p>
431         * Each TV input service is required to query the system whether the user is allowed to
432         * watch the current program before showing it to the user if the parental controls is
433         * enabled (i.e. {@link TvInputManager#isParentalControlsEnabled
434         * TvInputManager.isParentalControlsEnabled()} returns {@code true}). Whether the TV input
435         * service should block the content or not is determined by invoking
436         * {@link TvInputManager#isRatingBlocked TvInputManager.isRatingBlocked(TvContentRating)}
437         * with the content rating for the current program. Then the {@link TvInputManager} makes a
438         * judgment based on the user blocked ratings stored in the secure settings and returns the
439         * result. If the rating in question turns out to be allowed by the user, the TV input
440         * service must call this method to notify the application that is permitted to show the
441         * content.
442         * </p><p>
443         * Each TV input service also needs to continuously listen to any changes made to the
444         * parental controls settings by registering a broadcast receiver to receive
445         * {@link TvInputManager#ACTION_BLOCKED_RATINGS_CHANGED} and
446         * {@link TvInputManager#ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED} and immediately
447         * reevaluate the current program with the new parental controls settings.
448         * </p>
449         *
450         * @see #notifyContentBlocked
451         * @see TvInputManager
452         */
453        public void notifyContentAllowed() {
454            mHandler.post(new Runnable() {
455                @Override
456                public void run() {
457                    try {
458                        if (DEBUG) Log.d(TAG, "notifyContentAllowed");
459                        mSessionCallback.onContentAllowed();
460                    } catch (RemoteException e) {
461                        Log.w(TAG, "error in notifyContentAllowed");
462                    }
463                }
464            });
465        }
466
467        /**
468         * Informs the application that the current program content is blocked by parent controls.
469         * <p>
470         * Each TV input service is required to query the system whether the user is allowed to
471         * watch the current program before showing it to the user if the parental controls is
472         * enabled (i.e. {@link TvInputManager#isParentalControlsEnabled
473         * TvInputManager.isParentalControlsEnabled()} returns {@code true}). Whether the TV input
474         * service should block the content or not is determined by invoking
475         * {@link TvInputManager#isRatingBlocked TvInputManager.isRatingBlocked(TvContentRating)}
476         * with the content rating for the current program. Then the {@link TvInputManager} makes a
477         * judgment based on the user blocked ratings stored in the secure settings and returns the
478         * result. If the rating in question turns out to be blocked, the TV input service must
479         * immediately block the content and call this method with the content rating of the current
480         * program to prompt the PIN verification screen.
481         * </p><p>
482         * Each TV input service also needs to continuously listen to any changes made to the
483         * parental controls settings by registering a broadcast receiver to receive
484         * {@link TvInputManager#ACTION_BLOCKED_RATINGS_CHANGED} and
485         * {@link TvInputManager#ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED} and immediately
486         * reevaluate the current program with the new parental controls settings.
487         * </p>
488         *
489         * @param rating The content rating for the current TV program.
490         * @see #notifyContentAllowed
491         * @see TvInputManager
492         */
493        public void notifyContentBlocked(final TvContentRating rating) {
494            mHandler.post(new Runnable() {
495                @Override
496                public void run() {
497                    try {
498                        if (DEBUG) Log.d(TAG, "notifyContentBlocked");
499                        mSessionCallback.onContentBlocked(rating.flattenToString());
500                    } catch (RemoteException e) {
501                        Log.w(TAG, "error in notifyContentBlocked");
502                    }
503                }
504            });
505        }
506
507        /**
508         * Assigns a position of the {@link Surface} passed by {@link #onSetSurface}. The position
509         * is relative to an overlay view. {@see #onOverlayViewSizeChanged}.
510         *
511         * @param left Left position in pixels, relative to the overlay view.
512         * @param top Top position in pixels, relative to the overlay view.
513         * @param right Right position in pixels, relative to the overlay view.
514         * @param bottm Bottom position in pixels, relative to the overlay view.
515         * @hide
516         */
517        @SystemApi
518        public void layoutSurface(final int left, final int top, final int right, final int bottm) {
519            if (left > right || top > bottm) {
520                throw new IllegalArgumentException("Invalid parameter");
521            }
522            mHandler.post(new Runnable() {
523                @Override
524                public void run() {
525                    try {
526                        if (DEBUG) Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + ", r="
527                                + right + ", b=" + bottm + ",)");
528                        mSessionCallback.onLayoutSurface(left, top, right, bottm);
529                    } catch (RemoteException e) {
530                        Log.w(TAG, "error in layoutSurface");
531                    }
532                }
533            });
534        }
535
536        /**
537         * Called when the session is released.
538         */
539        public abstract void onRelease();
540
541        /**
542         * Set the current session as the "main" session. See {@link TvView#setMainTvView} for the
543         * meaning of "main".
544         * <p>
545         * This is primarily for HDMI-CEC active source management. TV input service that manages
546         * HDMI-CEC logical device should make sure not only to select the corresponding HDMI
547         * logical device as source device on {@code onSetMainSession(true)}, but also to select
548         * internal device on {@code onSetMainSession(false)}. Also, if surface is set to non-main
549         * session, it needs to select internal device after temporarily selecting corresponding
550         * HDMI logical device for set up.
551         * </p><p>
552         * It is guaranteed that {@code onSetMainSession(true)} for new session is called first,
553         * and {@code onSetMainSession(false)} for old session is called afterwards. This allows
554         * {@code onSetMainSession(false)} to be no-op when TV input service knows that the next
555         * main session corresponds to another HDMI logical device. Practically, this implies that
556         * one TV input service should handle all HDMI port and HDMI-CEC logical devices for smooth
557         * active source transition.
558         * </p>
559         *
560         * @param isMainSession If true, session is main.
561         * @hide
562         */
563        @SystemApi
564        public void onSetMainSession(boolean isMainSession) {
565        }
566
567        /**
568         * Sets the {@link Surface} for the current input session on which the TV input renders
569         * video.
570         *
571         * @param surface {@link Surface} an application passes to this TV input session.
572         * @return {@code true} if the surface was set, {@code false} otherwise.
573         */
574        public abstract boolean onSetSurface(Surface surface);
575
576        /**
577         * Called after any structural changes (format or size) have been made to the
578         * {@link Surface} passed by {@link #onSetSurface}. This method is always called
579         * at least once, after {@link #onSetSurface} with non-null {@link Surface} is called.
580         *
581         * @param format The new PixelFormat of the {@link Surface}.
582         * @param width The new width of the {@link Surface}.
583         * @param height The new height of the {@link Surface}.
584         */
585        public void onSurfaceChanged(int format, int width, int height) {
586        }
587
588        /**
589         * Called when a size of an overlay view is changed by an application. Even when the overlay
590         * view is disabled by {@link #setOverlayViewEnabled}, this is called. The size is same as
591         * the size of {@link Surface} in general. Once {@link #layoutSurface} is called, the sizes
592         * of {@link Surface} and the overlay view can be different.
593         *
594         * @param width The width of the overlay view.
595         * @param height The height of the overlay view.
596         * @hide
597         */
598        @SystemApi
599        public void onOverlayViewSizeChanged(int width, int height) {
600        }
601
602        /**
603         * Sets the relative stream volume of the current TV input session to handle the change of
604         * audio focus by setting.
605         *
606         * @param volume Volume scale from 0.0 to 1.0.
607         */
608        public abstract void onSetStreamVolume(float volume);
609
610        /**
611         * Tunes to a given channel. When the video is available, {@link #notifyVideoAvailable()}
612         * should be called. Also, {@link #notifyVideoUnavailable(int)} should be called when the
613         * TV input cannot continue playing the given channel.
614         *
615         * @param channelUri The URI of the channel.
616         * @return {@code true} the tuning was successful, {@code false} otherwise.
617         */
618        public abstract boolean onTune(Uri channelUri);
619
620        /**
621         * Calls {@link #onTune(Uri)}. Override this method in order to handle {@code params}.
622         *
623         * @param channelUri The URI of the channel.
624         * @param params The extra parameters from other applications.
625         * @return {@code true} the tuning was successful, {@code false} otherwise.
626         * @hide
627         */
628        @SystemApi
629        public boolean onTune(Uri channelUri, Bundle params) {
630            return onTune(channelUri);
631        }
632
633        /**
634         * Enables or disables the caption.
635         * <p>
636         * The locale for the user's preferred captioning language can be obtained by calling
637         * {@link CaptioningManager#getLocale CaptioningManager.getLocale()}.
638         *
639         * @param enabled {@code true} to enable, {@code false} to disable.
640         * @see CaptioningManager
641         */
642        public abstract void onSetCaptionEnabled(boolean enabled);
643
644        /**
645         * Requests to unblock the content according to the given rating.
646         * <p>
647         * The implementation should unblock the content.
648         * TV input service has responsibility to decide when/how the unblock expires
649         * while it can keep previously unblocked ratings in order not to ask a user
650         * to unblock whenever a content rating is changed.
651         * Therefore an unblocked rating can be valid for a channel, a program,
652         * or certain amount of time depending on the implementation.
653         * </p>
654         *
655         * @param unblockedRating An unblocked content rating
656         */
657        public void onUnblockContent(TvContentRating unblockedRating) {
658        }
659
660        /**
661         * Select a given track.
662         * <p>
663         * If this is done successfully, the implementation should call {@link #notifyTrackSelected}
664         * to help applications maintain the selcted track lists.
665         * </p>
666         *
667         * @param trackId The ID of the track to select. {@code null} means to unselect the current
668         *            track for a given type.
669         * @param type The type of the track to select. The type can be
670         *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
671         *            {@link TvTrackInfo#TYPE_SUBTITLE}.
672         * @see #notifyTrackSelected
673         */
674        public boolean onSelectTrack(int type, String trackId) {
675            return false;
676        }
677
678        /**
679         * Processes a private command sent from the application to the TV input. This can be used
680         * to provide domain-specific features that are only known between certain TV inputs and
681         * their clients.
682         *
683         * @param action Name of the command to be performed. This <em>must</em> be a scoped name,
684         *            i.e. prefixed with a package name you own, so that different developers will
685         *            not create conflicting commands.
686         * @param data Any data to include with the command.
687         * @hide
688         */
689        @SystemApi
690        public void onAppPrivateCommand(String action, Bundle data) {
691        }
692
693        /**
694         * Called when an application requests to create an overlay view. Each session
695         * implementation can override this method and return its own view.
696         *
697         * @return a view attached to the overlay window
698         */
699        public View onCreateOverlayView() {
700            return null;
701        }
702
703        /**
704         * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent)
705         * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event).
706         * <p>
707         * Override this to intercept key down events before they are processed by the application.
708         * If you return true, the application will not process the event itself. If you return
709         * false, the normal application processing will occur as if the TV input had not seen the
710         * event at all.
711         *
712         * @param keyCode The value in event.getKeyCode().
713         * @param event Description of the key event.
714         * @return If you handled the event, return {@code true}. If you want to allow the event to
715         *         be handled by the next receiver, return {@code false}.
716         */
717        @Override
718        public boolean onKeyDown(int keyCode, KeyEvent event) {
719            return false;
720        }
721
722        /**
723         * Default implementation of
724         * {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
725         * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event).
726         * <p>
727         * Override this to intercept key long press events before they are processed by the
728         * application. If you return true, the application will not process the event itself. If
729         * you return false, the normal application processing will occur as if the TV input had not
730         * seen the event at all.
731         *
732         * @param keyCode The value in event.getKeyCode().
733         * @param event Description of the key event.
734         * @return If you handled the event, return {@code true}. If you want to allow the event to
735         *         be handled by the next receiver, return {@code false}.
736         */
737        @Override
738        public boolean onKeyLongPress(int keyCode, KeyEvent event) {
739            return false;
740        }
741
742        /**
743         * Default implementation of
744         * {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
745         * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event).
746         * <p>
747         * Override this to intercept special key multiple events before they are processed by the
748         * application. If you return true, the application will not itself process the event. If
749         * you return false, the normal application processing will occur as if the TV input had not
750         * seen the event at all.
751         *
752         * @param keyCode The value in event.getKeyCode().
753         * @param count The number of times the action was made.
754         * @param event Description of the key event.
755         * @return If you handled the event, return {@code true}. If you want to allow the event to
756         *         be handled by the next receiver, return {@code false}.
757         */
758        @Override
759        public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
760            return false;
761        }
762
763        /**
764         * Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent)
765         * KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event).
766         * <p>
767         * Override this to intercept key up events before they are processed by the application. If
768         * you return true, the application will not itself process the event. If you return false,
769         * the normal application processing will occur as if the TV input had not seen the event at
770         * all.
771         *
772         * @param keyCode The value in event.getKeyCode().
773         * @param event Description of the key event.
774         * @return If you handled the event, return {@code true}. If you want to allow the event to
775         *         be handled by the next receiver, return {@code false}.
776         */
777        @Override
778        public boolean onKeyUp(int keyCode, KeyEvent event) {
779            return false;
780        }
781
782        /**
783         * Implement this method to handle touch screen motion events on the current input session.
784         *
785         * @param event The motion event being received.
786         * @return If you handled the event, return {@code true}. If you want to allow the event to
787         *         be handled by the next receiver, return {@code false}.
788         * @see View#onTouchEvent
789         */
790        public boolean onTouchEvent(MotionEvent event) {
791            return false;
792        }
793
794        /**
795         * Implement this method to handle trackball events on the current input session.
796         *
797         * @param event The motion event being received.
798         * @return If you handled the event, return {@code true}. If you want to allow the event to
799         *         be handled by the next receiver, return {@code false}.
800         * @see View#onTrackballEvent
801         */
802        public boolean onTrackballEvent(MotionEvent event) {
803            return false;
804        }
805
806        /**
807         * Implement this method to handle generic motion events on the current input session.
808         *
809         * @param event The motion event being received.
810         * @return If you handled the event, return {@code true}. If you want to allow the event to
811         *         be handled by the next receiver, return {@code false}.
812         * @see View#onGenericMotionEvent
813         */
814        public boolean onGenericMotionEvent(MotionEvent event) {
815            return false;
816        }
817
818        /**
819         * This method is called when the application would like to stop using the current input
820         * session.
821         */
822        void release() {
823            removeOverlayView(true);
824            onRelease();
825            if (mSurface != null) {
826                mSurface.release();
827                mSurface = null;
828            }
829        }
830
831        /**
832         * Calls {@link #onSetMainSession}.
833         */
834        void setMainSession(boolean isMainSession) {
835            onSetMainSession(isMainSession);
836        }
837
838        /**
839         * Calls {@link #onSetSurface}.
840         */
841        void setSurface(Surface surface) {
842            onSetSurface(surface);
843            if (mSurface != null) {
844                mSurface.release();
845            }
846            mSurface = surface;
847            // TODO: Handle failure.
848        }
849
850        /**
851         * Calls {@link #onSurfaceChanged}.
852         */
853        void dispatchSurfaceChanged(int format, int width, int height) {
854            if (DEBUG) {
855                Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width
856                        + ", height=" + height + ")");
857            }
858            onSurfaceChanged(format, width, height);
859        }
860
861        /**
862         * Calls {@link #onSetStreamVolume}.
863         */
864        void setStreamVolume(float volume) {
865            onSetStreamVolume(volume);
866        }
867
868        /**
869         * Calls {@link #onTune}.
870         */
871        void tune(Uri channelUri, Bundle params) {
872            onTune(channelUri, params);
873            // TODO: Handle failure.
874        }
875
876        /**
877         * Calls {@link #onSetCaptionEnabled}.
878         */
879        void setCaptionEnabled(boolean enabled) {
880            onSetCaptionEnabled(enabled);
881        }
882
883        /**
884         * Calls {@link #onSelectTrack}.
885         */
886        void selectTrack(int type, String trackId) {
887            onSelectTrack(type, trackId);
888        }
889
890        /**
891         * Calls {@link #onUnblockContent}.
892         */
893        void unblockContent(String unblockedRating) {
894            onUnblockContent(TvContentRating.unflattenFromString(unblockedRating));
895            // TODO: Handle failure.
896        }
897
898        /**
899         * Calls {@link #onAppPrivateCommand}.
900         */
901        void appPrivateCommand(String action, Bundle data) {
902            onAppPrivateCommand(action, data);
903        }
904
905        /**
906         * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach
907         * to the overlay window.
908         *
909         * @param windowToken A window token of an application.
910         * @param frame A position of the overlay view.
911         */
912        void createOverlayView(IBinder windowToken, Rect frame) {
913            if (mOverlayView != null) {
914                mWindowManager.removeView(mOverlayView);
915                mOverlayView = null;
916            }
917            if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")");
918            mWindowToken = windowToken;
919            mOverlayFrame = frame;
920            onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
921            if (!mOverlayViewEnabled) {
922                return;
923            }
924            mOverlayView = onCreateOverlayView();
925            if (mOverlayView == null) {
926                return;
927            }
928            // TvView's window type is TYPE_APPLICATION_MEDIA and we want to create
929            // an overlay window above the media window but below the application window.
930            int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
931            // We make the overlay view non-focusable and non-touchable so that
932            // the application that owns the window token can decide whether to consume or
933            // dispatch the input events.
934            int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
935                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
936                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
937            mWindowParams = new WindowManager.LayoutParams(
938                    frame.right - frame.left, frame.bottom - frame.top,
939                    frame.left, frame.top, type, flag, PixelFormat.TRANSPARENT);
940            mWindowParams.privateFlags |=
941                    WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
942            mWindowParams.gravity = Gravity.START | Gravity.TOP;
943            mWindowParams.token = windowToken;
944            mWindowManager.addView(mOverlayView, mWindowParams);
945        }
946
947        /**
948         * Relayouts the current overlay view.
949         *
950         * @param frame A new position of the overlay view.
951         */
952        void relayoutOverlayView(Rect frame) {
953            if (DEBUG) Log.d(TAG, "relayoutOverlayView(" + frame + ")");
954            if (mOverlayFrame == null || mOverlayFrame.width() != frame.width()
955                    || mOverlayFrame.height() != frame.height()) {
956                // Note: relayoutOverlayView is called whenever TvView's layout is changed
957                // regardless of setOverlayViewEnabled.
958                onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
959            }
960            mOverlayFrame = frame;
961            if (!mOverlayViewEnabled || mOverlayView == null) {
962                return;
963            }
964            mWindowParams.x = frame.left;
965            mWindowParams.y = frame.top;
966            mWindowParams.width = frame.right - frame.left;
967            mWindowParams.height = frame.bottom - frame.top;
968            mWindowManager.updateViewLayout(mOverlayView, mWindowParams);
969        }
970
971        /**
972         * Removes the current overlay view.
973         */
974        void removeOverlayView(boolean clearWindowToken) {
975            if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayView + ")");
976            if (clearWindowToken) {
977                mWindowToken = null;
978                mOverlayFrame = null;
979            }
980            if (mOverlayView != null) {
981                mWindowManager.removeView(mOverlayView);
982                mOverlayView = null;
983                mWindowParams = null;
984            }
985        }
986
987        /**
988         * Takes care of dispatching incoming input events and tells whether the event was handled.
989         */
990        int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
991            if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")");
992            boolean isNavigationKey = false;
993            if (event instanceof KeyEvent) {
994                KeyEvent keyEvent = (KeyEvent) event;
995                isNavigationKey = isNavigationKey(keyEvent.getKeyCode());
996                if (keyEvent.dispatch(this, mDispatcherState, this)) {
997                    return TvInputManager.Session.DISPATCH_HANDLED;
998                }
999            } else if (event instanceof MotionEvent) {
1000                MotionEvent motionEvent = (MotionEvent) event;
1001                final int source = motionEvent.getSource();
1002                if (motionEvent.isTouchEvent()) {
1003                    if (onTouchEvent(motionEvent)) {
1004                        return TvInputManager.Session.DISPATCH_HANDLED;
1005                    }
1006                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
1007                    if (onTrackballEvent(motionEvent)) {
1008                        return TvInputManager.Session.DISPATCH_HANDLED;
1009                    }
1010                } else {
1011                    if (onGenericMotionEvent(motionEvent)) {
1012                        return TvInputManager.Session.DISPATCH_HANDLED;
1013                    }
1014                }
1015            }
1016            if (mOverlayView == null || !mOverlayView.isAttachedToWindow()) {
1017                return TvInputManager.Session.DISPATCH_NOT_HANDLED;
1018            }
1019            if (!mOverlayView.hasWindowFocus()) {
1020                mOverlayView.getViewRootImpl().windowFocusChanged(true, true);
1021            }
1022            if (isNavigationKey && mOverlayView.hasFocusable()) {
1023                // If mOverlayView has focusable views, navigation key events should be always
1024                // handled. If not, it can make the application UI navigation messed up.
1025                // For example, in the case that the left-most view is focused, a left key event
1026                // will not be handled in ViewRootImpl. Then, the left key event will be handled in
1027                // the application during the UI navigation of the TV input.
1028                mOverlayView.getViewRootImpl().dispatchInputEvent(event);
1029                return TvInputManager.Session.DISPATCH_HANDLED;
1030            } else {
1031                mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver);
1032                return TvInputManager.Session.DISPATCH_IN_PROGRESS;
1033            }
1034        }
1035
1036        private void setSessionCallback(ITvInputSessionCallback callback) {
1037            mSessionCallback = callback;
1038        }
1039    }
1040
1041    /**
1042     * Base class for a TV input session which represents an external device connected to a
1043     * hardware TV input. Once TV input returns an implementation of this class on
1044     * {@link #onCreateSession(String)}, the framework will create a hardware session and forward
1045     * the application's surface to the hardware TV input.
1046     * @see #onCreateSession(String)
1047     */
1048    public abstract static class HardwareSession extends Session {
1049
1050        /**
1051         * Creates a new HardwareSession.
1052         *
1053         * @param context The context of the application
1054         */
1055        public HardwareSession(Context context) {
1056            super(context);
1057        }
1058
1059        private TvInputManager.Session mHardwareSession;
1060        private ITvInputSession mProxySession;
1061        private ITvInputSessionCallback mProxySessionCallback;
1062
1063        /**
1064         * Returns the hardware TV input ID the external device is connected to.
1065         * <p>
1066         * TV input is expected to provide {@link android.R.attr#setupActivity} so that
1067         * the application can launch it before using this TV input. The setup activity may let
1068         * the user select the hardware TV input to which the external device is connected. The ID
1069         * of the selected one should be stored in the TV input so that it can be returned here.
1070         * </p>
1071         */
1072        public abstract String getHardwareInputId();
1073
1074        private final TvInputManager.SessionCallback mHardwareSessionCallback =
1075                new TvInputManager.SessionCallback() {
1076            @Override
1077            public void onSessionCreated(TvInputManager.Session session) {
1078                mHardwareSession = session;
1079                SomeArgs args = SomeArgs.obtain();
1080                if (session != null) {
1081                    args.arg1 = mProxySession;
1082                    args.arg2 = mProxySessionCallback;
1083                    args.arg3 = session.getToken();
1084                } else {
1085                    args.arg1 = null;
1086                    args.arg2 = mProxySessionCallback;
1087                    args.arg3 = null;
1088                    onRelease();
1089                }
1090                mHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED, args)
1091                        .sendToTarget();
1092            }
1093
1094            @Override
1095            public void onVideoAvailable(final TvInputManager.Session session) {
1096                if (mHardwareSession == session) {
1097                    onHardwareVideoAvailable();
1098                }
1099            }
1100
1101            @Override
1102            public void onVideoUnavailable(final TvInputManager.Session session,
1103                    final int reason) {
1104                if (mHardwareSession == session) {
1105                    onHardwareVideoUnavailable(reason);
1106                }
1107            }
1108        };
1109
1110        /**
1111         * This method will not be called in {@link HardwareSession}. Framework will
1112         * forward the application's surface to the hardware TV input.
1113         */
1114        @Override
1115        public final boolean onSetSurface(Surface surface) {
1116            Log.e(TAG, "onSetSurface() should not be called in HardwareProxySession.");
1117            return false;
1118        }
1119
1120        /**
1121         * Called when the underlying hardware TV input session calls
1122         * {@link TvInputService.Session#notifyVideoAvailable()}.
1123         */
1124        public void onHardwareVideoAvailable() { }
1125
1126        /**
1127         * Called when the underlying hardware TV input session calls
1128         * {@link TvInputService.Session#notifyVideoUnavailable(int)}.
1129         *
1130         * @param reason The reason that the hardware TV input stopped the playback:
1131         * <ul>
1132         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
1133         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
1134         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
1135         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
1136         * </ul>
1137         */
1138        public void onHardwareVideoUnavailable(int reason) { }
1139    }
1140
1141    /** @hide */
1142    public static boolean isNavigationKey(int keyCode) {
1143        switch (keyCode) {
1144            case KeyEvent.KEYCODE_DPAD_LEFT:
1145            case KeyEvent.KEYCODE_DPAD_RIGHT:
1146            case KeyEvent.KEYCODE_DPAD_UP:
1147            case KeyEvent.KEYCODE_DPAD_DOWN:
1148            case KeyEvent.KEYCODE_DPAD_CENTER:
1149            case KeyEvent.KEYCODE_PAGE_UP:
1150            case KeyEvent.KEYCODE_PAGE_DOWN:
1151            case KeyEvent.KEYCODE_MOVE_HOME:
1152            case KeyEvent.KEYCODE_MOVE_END:
1153            case KeyEvent.KEYCODE_TAB:
1154            case KeyEvent.KEYCODE_SPACE:
1155            case KeyEvent.KEYCODE_ENTER:
1156                return true;
1157        }
1158        return false;
1159    }
1160
1161    @SuppressLint("HandlerLeak")
1162    private final class ServiceHandler extends Handler {
1163        private static final int DO_CREATE_SESSION = 1;
1164        private static final int DO_NOTIFY_SESSION_CREATED = 2;
1165        private static final int DO_ADD_HARDWARE_TV_INPUT = 3;
1166        private static final int DO_REMOVE_HARDWARE_TV_INPUT = 4;
1167        private static final int DO_ADD_HDMI_CEC_TV_INPUT = 5;
1168        private static final int DO_REMOVE_HDMI_CEC_TV_INPUT = 6;
1169
1170        public ServiceHandler(Looper looper) {
1171            super(looper);
1172        }
1173
1174        private void broadcastAddHardwareTvInput(int deviceId, TvInputInfo inputInfo) {
1175            int n = mCallbacks.beginBroadcast();
1176            for (int i = 0; i < n; ++i) {
1177                try {
1178                    mCallbacks.getBroadcastItem(i).addHardwareTvInput(deviceId, inputInfo);
1179                } catch (RemoteException e) {
1180                    Log.e(TAG, "Error while broadcasting.", e);
1181                }
1182            }
1183            mCallbacks.finishBroadcast();
1184        }
1185
1186        private void broadcastAddHdmiCecTvInput(
1187                int logicalAddress, TvInputInfo inputInfo) {
1188            int n = mCallbacks.beginBroadcast();
1189            for (int i = 0; i < n; ++i) {
1190                try {
1191                    mCallbacks.getBroadcastItem(i).addHdmiCecTvInput(logicalAddress, inputInfo);
1192                } catch (RemoteException e) {
1193                    Log.e(TAG, "Error while broadcasting.", e);
1194                }
1195            }
1196            mCallbacks.finishBroadcast();
1197        }
1198
1199        private void broadcastRemoveTvInput(String inputId) {
1200            int n = mCallbacks.beginBroadcast();
1201            for (int i = 0; i < n; ++i) {
1202                try {
1203                    mCallbacks.getBroadcastItem(i).removeTvInput(inputId);
1204                } catch (RemoteException e) {
1205                    Log.e(TAG, "Error while broadcasting.", e);
1206                }
1207            }
1208            mCallbacks.finishBroadcast();
1209        }
1210
1211        @Override
1212        public final void handleMessage(Message msg) {
1213            switch (msg.what) {
1214                case DO_CREATE_SESSION: {
1215                    SomeArgs args = (SomeArgs) msg.obj;
1216                    InputChannel channel = (InputChannel) args.arg1;
1217                    ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2;
1218                    String inputId = (String) args.arg3;
1219                    Session sessionImpl = onCreateSession(inputId);
1220                    args.recycle();
1221                    if (sessionImpl == null) {
1222                        try {
1223                            // Failed to create a session.
1224                            cb.onSessionCreated(null, null);
1225                        } catch (RemoteException e) {
1226                            Log.e(TAG, "error in onSessionCreated");
1227                        }
1228                    } else {
1229                        sessionImpl.setSessionCallback(cb);
1230                        ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this,
1231                                sessionImpl, channel);
1232                        if (sessionImpl instanceof HardwareSession) {
1233                            HardwareSession proxySession =
1234                                    ((HardwareSession) sessionImpl);
1235                            String harewareInputId = proxySession.getHardwareInputId();
1236                            if (!TextUtils.isEmpty(harewareInputId)) {
1237                                // TODO: check if the given ID is really hardware TV input.
1238                                proxySession.mProxySession = stub;
1239                                proxySession.mProxySessionCallback = cb;
1240                                TvInputManager manager = (TvInputManager) getSystemService(
1241                                        Context.TV_INPUT_SERVICE);
1242                                manager.createSession(harewareInputId,
1243                                        proxySession.mHardwareSessionCallback, mServiceHandler);
1244                            } else {
1245                                sessionImpl.onRelease();
1246                                Log.w(TAG, "Hardware input id is not setup yet.");
1247                                try {
1248                                    cb.onSessionCreated(null, null);
1249                                } catch (RemoteException e) {
1250                                    Log.e(TAG, "error in onSessionCreated");
1251                                }
1252                            }
1253                        } else {
1254                            SomeArgs someArgs = SomeArgs.obtain();
1255                            someArgs.arg1 = stub;
1256                            someArgs.arg2 = cb;
1257                            someArgs.arg3 = null;
1258                            mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED,
1259                                    someArgs).sendToTarget();
1260                        }
1261                    }
1262                    return;
1263                }
1264                case DO_NOTIFY_SESSION_CREATED: {
1265                    SomeArgs args = (SomeArgs) msg.obj;
1266                    ITvInputSession stub = (ITvInputSession) args.arg1;
1267                    ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2;
1268                    IBinder hardwareSessionToken = (IBinder) args.arg3;
1269                    try {
1270                        cb.onSessionCreated(stub, hardwareSessionToken);
1271                    } catch (RemoteException e) {
1272                        Log.e(TAG, "error in onSessionCreated");
1273                    }
1274                    args.recycle();
1275                    return;
1276                }
1277                case DO_ADD_HARDWARE_TV_INPUT: {
1278                    TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj;
1279                    TvInputInfo inputInfo = onHardwareAdded(hardwareInfo);
1280                    if (inputInfo != null) {
1281                        broadcastAddHardwareTvInput(hardwareInfo.getDeviceId(), inputInfo);
1282                    }
1283                    return;
1284                }
1285                case DO_REMOVE_HARDWARE_TV_INPUT: {
1286                    TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj;
1287                    String inputId = onHardwareRemoved(hardwareInfo);
1288                    if (inputId != null) {
1289                        broadcastRemoveTvInput(inputId);
1290                    }
1291                    return;
1292                }
1293                case DO_ADD_HDMI_CEC_TV_INPUT: {
1294                    HdmiCecDeviceInfo cecDeviceInfo = (HdmiCecDeviceInfo) msg.obj;
1295                    TvInputInfo inputInfo = onHdmiCecDeviceAdded(cecDeviceInfo);
1296                    if (inputInfo != null) {
1297                        broadcastAddHdmiCecTvInput(cecDeviceInfo.getLogicalAddress(), inputInfo);
1298                    }
1299                    return;
1300                }
1301                case DO_REMOVE_HDMI_CEC_TV_INPUT: {
1302                    HdmiCecDeviceInfo cecDeviceInfo = (HdmiCecDeviceInfo) msg.obj;
1303                    String inputId = onHdmiCecDeviceRemoved(cecDeviceInfo);
1304                    if (inputId != null) {
1305                        broadcastRemoveTvInput(inputId);
1306                    }
1307                    return;
1308                }
1309                default: {
1310                    Log.w(TAG, "Unhandled message code: " + msg.what);
1311                    return;
1312                }
1313            }
1314        }
1315    }
1316}
1317