diff options
author | Jae Seo <jaeseo@google.com> | 2014-04-29 06:22:00 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2014-04-29 06:22:00 +0000 |
commit | 19db0f036aa889ff05220b630262461e333faa85 (patch) | |
tree | e82c19d049c8e8e6aa3890ba517a65555c16cf88 | |
parent | a77a88e4364750eb9ec4dc9f92670a60f0709905 (diff) | |
parent | 6a6059a29edf31e65541b3d8927a46f5846fb0a2 (diff) | |
download | frameworks_base-19db0f036aa889ff05220b630262461e333faa85.zip frameworks_base-19db0f036aa889ff05220b630262461e333faa85.tar.gz frameworks_base-19db0f036aa889ff05220b630262461e333faa85.tar.bz2 |
Merge "Dispatch input events to the TV input"
-rw-r--r-- | api/current.txt | 16 | ||||
-rw-r--r-- | core/java/android/tv/ITvInputClient.aidl | 3 | ||||
-rw-r--r-- | core/java/android/tv/ITvInputService.aidl | 3 | ||||
-rw-r--r-- | core/java/android/tv/ITvInputSessionWrapper.java | 67 | ||||
-rw-r--r-- | core/java/android/tv/TvInputManager.java | 266 | ||||
-rw-r--r-- | core/java/android/tv/TvInputService.java | 205 | ||||
-rw-r--r-- | core/java/android/tv/TvView.java | 142 | ||||
-rw-r--r-- | core/java/android/view/ViewRootImpl.java | 16 | ||||
-rw-r--r-- | services/core/java/com/android/server/tv/TvInputManagerService.java | 21 |
9 files changed, 691 insertions, 48 deletions
diff --git a/api/current.txt b/api/current.txt index c8a59b8..1c8698a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -28163,12 +28163,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); } @@ -28178,9 +28185,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 43be6f0..538f8a1 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 672784f..4f1bc2b 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 a6e0877..3ccccf3 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 05f0b9c..7b9b1fb 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<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(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 80eb407..636e3b4 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 @@ -203,6 +226,121 @@ public abstract class TvInputService extends Service { } /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent) + * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event). + * <p> + * 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). + * <p> + * 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). + * <p> + * 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). + * <p> + * 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 325950d..289823b 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. + * <p> + * 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. + * <p> + * 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 5a48a9a..14e422c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -3212,8 +3212,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( @@ -5795,7 +5798,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 5ceb992..649f9dc 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; } } |