TvInputService.java revision 969167dc05a6485a32d160895871cff46fd81884
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.app.Service;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.Message;
31import android.os.RemoteCallbackList;
32import android.os.RemoteException;
33import android.util.Log;
34import android.view.Gravity;
35import android.view.InputChannel;
36import android.view.InputDevice;
37import android.view.InputEvent;
38import android.view.InputEventReceiver;
39import android.view.KeyEvent;
40import android.view.MotionEvent;
41import android.view.Surface;
42import android.view.View;
43import android.view.WindowManager;
44import android.view.accessibility.CaptioningManager;
45
46import com.android.internal.annotations.VisibleForTesting;
47import com.android.internal.os.SomeArgs;
48
49import java.util.List;
50
51/**
52 * The TvInputService class represents a TV input or source such as HDMI or built-in tuner which
53 * provides pass-through video or broadcast TV programs.
54 * <p>
55 * Applications will not normally use this service themselves, instead relying on the standard
56 * interaction provided by {@link TvView}. Those implementing TV input services should normally do
57 * so by deriving from this class and providing their own session implementation based on
58 * {@link TvInputService.Session}. All TV input services must require that clients hold the
59 * {@link android.Manifest.permission#BIND_TV_INPUT} in order to interact with the service; if this
60 * permission is not specified in the manifest, the system will refuse to bind to that TV input
61 * service.
62 * </p>
63 */
64public abstract class TvInputService extends Service {
65    // STOPSHIP: Turn debugging off.
66    private static final boolean DEBUG = true;
67    private static final String TAG = "TvInputService";
68
69    /**
70     * This is the interface name that a service implementing a TV input should say that it support
71     * -- that is, this is the action it uses for its intent filter. To be supported, the service
72     * must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that
73     * other applications cannot abuse it.
74     */
75    public static final String SERVICE_INTERFACE = "android.media.tv.TvInputService";
76
77    /**
78     * Name under which a TvInputService component publishes information about itself.
79     * This meta-data must reference an XML resource containing an
80     * <code>&lt;{@link android.R.styleable#TvInputService tv-input}&gt;</code>
81     * tag.
82     */
83    public static final String SERVICE_META_DATA = "android.media.tv.input";
84
85    private final Handler mHandler = new ServiceHandler();
86    private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks =
87            new RemoteCallbackList<ITvInputServiceCallback>();
88
89    @Override
90    public final IBinder onBind(Intent intent) {
91        return new ITvInputService.Stub() {
92            @Override
93            public void registerCallback(ITvInputServiceCallback cb) {
94                if (cb != null) {
95                    mCallbacks.register(cb);
96                }
97            }
98
99            @Override
100            public void unregisterCallback(ITvInputServiceCallback cb) {
101                if (cb != null) {
102                    mCallbacks.unregister(cb);
103                }
104            }
105
106            @Override
107            public void createSession(InputChannel channel, ITvInputSessionCallback cb) {
108                if (channel == null) {
109                    Log.w(TAG, "Creating session without input channel");
110                }
111                if (cb == null) {
112                    return;
113                }
114                SomeArgs args = SomeArgs.obtain();
115                args.arg1 = channel;
116                args.arg2 = cb;
117                mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget();
118            }
119        };
120    }
121
122    /**
123     * Get the number of callbacks that are registered.
124     *
125     * @hide
126     */
127    @VisibleForTesting
128    public final int getRegisteredCallbackCount() {
129        return mCallbacks.getRegisteredCallbackCount();
130    }
131
132    /**
133     * Returns a concrete implementation of {@link Session}.
134     * <p>
135     * May return {@code null} if this TV input service fails to create a session for some reason.
136     * </p>
137     */
138    public abstract Session onCreateSession();
139
140    /**
141     * Base class for derived classes to implement to provide a TV input session.
142     */
143    public abstract class Session implements KeyEvent.Callback {
144        private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
145        private final WindowManager mWindowManager;
146        private WindowManager.LayoutParams mWindowParams;
147        private Surface mSurface;
148        private View mOverlayView;
149        private boolean mOverlayViewEnabled;
150        private IBinder mWindowToken;
151        private Rect mOverlayFrame;
152        private ITvInputSessionCallback mSessionCallback;
153
154        public Session() {
155            mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
156        }
157
158        /**
159         * Enables or disables the overlay view. By default, the overlay view is disabled. Must be
160         * called explicitly after the session is created to enable the overlay view.
161         *
162         * @param enable {@code true} if you want to enable the overlay view. {@code false}
163         *            otherwise.
164         */
165        public void setOverlayViewEnabled(final boolean enable) {
166            mHandler.post(new Runnable() {
167                @Override
168                public void run() {
169                    if (enable == mOverlayViewEnabled) {
170                        return;
171                    }
172                    mOverlayViewEnabled = enable;
173                    if (enable) {
174                        if (mWindowToken != null) {
175                            createOverlayView(mWindowToken, mOverlayFrame);
176                        }
177                    } else {
178                        removeOverlayView(false);
179                    }
180                }
181            });
182        }
183
184        /**
185         * Dispatches an event to the application using this session.
186         *
187         * @param eventType The type of the event.
188         * @param eventArgs Optional arguments of the event.
189         * @hide
190         */
191        public void dispatchSessionEvent(final String eventType, final Bundle eventArgs) {
192            if (eventType == null) {
193                throw new IllegalArgumentException("eventType should not be null.");
194            }
195            mHandler.post(new Runnable() {
196                @Override
197                public void run() {
198                    try {
199                        if (DEBUG) Log.d(TAG, "dispatchSessionEvent(" + eventType + ")");
200                        mSessionCallback.onSessionEvent(eventType, eventArgs);
201                    } catch (RemoteException e) {
202                        Log.w(TAG, "error in sending event (event=" + eventType + ")");
203                    }
204                }
205            });
206        }
207
208        /**
209         * Notifies the channel of the session is retuned by TV input.
210         *
211         * @param channelUri The URI of a channel.
212         */
213        public void dispatchChannelRetuned(final Uri channelUri) {
214            mHandler.post(new Runnable() {
215                @Override
216                public void run() {
217                    try {
218                        if (DEBUG) Log.d(TAG, "dispatchChannelRetuned");
219                        mSessionCallback.onChannelRetuned(channelUri);
220                    } catch (RemoteException e) {
221                        Log.w(TAG, "error in dispatchChannelRetuned");
222                    }
223                }
224            });
225        }
226
227        /**
228         * Sends the change on the track information. This is expected to be called whenever a
229         * track is added/removed and the metadata of a track is modified.
230         *
231         * @param tracks A list which includes track information.
232         */
233        public void dispatchTrackInfoChanged(final List<TvTrackInfo> tracks) {
234            if (!TvTrackInfo.checkSanity(tracks)) {
235                throw new IllegalArgumentException(
236                        "Two or more selected tracks for a track type.");
237            }
238            mHandler.post(new Runnable() {
239                @Override
240                public void run() {
241                    try {
242                        if (DEBUG) Log.d(TAG, "dispatchTrackInfoChanged");
243                        mSessionCallback.onTrackInfoChanged(tracks);
244                    } catch (RemoteException e) {
245                        Log.w(TAG, "error in dispatchTrackInfoChanged");
246                    }
247                }
248            });
249        }
250
251        /**
252         * Informs the application that video is available and the playback of the TV stream has
253         * been started.
254         */
255        public void dispatchVideoAvailable() {
256            mHandler.post(new Runnable() {
257                @Override
258                public void run() {
259                    try {
260                        if (DEBUG) Log.d(TAG, "dispatchVideoAvailable");
261                        mSessionCallback.onVideoAvailable();
262                    } catch (RemoteException e) {
263                        Log.w(TAG, "error in dispatchVideoAvailable");
264                    }
265                }
266            });
267        }
268
269        /**
270         * Informs the application that video is not available, so the TV input cannot continue
271         * playing the TV stream.
272         *
273         * @param reason The reason why the TV input stopped the playback:
274         * <ul>
275         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
276         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNE}
277         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
278         * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
279         * </ul>
280         */
281        public void dispatchVideoUnavailable(final int reason) {
282            if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
283                    && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNE
284                    && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL
285                    && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
286                throw new IllegalArgumentException("Unknown reason: " + reason);
287            }
288            mHandler.post(new Runnable() {
289                @Override
290                public void run() {
291                    try {
292                        if (DEBUG) Log.d(TAG, "dispatchVideoUnavailable");
293                        mSessionCallback.onVideoUnavailable(reason);
294                    } catch (RemoteException e) {
295                        Log.w(TAG, "error in dispatchVideoUnavailable");
296                    }
297                }
298            });
299        }
300
301        /**
302         * Called when the session is released.
303         */
304        public abstract void onRelease();
305
306        /**
307         * Sets the {@link Surface} for the current input session on which the TV input renders
308         * video.
309         *
310         * @param surface {@link Surface} an application passes to this TV input session.
311         * @return {@code true} if the surface was set, {@code false} otherwise.
312         */
313        public abstract boolean onSetSurface(Surface surface);
314
315        /**
316         * Sets the relative stream volume of the current TV input session to handle the change of
317         * audio focus by setting.
318         *
319         * @param volume Volume scale from 0.0 to 1.0.
320         */
321        public abstract void onSetStreamVolume(float volume);
322
323        /**
324         * Tunes to a given channel. When the video is available, {@link #dispatchVideoAvailable()}
325         * should be called. Also, {@link #dispatchVideoUnavailable(int)} should be called when the
326         * TV input cannot continue playing the given channel.
327         *
328         * @param channelUri The URI of the channel.
329         * @return {@code true} the tuning was successful, {@code false} otherwise.
330         */
331        public abstract boolean onTune(Uri channelUri);
332
333        /**
334         * Enables or disables the caption.
335         * <p>
336         * The locale for the user's preferred captioning language can be obtained by calling
337         * {@link CaptioningManager#getLocale CaptioningManager.getLocale()}.
338         *
339         * @param enabled {@code true} to enable, {@code false} to disable.
340         * @see CaptioningManager
341         */
342        public abstract void onSetCaptionEnabled(boolean enabled);
343
344        /**
345         * Selects a given track.
346         * <p>
347         * If it is called multiple times on the same type of track (ie. Video, Audio, Text), the
348         * track selected previously should be unselected in the implementation of this method.
349         * Also, if the select operation was successful, the implementation should call
350         * {@link #dispatchTrackInfoChanged(List)} to report the updated track information.
351         * </p>
352         *
353         * @param track The track to be selected.
354         * @return {@code true} if the select operation was successful, {@code false} otherwise.
355         * @see #dispatchTrackInfoChanged
356         * @see TvTrackInfo#KEY_IS_SELECTED
357         */
358        public boolean onSelectTrack(TvTrackInfo track) {
359            return false;
360        }
361
362        /**
363         * Unselects a given track.
364         * <p>
365         * If the unselect operation was successful, the implementation should call
366         * {@link #dispatchTrackInfoChanged(List)} to report the updated track information.
367         * </p>
368         *
369         * @param track The track to be unselected.
370         * @return {@code true} if the unselect operation was successful, {@code false} otherwise.
371         * @see #dispatchTrackInfoChanged
372         * @see TvTrackInfo#KEY_IS_SELECTED
373         */
374        public boolean onUnselectTrack(TvTrackInfo track) {
375            return false;
376        }
377
378        /**
379         * Called when an application requests to create an overlay view. Each session
380         * implementation can override this method and return its own view.
381         *
382         * @return a view attached to the overlay window
383         */
384        public View onCreateOverlayView() {
385            return null;
386        }
387
388        /**
389         * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent)
390         * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event).
391         * <p>
392         * Override this to intercept key down events before they are processed by the application.
393         * If you return true, the application will not process the event itself. If you return
394         * false, the normal application processing will occur as if the TV input had not seen the
395         * event at all.
396         *
397         * @param keyCode The value in event.getKeyCode().
398         * @param event Description of the key event.
399         * @return If you handled the event, return {@code true}. If you want to allow the event to
400         *         be handled by the next receiver, return {@code false}.
401         */
402        @Override
403        public boolean onKeyDown(int keyCode, KeyEvent event) {
404            return false;
405        }
406
407        /**
408         * Default implementation of
409         * {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
410         * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event).
411         * <p>
412         * Override this to intercept key long press events before they are processed by the
413         * application. If you return true, the application will not process the event itself. If
414         * you return false, the normal application processing will occur as if the TV input had not
415         * seen the event at all.
416         *
417         * @param keyCode The value in event.getKeyCode().
418         * @param event Description of the key event.
419         * @return If you handled the event, return {@code true}. If you want to allow the event to
420         *         be handled by the next receiver, return {@code false}.
421         */
422        @Override
423        public boolean onKeyLongPress(int keyCode, KeyEvent event) {
424            return false;
425        }
426
427        /**
428         * Default implementation of
429         * {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
430         * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event).
431         * <p>
432         * Override this to intercept special key multiple events before they are processed by the
433         * application. If you return true, the application will not itself process the event. If
434         * you return false, the normal application processing will occur as if the TV input had not
435         * seen the event at all.
436         *
437         * @param keyCode The value in event.getKeyCode().
438         * @param count The number of times the action was made.
439         * @param event Description of the key event.
440         * @return If you handled the event, return {@code true}. If you want to allow the event to
441         *         be handled by the next receiver, return {@code false}.
442         */
443        @Override
444        public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
445            return false;
446        }
447
448        /**
449         * Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent)
450         * KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event).
451         * <p>
452         * Override this to intercept key up events before they are processed by the application. If
453         * you return true, the application will not itself process the event. If you return false,
454         * the normal application processing will occur as if the TV input had not seen the event at
455         * all.
456         *
457         * @param keyCode The value in event.getKeyCode().
458         * @param event Description of the key event.
459         * @return If you handled the event, return {@code true}. If you want to allow the event to
460         *         be handled by the next receiver, return {@code false}.
461         */
462        @Override
463        public boolean onKeyUp(int keyCode, KeyEvent event) {
464            return false;
465        }
466
467        /**
468         * Implement this method to handle touch screen motion events on the current input session.
469         *
470         * @param event The motion event being received.
471         * @return If you handled the event, return {@code true}. If you want to allow the event to
472         *         be handled by the next receiver, return {@code false}.
473         * @see View#onTouchEvent
474         */
475        public boolean onTouchEvent(MotionEvent event) {
476            return false;
477        }
478
479        /**
480         * Implement this method to handle trackball events on the current input session.
481         *
482         * @param event The motion event being received.
483         * @return If you handled the event, return {@code true}. If you want to allow the event to
484         *         be handled by the next receiver, return {@code false}.
485         * @see View#onTrackballEvent
486         */
487        public boolean onTrackballEvent(MotionEvent event) {
488            return false;
489        }
490
491        /**
492         * Implement this method to handle generic motion events on the current input session.
493         *
494         * @param event The motion event being received.
495         * @return If you handled the event, return {@code true}. If you want to allow the event to
496         *         be handled by the next receiver, return {@code false}.
497         * @see View#onGenericMotionEvent
498         */
499        public boolean onGenericMotionEvent(MotionEvent event) {
500            return false;
501        }
502
503        /**
504         * This method is called when the application would like to stop using the current input
505         * session.
506         */
507        void release() {
508            onRelease();
509            if (mSurface != null) {
510                mSurface.release();
511                mSurface = null;
512            }
513            removeOverlayView(true);
514        }
515
516        /**
517         * Calls {@link #onSetSurface}.
518         */
519        void setSurface(Surface surface) {
520            onSetSurface(surface);
521            if (mSurface != null) {
522                mSurface.release();
523            }
524            mSurface = surface;
525            // TODO: Handle failure.
526        }
527
528        /**
529         * Calls {@link #onSetStreamVolume}.
530         */
531        void setVolume(float volume) {
532            onSetStreamVolume(volume);
533        }
534
535        /**
536         * Calls {@link #onTune}.
537         */
538        void tune(Uri channelUri) {
539            onTune(channelUri);
540            // TODO: Handle failure.
541        }
542
543        /**
544         * Calls {@link #onSetCaptionEnabled}.
545         */
546        void setCaptionEnabled(boolean enabled) {
547            onSetCaptionEnabled(enabled);
548        }
549
550        /**
551         * Calls {@link #onSelectTrack}.
552         */
553        void selectTrack(TvTrackInfo track) {
554            onSelectTrack(track);
555            // TODO: Handle failure.
556        }
557
558        /**
559         * Calls {@link #onUnselectTrack}.
560         */
561        void unselectTrack(TvTrackInfo track) {
562            onUnselectTrack(track);
563            // TODO: Handle failure.
564        }
565
566        /**
567         * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach
568         * to the overlay window.
569         *
570         * @param windowToken A window token of an application.
571         * @param frame A position of the overlay view.
572         */
573        void createOverlayView(IBinder windowToken, Rect frame) {
574            if (mOverlayView != null) {
575                mWindowManager.removeView(mOverlayView);
576                mOverlayView = null;
577            }
578            if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")");
579            mWindowToken = windowToken;
580            mOverlayFrame = frame;
581            if (!mOverlayViewEnabled) {
582                return;
583            }
584            mOverlayView = onCreateOverlayView();
585            if (mOverlayView == null) {
586                return;
587            }
588            // TvView's window type is TYPE_APPLICATION_MEDIA and we want to create
589            // an overlay window above the media window but below the application window.
590            int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
591            // We make the overlay view non-focusable and non-touchable so that
592            // the application that owns the window token can decide whether to consume or
593            // dispatch the input events.
594            int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
595                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
596                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
597            mWindowParams = new WindowManager.LayoutParams(
598                    frame.right - frame.left, frame.bottom - frame.top,
599                    frame.left, frame.top, type, flag, PixelFormat.TRANSPARENT);
600            mWindowParams.privateFlags |=
601                    WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
602            mWindowParams.gravity = Gravity.START | Gravity.TOP;
603            mWindowParams.token = windowToken;
604            mWindowManager.addView(mOverlayView, mWindowParams);
605        }
606
607        /**
608         * Relayouts the current overlay view.
609         *
610         * @param frame A new position of the overlay view.
611         */
612        void relayoutOverlayView(Rect frame) {
613            if (DEBUG) Log.d(TAG, "relayoutOverlayView(" + frame + ")");
614            mOverlayFrame = frame;
615            if (!mOverlayViewEnabled || mOverlayView == null) {
616                return;
617            }
618            mWindowParams.x = frame.left;
619            mWindowParams.y = frame.top;
620            mWindowParams.width = frame.right - frame.left;
621            mWindowParams.height = frame.bottom - frame.top;
622            mWindowManager.updateViewLayout(mOverlayView, mWindowParams);
623        }
624
625        /**
626         * Removes the current overlay view.
627         */
628        void removeOverlayView(boolean clearWindowToken) {
629            if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayView + ")");
630            if (clearWindowToken) {
631                mWindowToken = null;
632                mOverlayFrame = null;
633            }
634            if (mOverlayView != null) {
635                mWindowManager.removeView(mOverlayView);
636                mOverlayView = null;
637                mWindowParams = null;
638            }
639        }
640
641        /**
642         * Takes care of dispatching incoming input events and tells whether the event was handled.
643         */
644        int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
645            if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")");
646            boolean isNavigationKey = false;
647            if (event instanceof KeyEvent) {
648                KeyEvent keyEvent = (KeyEvent) event;
649                isNavigationKey = isNavigationKey(keyEvent.getKeyCode());
650                if (keyEvent.dispatch(this, mDispatcherState, this)) {
651                    return TvInputManager.Session.DISPATCH_HANDLED;
652                }
653            } else if (event instanceof MotionEvent) {
654                MotionEvent motionEvent = (MotionEvent) event;
655                final int source = motionEvent.getSource();
656                if (motionEvent.isTouchEvent()) {
657                    if (onTouchEvent(motionEvent)) {
658                        return TvInputManager.Session.DISPATCH_HANDLED;
659                    }
660                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
661                    if (onTrackballEvent(motionEvent)) {
662                        return TvInputManager.Session.DISPATCH_HANDLED;
663                    }
664                } else {
665                    if (onGenericMotionEvent(motionEvent)) {
666                        return TvInputManager.Session.DISPATCH_HANDLED;
667                    }
668                }
669            }
670            if (mOverlayView == null || !mOverlayView.isAttachedToWindow()) {
671                return TvInputManager.Session.DISPATCH_NOT_HANDLED;
672            }
673            if (!mOverlayView.hasWindowFocus()) {
674                mOverlayView.getViewRootImpl().windowFocusChanged(true, true);
675            }
676            if (isNavigationKey && mOverlayView.hasFocusable()) {
677                // If mOverlayView has focusable views, navigation key events should be always
678                // handled. If not, it can make the application UI navigation messed up.
679                // For example, in the case that the left-most view is focused, a left key event
680                // will not be handled in ViewRootImpl. Then, the left key event will be handled in
681                // the application during the UI navigation of the TV input.
682                mOverlayView.getViewRootImpl().dispatchInputEvent(event);
683                return TvInputManager.Session.DISPATCH_HANDLED;
684            } else {
685                mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver);
686                return TvInputManager.Session.DISPATCH_IN_PROGRESS;
687            }
688        }
689
690        private void setSessionCallback(ITvInputSessionCallback callback) {
691            mSessionCallback = callback;
692        }
693    }
694
695    /** @hide */
696    public static boolean isNavigationKey(int keyCode) {
697        switch (keyCode) {
698            case KeyEvent.KEYCODE_DPAD_LEFT:
699            case KeyEvent.KEYCODE_DPAD_RIGHT:
700            case KeyEvent.KEYCODE_DPAD_UP:
701            case KeyEvent.KEYCODE_DPAD_DOWN:
702            case KeyEvent.KEYCODE_DPAD_CENTER:
703            case KeyEvent.KEYCODE_PAGE_UP:
704            case KeyEvent.KEYCODE_PAGE_DOWN:
705            case KeyEvent.KEYCODE_MOVE_HOME:
706            case KeyEvent.KEYCODE_MOVE_END:
707            case KeyEvent.KEYCODE_TAB:
708            case KeyEvent.KEYCODE_SPACE:
709            case KeyEvent.KEYCODE_ENTER:
710                return true;
711        }
712        return false;
713    }
714
715    @SuppressLint("HandlerLeak")
716    private final class ServiceHandler extends Handler {
717        private static final int DO_CREATE_SESSION = 1;
718
719        @Override
720        public final void handleMessage(Message msg) {
721            switch (msg.what) {
722                case DO_CREATE_SESSION: {
723                    SomeArgs args = (SomeArgs) msg.obj;
724                    InputChannel channel = (InputChannel) args.arg1;
725                    ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2;
726                    try {
727                        Session sessionImpl = onCreateSession();
728                        if (sessionImpl == null) {
729                            // Failed to create a session.
730                            cb.onSessionCreated(null);
731                        } else {
732                            sessionImpl.setSessionCallback(cb);
733                            ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this,
734                                    sessionImpl, channel);
735                            cb.onSessionCreated(stub);
736                        }
737                    } catch (RemoteException e) {
738                        Log.e(TAG, "error in onSessionCreated");
739                    }
740                    args.recycle();
741                    return;
742                }
743                default: {
744                    Log.w(TAG, "Unhandled message code: " + msg.what);
745                    return;
746                }
747            }
748        }
749    }
750}
751