summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJae Seo <jaeseo@google.com>2014-04-29 06:22:00 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2014-04-29 06:22:00 +0000
commit19db0f036aa889ff05220b630262461e333faa85 (patch)
treee82c19d049c8e8e6aa3890ba517a65555c16cf88
parenta77a88e4364750eb9ec4dc9f92670a60f0709905 (diff)
parent6a6059a29edf31e65541b3d8927a46f5846fb0a2 (diff)
downloadframeworks_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.txt16
-rw-r--r--core/java/android/tv/ITvInputClient.aidl3
-rw-r--r--core/java/android/tv/ITvInputService.aidl3
-rw-r--r--core/java/android/tv/ITvInputSessionWrapper.java67
-rw-r--r--core/java/android/tv/TvInputManager.java266
-rw-r--r--core/java/android/tv/TvInputService.java205
-rw-r--r--core/java/android/tv/TvView.java142
-rw-r--r--core/java/android/view/ViewRootImpl.java16
-rw-r--r--services/core/java/com/android/server/tv/TvInputManagerService.java21
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;
}
}