Merge "Dispatch input events to the TV input"

This commit is contained in:
Jae Seo
2014-04-29 06:22:00 +00:00
committed by Android (Google) Code Review
9 changed files with 691 additions and 48 deletions

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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).
* <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: {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}
}