From 6a6059a29edf31e65541b3d8927a46f5846fb0a2 Mon Sep 17 00:00:00 2001 From: Jae Seo Date: Thu, 17 Apr 2014 21:35:29 -0700 Subject: [PATCH] Dispatch input events to the TV input TvInputManagerService now creates an InputChannel pair and passes one end to the app and the other end to the service. Then the TvInputManager in the app creates an InputEventSender around one end of the channel to send events and the service creates an InputEventReceiver around the other end of the channel to receive events. Most of the newly added code here was borrowed heavily from the IME. Change-Id: I0bd83847cba0033ccb6f4b6dad267ebeaf5e9c7c --- api/current.txt | 16 +- core/java/android/tv/ITvInputClient.aidl | 3 +- core/java/android/tv/ITvInputService.aidl | 3 +- .../android/tv/ITvInputSessionWrapper.java | 67 ++++- core/java/android/tv/TvInputManager.java | 266 +++++++++++++++++- core/java/android/tv/TvInputService.java | 205 +++++++++++++- core/java/android/tv/TvView.java | 142 +++++++++- core/java/android/view/ViewRootImpl.java | 16 +- .../server/tv/TvInputManagerService.java | 21 +- 9 files changed, 691 insertions(+), 48 deletions(-) diff --git a/api/current.txt b/api/current.txt index 34b1ac800ac54..c924aac4849d2 100644 --- a/api/current.txt +++ b/api/current.txt @@ -28161,12 +28161,19 @@ package android.tv { field public static final java.lang.String SERVICE_INTERFACE = "android.tv.TvInputService"; } - public abstract class TvInputService.TvInputSessionImpl { + public abstract class TvInputService.TvInputSessionImpl implements android.view.KeyEvent.Callback { ctor public TvInputService.TvInputSessionImpl(); method public android.view.View onCreateOverlayView(); + method public boolean onGenericMotionEvent(android.view.MotionEvent); + method public boolean onKeyDown(int, android.view.KeyEvent); + method public boolean onKeyLongPress(int, android.view.KeyEvent); + method public boolean onKeyMultiple(int, int, android.view.KeyEvent); + method public boolean onKeyUp(int, android.view.KeyEvent); method public abstract void onRelease(); method public abstract boolean onSetSurface(android.view.Surface); method public abstract void onSetVolume(float); + method public boolean onTouchEvent(android.view.MotionEvent); + method public boolean onTrackballEvent(android.view.MotionEvent); method public abstract boolean onTune(android.net.Uri); method public void setOverlayViewEnabled(boolean); } @@ -28176,9 +28183,16 @@ package android.tv { ctor public TvView(android.content.Context, android.util.AttributeSet); ctor public TvView(android.content.Context, android.util.AttributeSet, int); method public void bindTvInput(android.content.ComponentName, android.tv.TvInputManager.SessionCreateCallback); + method public boolean dispatchUnhandledInputEvent(android.view.InputEvent); + method public boolean onUnhandledInputEvent(android.view.InputEvent); + method public void setOnUnhandledInputEventListener(android.tv.TvView.OnUnhandledInputEventListener); method public void unbindTvInput(); } + public static abstract interface TvView.OnUnhandledInputEventListener { + method public abstract boolean onUnhandledInputEvent(android.view.InputEvent); + } + } package android.util { diff --git a/core/java/android/tv/ITvInputClient.aidl b/core/java/android/tv/ITvInputClient.aidl index 43be6f008fd25..538f8a173daa9 100644 --- a/core/java/android/tv/ITvInputClient.aidl +++ b/core/java/android/tv/ITvInputClient.aidl @@ -18,6 +18,7 @@ package android.tv; import android.content.ComponentName; import android.tv.ITvInputSession; +import android.view.InputChannel; /** * Interface a client of the ITvInputManager implements, to identify itself and receive information @@ -25,6 +26,6 @@ import android.tv.ITvInputSession; * @hide */ oneway interface ITvInputClient { - void onSessionCreated(in ComponentName name, IBinder token, int seq); + void onSessionCreated(in ComponentName name, IBinder token, in InputChannel channel, int seq); void onAvailabilityChanged(in ComponentName name, boolean isAvailable); } diff --git a/core/java/android/tv/ITvInputService.aidl b/core/java/android/tv/ITvInputService.aidl index 672784fc6dff5..4f1bc2bc74dc1 100644 --- a/core/java/android/tv/ITvInputService.aidl +++ b/core/java/android/tv/ITvInputService.aidl @@ -18,6 +18,7 @@ package android.tv; import android.tv.ITvInputServiceCallback; import android.tv.ITvInputSessionCallback; +import android.view.InputChannel; /** * Top-level interface to a TV input component (implemented in a Service). @@ -26,5 +27,5 @@ import android.tv.ITvInputSessionCallback; oneway interface ITvInputService { void registerCallback(ITvInputServiceCallback callback); void unregisterCallback(in ITvInputServiceCallback callback); - void createSession(ITvInputSessionCallback callback); + void createSession(in InputChannel channel, ITvInputSessionCallback callback); } diff --git a/core/java/android/tv/ITvInputSessionWrapper.java b/core/java/android/tv/ITvInputSessionWrapper.java index a6e0877cc9144..3ccccf391a756 100644 --- a/core/java/android/tv/ITvInputSessionWrapper.java +++ b/core/java/android/tv/ITvInputSessionWrapper.java @@ -20,9 +20,16 @@ import android.content.Context; import android.graphics.Rect; import android.net.Uri; import android.os.IBinder; +import android.os.Looper; import android.os.Message; +import android.tv.TvInputManager.Session; import android.tv.TvInputService.TvInputSessionImpl; import android.util.Log; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.Surface; import com.android.internal.os.HandlerCaller; @@ -45,50 +52,66 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand private static final int DO_RELAYOUT_OVERLAY_VIEW = 6; private static final int DO_REMOVE_OVERLAY_VIEW = 7; - private TvInputSessionImpl mTvInputSession; private final HandlerCaller mCaller; - public ITvInputSessionWrapper(Context context, TvInputSessionImpl session) { + private TvInputSessionImpl mTvInputSessionImpl; + private InputChannel mChannel; + private TvInputEventReceiver mReceiver; + + public ITvInputSessionWrapper(Context context, TvInputSessionImpl sessionImpl, + InputChannel channel) { mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); - mTvInputSession = session; + mTvInputSessionImpl = sessionImpl; + mChannel = channel; + if (channel != null) { + mReceiver = new TvInputEventReceiver(channel, context.getMainLooper()); + } } @Override public void executeMessage(Message msg) { - if (mTvInputSession == null) { + if (mTvInputSessionImpl == null) { return; } switch (msg.what) { case DO_RELEASE: { - mTvInputSession.release(); - mTvInputSession = null; + mTvInputSessionImpl.release(); + mTvInputSessionImpl = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } return; } case DO_SET_SURFACE: { - mTvInputSession.setSurface((Surface) msg.obj); + mTvInputSessionImpl.setSurface((Surface) msg.obj); return; } case DO_SET_VOLUME: { - mTvInputSession.setVolume((Float) msg.obj); + mTvInputSessionImpl.setVolume((Float) msg.obj); return; } case DO_TUNE: { - mTvInputSession.tune((Uri) msg.obj); + mTvInputSessionImpl.tune((Uri) msg.obj); return; } case DO_CREATE_OVERLAY_VIEW: { SomeArgs args = (SomeArgs) msg.obj; - mTvInputSession.createOverlayView((IBinder) args.arg1, (Rect) args.arg2); + mTvInputSessionImpl.createOverlayView((IBinder) args.arg1, (Rect) args.arg2); args.recycle(); return; } case DO_RELAYOUT_OVERLAY_VIEW: { - mTvInputSession.relayoutOverlayView((Rect) msg.obj); + mTvInputSessionImpl.relayoutOverlayView((Rect) msg.obj); return; } case DO_REMOVE_OVERLAY_VIEW: { - mTvInputSession.removeOverlayView(true); + mTvInputSessionImpl.removeOverlayView(true); return; } default: { @@ -133,4 +156,24 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand public void removeOverlayView() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_OVERLAY_VIEW)); } + + private final class TvInputEventReceiver extends InputEventReceiver { + public TvInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mTvInputSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mTvInputSessionImpl.dispatchInputEvent(event, this); + if (handled != Session.DISPATCH_IN_PROGRESS) { + finishInputEvent(event, handled == Session.DISPATCH_HANDLED); + } + } + } } diff --git a/core/java/android/tv/TvInputManager.java b/core/java/android/tv/TvInputManager.java index 05f0b9cf70ab6..7b9b1fb6b67fd 100644 --- a/core/java/android/tv/TvInputManager.java +++ b/core/java/android/tv/TvInputManager.java @@ -21,9 +21,16 @@ import android.graphics.Rect; import android.net.Uri; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.util.Pools.Pool; +import android.util.Pools.SimplePool; import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; import android.view.Surface; import android.view.View; @@ -138,7 +145,8 @@ public final class TvInputManager { mUserId = userId; mClient = new ITvInputClient.Stub() { @Override - public void onSessionCreated(ComponentName name, IBinder token, int seq) { + public void onSessionCreated(ComponentName name, IBinder token, InputChannel channel, + int seq) { synchronized (mSessionCreateCallbackRecordMap) { SessionCreateCallbackRecord record = mSessionCreateCallbackRecordMap.get(seq); mSessionCreateCallbackRecordMap.delete(seq); @@ -148,7 +156,7 @@ public final class TvInputManager { } Session session = null; if (token != null) { - session = new Session(name, token, mService, mUserId); + session = new Session(token, channel, mService, mUserId); } record.postSessionCreated(session); } @@ -321,13 +329,30 @@ public final class TvInputManager { /** The Session provides the per-session functionality of TV inputs. */ public static final class Session { + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; + private final ITvInputManager mService; private final int mUserId; + + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); + + private final Pool mPendingEventPool = new SimplePool(20); + private final SparseArray mPendingEvents = new SparseArray(20); + private IBinder mToken; + private TvInputEventSender mSender; + private InputChannel mChannel; /** @hide */ - private Session(ComponentName name, IBinder token, ITvInputManager service, int userId) { + private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId) { mToken = token; + mChannel = channel; mService = service; mUserId = userId; } @@ -347,6 +372,18 @@ public final class TvInputManager { } catch (RemoteException e) { throw new RuntimeException(e); } + + synchronized (mHandler) { + if (mChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mChannel.dispose(); + mChannel = null; + } + } } /** @@ -478,5 +515,228 @@ public final class TvInputManager { throw new RuntimeException(e); } } + + /** + * Dispatches an input event to this session. + * + * @param event {@link InputEvent} to dispatch. + * @param token A token used to identify the input event later in the callback. + * @param callback A callback used to receive the dispatch result. + * @param handler {@link Handler} that the dispatch result will be delivered to. + * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns + * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns + * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will + * be invoked later. + * @throws IllegalArgumentException if any of the necessary arguments is {@code null}. + * @hide + */ + public int dispatchInputEvent(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + if (event == null) { + throw new IllegalArgumentException("event cannot be null"); + } + if (callback != null && handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mHandler) { + if (mChannel == null) { + return DISPATCH_NOT_HANDLED; + } + PendingEvent p = obtainPendingEventLocked(event, token, callback, handler); + if (Looper.myLooper() == Looper.getMainLooper()) { + // Already running on the main thread so we can send the event immediately. + return sendInputEventOnMainLooperLocked(p); + } + + // Post the event to the main thread. + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessage(msg); + return DISPATCH_IN_PROGRESS; + } + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token a token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + public void onFinishedInputEvent(Object token, boolean handled); + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for seesion to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + PendingEvent p = mPendingEventPool.acquire(); + if (p == null) { + p = new PendingEvent(); + } + p.mEvent = event; + p.mToken = token; + p.mCallback = callback; + p.mHandler = handler; + return p; + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + private final class InputEventHandler extends Handler { + public static final int MSG_SEND_INPUT_EVENT = 1; + public static final int MSG_TIMEOUT_INPUT_EVENT = 2; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + private final class TvInputEventSender extends InputEventSender { + public TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mToken; + public FinishedInputEventCallback mCallback; + public Handler mHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mToken = null; + mCallback = null; + mHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mToken, mHandled); + + synchronized (mHandler) { + recyclePendingEventLocked(this); + } + } + } } } diff --git a/core/java/android/tv/TvInputService.java b/core/java/android/tv/TvInputService.java index 80eb407522288..636e3b40616c2 100644 --- a/core/java/android/tv/TvInputService.java +++ b/core/java/android/tv/TvInputService.java @@ -28,13 +28,21 @@ import android.os.IBinder; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.tv.TvInputManager.Session; import android.util.Log; import android.view.Gravity; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.SomeArgs; /** * A base class for implementing television input service. @@ -89,10 +97,17 @@ public abstract class TvInputService extends Service { } @Override - public void createSession(ITvInputSessionCallback cb) { - if (cb != null) { - mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, cb).sendToTarget(); + public void createSession(InputChannel channel, ITvInputSessionCallback cb) { + if (channel == null) { + Log.w(TAG, "Creating session without input channel"); } + if (cb == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = channel; + args.arg2 = cb; + mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget(); } }; } @@ -131,7 +146,8 @@ public abstract class TvInputService extends Service { /** * Base class for derived classes to implement to provide {@link TvInputManager.Session}. */ - public abstract class TvInputSessionImpl { + public abstract class TvInputSessionImpl implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); private final WindowManager mWindowManager; private WindowManager.LayoutParams mWindowParams; private View mOverlayView; @@ -143,6 +159,13 @@ public abstract class TvInputService extends Service { mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); } + /** + * Enables or disables the overlay view. By default, the overlay view is disabled. Must be + * called explicitly after the session is created to enable the overlay view. + * + * @param enable {@code true} if you want to enable the overlay view. {@code false} + * otherwise. + */ public void setOverlayViewEnabled(final boolean enable) { mHandler.post(new Runnable() { @Override @@ -202,6 +225,121 @@ public abstract class TvInputService extends Service { return null; } + /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent) + * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event). + *

+ * Override this to intercept key down events before they are processed by the application. + * If you return true, the application will not process the event itself. If you return + * false, the normal application processing will occur as if the TV input had not seen the + * event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + /** + * Default implementation of + * {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event). + *

+ * Override this to intercept key long press events before they are processed by the + * application. If you return true, the application will not process the event itself. If + * you return false, the normal application processing will occur as if the TV input had not + * seen the event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + /** + * Default implementation of + * {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) + * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event). + *

+ * Override this to intercept special key multiple events before they are processed by the + * application. If you return true, the application will not itself process the event. If + * you return false, the normal application processing will occur as if the TV input had not + * seen the event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param count The number of times the action was made. + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + + /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent) + * KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event). + *

+ * Override this to intercept key up events before they are processed by the application. If + * you return true, the application will not itself process the event. If you return false, + * the normal application processing will occur as if the TV input had not seen the event at + * all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + /** + * Implement this method to handle touch screen motion events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTouchEvent + */ + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + /** + * Implement this method to handle trackball events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTrackballEvent + */ + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + /** + * Implement this method to handle generic motion events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onGenericMotionEvent + */ + public boolean onGenericMotionEvent(MotionEvent event) { + return false; + } + /** * This method is called when the application would like to stop using the current input * session. @@ -212,7 +350,7 @@ public abstract class TvInputService extends Service { } /** - * Calls {@link onSetSurface}. + * Calls {@link #onSetSurface}. */ void setSurface(Surface surface) { onSetSurface(surface); @@ -220,14 +358,14 @@ public abstract class TvInputService extends Service { } /** - * Calls {@link onSetVolume}. + * Calls {@link #onSetVolume}. */ void setVolume(float volume) { onSetVolume(volume); } /** - * Calls {@link onTune}. + * Calls {@link #onTune}. */ void tune(Uri channelUri) { onTune(channelUri); @@ -235,8 +373,8 @@ public abstract class TvInputService extends Service { } /** - * Creates an overlay view. This calls {@link onCreateOverlayView} to get - * a view to attach to the overlay window. + * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach + * to the overlay window. * * @param windowToken A window token of an application. * @param frame A position of the overlay view. @@ -314,6 +452,42 @@ public abstract class TvInputService extends Service { mWindowParams = null; } } + + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + if (((KeyEvent) event).dispatch(this, mDispatcherState, this)) { + return Session.DISPATCH_HANDLED; + } + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } + } + if (mOverlayView == null) { + return Session.DISPATCH_NOT_HANDLED; + } + if (!mOverlayView.hasWindowFocus()) { + mOverlayView.getViewRootImpl().windowFocusChanged(true, true); + } + mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver); + return Session.DISPATCH_IN_PROGRESS; + } } private final class ServiceHandler extends Handler { @@ -324,20 +498,23 @@ public abstract class TvInputService extends Service { public final void handleMessage(Message msg) { switch (msg.what) { case DO_CREATE_SESSION: { - ITvInputSessionCallback cb = (ITvInputSessionCallback) msg.obj; + SomeArgs args = (SomeArgs) msg.obj; + InputChannel channel = (InputChannel) args.arg1; + ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2; try { TvInputSessionImpl sessionImpl = onCreateSession(); if (sessionImpl == null) { // Failed to create a session. cb.onSessionCreated(null); - return; + } else { + ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, + sessionImpl, channel); + cb.onSessionCreated(stub); } - ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, - sessionImpl); - cb.onSessionCreated(stub); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated"); } + args.recycle(); return; } case DO_BROADCAST_AVAILABILITY_CHANGE: { diff --git a/core/java/android/tv/TvView.java b/core/java/android/tv/TvView.java index 325950d024fb0..289823b4a2cd4 100644 --- a/core/java/android/tv/TvView.java +++ b/core/java/android/tv/TvView.java @@ -20,20 +20,24 @@ import android.content.ComponentName; import android.content.Context; import android.graphics.Rect; import android.os.Handler; -import android.tv.TvInputManager; import android.tv.TvInputManager.Session; +import android.tv.TvInputManager.Session.FinishedInputEventCallback; import android.tv.TvInputManager.SessionCreateCallback; import android.util.AttributeSet; import android.util.Log; +import android.view.InputEvent; +import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; -import android.view.ViewTreeObserver; /** * View playing TV */ public class TvView extends SurfaceView { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; private static final String TAG = "TvView"; private final Handler mHandler = new Handler(); @@ -41,11 +45,11 @@ public class TvView extends SurfaceView { private Surface mSurface; private boolean mOverlayViewCreated; private Rect mOverlayViewFrame; - private boolean mGlobalListenersAdded; - private TvInputManager mTvInputManager; + private final TvInputManager mTvInputManager; private SessionCreateCallback mSessionCreateCallback; + private OnUnhandledInputEventListener mOnUnhandledInputEventListener; - private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width @@ -70,6 +74,25 @@ public class TvView extends SurfaceView { } }; + private final FinishedInputEventCallback mFinishedInputEventCallback = + new FinishedInputEventCallback() { + @Override + public void onFinishedInputEvent(Object token, boolean handled) { + if (DEBUG) { + Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")"); + } + if (handled) { + return; + } + // TODO: Re-order unhandled events. + InputEvent event = (InputEvent) token; + if (dispatchUnhandledInputEvent(event)) { + return; + } + getViewRootImpl().dispatchUnhandledInputEvent(event); + } + }; + public TvView(Context context) { this(context, null, 0); } @@ -124,6 +147,98 @@ public class TvView extends SurfaceView { } } + /** + * Dispatches an unhandled input event to the next receiver. + *

+ * Except system keys, TvView always consumes input events in the normal flow. This is called + * asynchronously from where the event is dispatched. It gives the host application a chance to + * dispatch the unhandled input events. + * + * @param event The input event. + * @return {@code true} if the event was handled by the view, {@code false} otherwise. + */ + public boolean dispatchUnhandledInputEvent(InputEvent event) { + if (mOnUnhandledInputEventListener != null) { + if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { + return true; + } + } + return onUnhandledInputEvent(event); + } + + /** + * Called when an unhandled input event was also not handled by the user provided callback. This + * is the last chance to handle the unhandled input event in the TvView. + * + * @param event The input event. + * @return If you handled the event, return {@code true}. If you want to allow the event to be + * handled by the next receiver, return {@code false}. + */ + public boolean onUnhandledInputEvent(InputEvent event) { + return false; + } + + /** + * Registers a callback to be invoked when an input event was not handled by the bound TV input. + * + * @param listener The callback to invoke when the unhandled input event was received. + */ + public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { + mOnUnhandledInputEventListener = listener; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (super.dispatchKeyEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (super.dispatchTouchEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + if (super.dispatchTrackballEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (super.dispatchGenericMotionEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -196,6 +311,23 @@ public class TvView extends SurfaceView { location[0] + getWidth(), location[1] + getHeight()); } + /** + * Interface definition for a callback to be invoked when the unhandled input event is received. + */ + public interface OnUnhandledInputEventListener { + /** + * Called when an input event was not handled by the bound TV input. + *

+ * This is called asynchronously from where the event is dispatched. It gives the host + * application a chance to handle the unhandled input events. + * + * @param event The input event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + boolean onUnhandledInputEvent(InputEvent event); + } + private class MySessionCreateCallback implements SessionCreateCallback { final SessionCreateCallback mExternalCallback; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 246905d6995c5..020086f1c179f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -3204,8 +3204,11 @@ public final class ViewRootImpl implements ViewParent, doDie(); break; case MSG_DISPATCH_INPUT_EVENT: { - InputEvent event = (InputEvent)msg.obj; - enqueueInputEvent(event, null, 0, true); + SomeArgs args = (SomeArgs)msg.obj; + InputEvent event = (InputEvent)args.arg1; + InputEventReceiver receiver = (InputEventReceiver)args.arg2; + enqueueInputEvent(event, receiver, 0, true); + args.recycle(); } break; case MSG_DISPATCH_KEY_FROM_IME: { if (LOCAL_LOGV) Log.v( @@ -5787,7 +5790,14 @@ public final class ViewRootImpl implements ViewParent, } public void dispatchInputEvent(InputEvent event) { - Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, event); + dispatchInputEvent(event, null); + } + + public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = event; + args.arg2 = receiver; + Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args); msg.setAsynchronous(true); mHandler.sendMessage(msg); } diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 5ceb992fe463c..649f9dc48a719 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -49,9 +49,9 @@ import android.tv.ITvInputSession; import android.tv.ITvInputSessionCallback; import android.tv.TvInputInfo; import android.tv.TvInputService; -import android.util.Log; import android.util.Slog; import android.util.SparseArray; +import android.view.InputChannel; import android.view.Surface; import com.android.internal.content.PackageMonitor; @@ -292,6 +292,9 @@ public final class TvInputManagerService extends SystemService { Slog.d(TAG, "createSessionInternalLocked(name=" + sessionState.name.getClassName() + ")"); } + + final InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString()); + // Set up a callback to send the session token. ITvInputSessionCallback callback = new ITvInputSessionCallback.Stub() { @Override @@ -304,30 +307,32 @@ public final class TvInputManagerService extends SystemService { if (session == null) { removeSessionStateLocked(sessionToken, userId); sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null, - sessionState.seq, userId); + null, sessionState.seq, userId); } else { sendSessionTokenToClientLocked(sessionState.client, sessionState.name, - sessionToken, sessionState.seq, userId); + sessionToken, channels[0], sessionState.seq, userId); } + channels[0].dispose(); } } }; // Create a session. When failed, send a null token immediately. try { - service.createSession(callback); + service.createSession(channels[1], callback); } catch (RemoteException e) { Slog.e(TAG, "error in createSession", e); removeSessionStateLocked(sessionToken, userId); - sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null, + sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null, null, sessionState.seq, userId); } + channels[1].dispose(); } private void sendSessionTokenToClientLocked(ITvInputClient client, ComponentName name, - IBinder sessionToken, int seq, int userId) { + IBinder sessionToken, InputChannel channel, int seq, int userId) { try { - client.onSessionCreated(name, sessionToken, seq); + client.onSessionCreated(name, sessionToken, channel, seq); } catch (RemoteException exception) { Slog.e(TAG, "error in onSessionCreated", exception); } @@ -834,7 +839,7 @@ public final class TvInputManagerService extends SystemService { return; } default: { - Log.w(TAG, "Unhandled message code: " + msg.what); + Slog.w(TAG, "Unhandled message code: " + msg.what); return; } }