Merge "TIAF: handle Input Channel"

This commit is contained in:
Shubang Lu
2021-11-24 11:08:50 +00:00
committed by Android (Google) Code Review
5 changed files with 420 additions and 28 deletions

View File

@@ -16,13 +16,15 @@
package android.media.tv.interactive;
import android.view.InputChannel;
/**
* Interface a client of the ITvIAppManager implements, to identify itself and receive information
* about changes to the state of each TV interactive application service.
* @hide
*/
oneway interface ITvIAppClient {
void onSessionCreated(in String iAppServiceId, IBinder token, int seq);
void onSessionCreated(in String iAppServiceId, IBinder token, in InputChannel channel, int seq);
void onSessionReleased(int seq);
void onLayoutSurface(int left, int top, int right, int bottom, int seq);
}

View File

@@ -18,6 +18,7 @@ package android.media.tv.interactive;
import android.media.tv.interactive.ITvIAppServiceCallback;
import android.media.tv.interactive.ITvIAppSessionCallback;
import android.view.InputChannel;
/**
* Top-level interface to a TV IApp component (implemented in a Service). It's used for
@@ -27,5 +28,6 @@ import android.media.tv.interactive.ITvIAppSessionCallback;
oneway interface ITvIAppService {
void registerCallback(in ITvIAppServiceCallback callback);
void unregisterCallback(in ITvIAppServiceCallback callback);
void createSession(in ITvIAppSessionCallback callback, in String iAppServiceId, int type);
void createSession(in InputChannel channel, in ITvIAppSessionCallback callback,
in String iAppServiceId, int type);
}

View File

@@ -23,9 +23,15 @@ import android.content.Context;
import android.media.tv.TvInputManager;
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;
import android.util.SparseArray;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.InputEventSender;
import android.view.Surface;
import com.android.internal.util.Preconditions;
@@ -67,8 +73,8 @@ public final class TvIAppManager {
mUserId = userId;
mClient = new ITvIAppClient.Stub() {
@Override
public void onSessionCreated(String iAppServiceId, IBinder token, int seq) {
// TODO: use InputChannel for input events
public void onSessionCreated(String iAppServiceId, IBinder token, InputChannel channel,
int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
@@ -77,7 +83,7 @@ public final class TvIAppManager {
}
Session session = null;
if (token != null) {
session = new Session(token, mService, mUserId, seq,
session = new Session(token, channel, mService, mUserId, seq,
mSessionCallbackRecordMap);
} else {
mSessionCallbackRecordMap.delete(seq);
@@ -351,18 +357,33 @@ public final class TvIAppManager {
* @hide
*/
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 ITvIAppManager mService;
private final int mUserId;
private final int mSeq;
private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
private IBinder mToken;
// 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 TvInputManager.Session mInputSession;
private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20);
private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20);
private Session(IBinder token, ITvIAppManager service, int userId, int seq,
SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
private IBinder mToken;
private TvInputEventSender mSender;
private InputChannel mInputChannel;
private Session(IBinder token, InputChannel channel, ITvIAppManager service, int userId,
int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
mToken = token;
mInputChannel = channel;
mService = service;
mUserId = userId;
mSeq = seq;
@@ -427,6 +448,43 @@ public final class TvIAppManager {
}
}
/**
* Dispatches an input event to this session.
*
* @param event An {@link InputEvent} to dispatch. Cannot be {@code null}.
* @param token A token used to identify the input event later in the callback.
* @param callback A callback used to receive the dispatch result. Cannot be {@code null}.
* @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be
* {@code null}.
* @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.
* @hide
*/
public int dispatchInputEvent(@NonNull InputEvent event, Object token,
@NonNull FinishedInputEventCallback callback, @NonNull Handler handler) {
Preconditions.checkNotNull(event);
Preconditions.checkNotNull(callback);
Preconditions.checkNotNull(handler);
synchronized (mHandler) {
if (mInputChannel == 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;
}
}
/**
* Releases this session.
*/
@@ -444,12 +502,208 @@ public final class TvIAppManager {
releaseInternal();
}
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 void releaseInternal() {
mToken = null;
synchronized (mHandler) {
if (mInputChannel != null) {
if (mSender != null) {
flushPendingEventsLocked();
mSender.dispose();
mSender = null;
}
mInputChannel.dispose();
mInputChannel = null;
}
}
synchronized (mSessionCallbackRecordMap) {
mSessionCallbackRecordMap.delete(mSeq);
}
}
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.mEventToken = token;
p.mCallback = callback;
p.mEventHandler = handler;
return p;
}
// Assumes the event has already been removed from the queue.
void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
p.mHandled = handled;
if (p.mEventHandler.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.mEventHandler, p);
msg.setAsynchronous(true);
msg.sendToTarget();
}
}
// 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 (mInputChannel != null) {
if (mSender == null) {
mSender = new TvInputEventSender(mInputChannel, 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 session to handle input event after "
+ INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
} else {
mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
}
}
invokeFinishedInputEventCallback(p, handled);
}
private void recyclePendingEventLocked(PendingEvent p) {
p.recycle();
mPendingEventPool.release(p);
}
/**
* 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.
*/
void onFinishedInputEvent(Object token, boolean handled);
}
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 {
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 mEventToken;
public FinishedInputEventCallback mCallback;
public Handler mEventHandler;
public boolean mHandled;
public void recycle() {
mEvent = null;
mEventToken = null;
mCallback = null;
mEventHandler = null;
mHandled = false;
}
@Override
public void run() {
mCallback.onFinishedInputEvent(mEventToken, mHandled);
synchronized (mEventHandler) {
recyclePendingEventLocked(this);
}
}
}
}
private static final class SessionCallbackRecord {

View File

@@ -25,11 +25,17 @@ import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
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 com.android.internal.os.SomeArgs;
@@ -85,14 +91,16 @@ public abstract class TvIAppService extends Service {
}
@Override
public void createSession(ITvIAppSessionCallback cb, String iAppServiceId, int type) {
public void createSession(InputChannel channel, ITvIAppSessionCallback cb,
String iAppServiceId, int type) {
if (cb == null) {
return;
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = cb;
args.arg2 = iAppServiceId;
args.arg3 = type;
args.arg1 = channel;
args.arg2 = cb;
args.arg3 = iAppServiceId;
args.arg4 = type;
mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args)
.sendToTarget();
}
@@ -122,6 +130,8 @@ public abstract class TvIAppService extends Service {
* @hide
*/
public abstract static class Session implements KeyEvent.Callback {
private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
private final Object mLock = new Object();
// @GuardedBy("mLock")
private ITvIAppSessionCallback mSessionCallback;
@@ -181,6 +191,60 @@ public abstract class TvIAppService extends Service {
public void onRelease() {
}
/**
* TODO: JavaDoc of APIs related to input events.
* @hide
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return false;
}
/**
* @hide
*/
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return false;
}
/**
* @hide
*/
@Override
public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
return false;
}
/**
* @hide
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return false;
}
/**
* @hide
*/
public boolean onTouchEvent(MotionEvent event) {
return false;
}
/**
* @hide
*/
public boolean onTrackballEvent(MotionEvent event) {
return false;
}
/**
* @hide
*/
public boolean onGenericMotionEvent(MotionEvent event) {
return false;
}
/**
* Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
* is relative to the overlay view that sits on top of this surface.
@@ -226,6 +290,39 @@ public abstract class TvIAppService extends Service {
}
}
/**
* 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) {
KeyEvent keyEvent = (KeyEvent) event;
if (keyEvent.dispatch(this, mDispatcherState, this)) {
return TvIAppManager.Session.DISPATCH_HANDLED;
}
// TODO: special handlings of navigation keys and media keys
} else if (event instanceof MotionEvent) {
MotionEvent motionEvent = (MotionEvent) event;
final int source = motionEvent.getSource();
if (motionEvent.isTouchEvent()) {
if (onTouchEvent(motionEvent)) {
return TvIAppManager.Session.DISPATCH_HANDLED;
}
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
if (onTrackballEvent(motionEvent)) {
return TvIAppManager.Session.DISPATCH_HANDLED;
}
} else {
if (onGenericMotionEvent(motionEvent)) {
return TvIAppManager.Session.DISPATCH_HANDLED;
}
}
}
// TODO: handle overlay view
return TvIAppManager.Session.DISPATCH_NOT_HANDLED;
}
private void initialize(ITvIAppSessionCallback callback) {
synchronized (mLock) {
mSessionCallback = callback;
@@ -281,10 +378,17 @@ public abstract class TvIAppService extends Service {
* @hide
*/
public static class ITvIAppSessionWrapper extends ITvIAppSession.Stub {
// TODO: put ITvIAppSessionWrapper in a separate Java file
private final Session mSessionImpl;
private InputChannel mChannel;
private TvIAppEventReceiver mReceiver;
public ITvIAppSessionWrapper(Session mSessionImpl) {
public ITvIAppSessionWrapper(Context context, Session mSessionImpl, InputChannel channel) {
this.mSessionImpl = mSessionImpl;
mChannel = channel;
if (channel != null) {
mReceiver = new TvIAppEventReceiver(channel, context.getMainLooper());
}
}
@Override
@@ -306,6 +410,26 @@ public abstract class TvIAppService extends Service {
public void dispatchSurfaceChanged(int format, int width, int height) {
mSessionImpl.dispatchSurfaceChanged(format, width, height);
}
private final class TvIAppEventReceiver extends InputEventReceiver {
TvIAppEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEvent(InputEvent event) {
if (mSessionImpl == null) {
// The session has been finished.
finishInputEvent(event, false);
return;
}
int handled = mSessionImpl.dispatchInputEvent(event, this);
if (handled != TvIAppManager.Session.DISPATCH_IN_PROGRESS) {
finishInputEvent(event, handled == TvIAppManager.Session.DISPATCH_HANDLED);
}
}
}
}
@SuppressLint("HandlerLeak")
@@ -318,9 +442,10 @@ public abstract class TvIAppService extends Service {
switch (msg.what) {
case DO_CREATE_SESSION: {
SomeArgs args = (SomeArgs) msg.obj;
ITvIAppSessionCallback cb = (ITvIAppSessionCallback) args.arg1;
String iAppServiceId = (String) args.arg2;
int type = (int) args.arg3;
InputChannel channel = (InputChannel) args.arg1;
ITvIAppSessionCallback cb = (ITvIAppSessionCallback) args.arg2;
String iAppServiceId = (String) args.arg3;
int type = (int) args.arg4;
args.recycle();
Session sessionImpl = onCreateSession(iAppServiceId, type);
if (sessionImpl == null) {
@@ -332,7 +457,8 @@ public abstract class TvIAppService extends Service {
}
return;
}
ITvIAppSession stub = new ITvIAppSessionWrapper(sessionImpl);
ITvIAppSession stub = new ITvIAppSessionWrapper(
TvIAppService.this, sessionImpl, channel);
SomeArgs someArgs = SomeArgs.obtain();
someArgs.arg1 = sessionImpl;

View File

@@ -47,6 +47,7 @@ import android.os.UserManager;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import android.view.InputChannel;
import android.view.Surface;
import com.android.internal.annotations.GuardedBy;
@@ -611,14 +612,14 @@ public class TvIAppManagerService extends SystemService {
if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) {
// Only current user and its running profiles can create sessions.
// Let the client get onConnectionFailed callback for this case.
sendSessionTokenToClientLocked(client, iAppServiceId, null, seq);
sendSessionTokenToClientLocked(client, iAppServiceId, null, null, seq);
return;
}
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
TvIAppState iAppState = userState.mIAppMap.get(iAppServiceId);
if (iAppState == null) {
Slogf.w(TAG, "Failed to find state for iAppServiceId=" + iAppServiceId);
sendSessionTokenToClientLocked(client, iAppServiceId, null, seq);
sendSessionTokenToClientLocked(client, iAppServiceId, null, null, seq);
return;
}
ServiceState serviceState =
@@ -631,7 +632,7 @@ public class TvIAppManagerService extends SystemService {
}
// Send a null token immediately while reconnecting.
if (serviceState.mReconnecting) {
sendSessionTokenToClientLocked(client, iAppServiceId, null, seq);
sendSessionTokenToClientLocked(client, iAppServiceId, null, null, seq);
return;
}
@@ -780,9 +781,9 @@ public class TvIAppManagerService extends SystemService {
@GuardedBy("mLock")
private void sendSessionTokenToClientLocked(ITvIAppClient client, String iAppServiceId,
IBinder sessionToken, int seq) {
IBinder sessionToken, InputChannel channel, int seq) {
try {
client.onSessionCreated(iAppServiceId, sessionToken, seq);
client.onSessionCreated(iAppServiceId, sessionToken, channel, seq);
} catch (RemoteException e) {
Slogf.e(TAG, "error in onSessionCreated", e);
}
@@ -797,20 +798,23 @@ public class TvIAppManagerService extends SystemService {
Slogf.d(TAG, "createSessionInternalLocked(iAppServiceId="
+ sessionState.mIAppServiceId + ")");
}
InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
// Set up a callback to send the session token.
ITvIAppSessionCallback callback = new SessionCallback(sessionState);
ITvIAppSessionCallback callback = new SessionCallback(sessionState, channels);
boolean created = true;
// Create a session. When failed, send a null token immediately.
try {
service.createSession(callback, sessionState.mIAppServiceId, sessionState.mType);
service.createSession(
channels[1], callback, sessionState.mIAppServiceId, sessionState.mType);
} catch (RemoteException e) {
Slogf.e(TAG, "error in createSession", e);
sendSessionTokenToClientLocked(sessionState.mClient, sessionState.mIAppServiceId, null,
sessionState.mSeq);
null, sessionState.mSeq);
created = false;
}
channels[1].dispose();
return created;
}
@@ -883,7 +887,7 @@ public class TvIAppManagerService extends SystemService {
for (SessionState sessionState : sessionsToAbort) {
removeSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId);
sendSessionTokenToClientLocked(sessionState.mClient,
sessionState.mIAppServiceId, null, sessionState.mSeq);
sessionState.mIAppServiceId, null, null, sessionState.mSeq);
}
updateServiceConnectionLocked(serviceState.mComponent, userId);
}
@@ -1136,9 +1140,11 @@ public class TvIAppManagerService extends SystemService {
private final class SessionCallback extends ITvIAppSessionCallback.Stub {
private final SessionState mSessionState;
private final InputChannel[] mInputChannels;
SessionCallback(SessionState sessionState) {
SessionCallback(SessionState sessionState, InputChannel[] channels) {
mSessionState = sessionState;
mInputChannels = channels;
}
@Override
@@ -1154,12 +1160,14 @@ public class TvIAppManagerService extends SystemService {
mSessionState.mClient,
mSessionState.mIAppServiceId,
mSessionState.mSessionToken,
mInputChannels[0],
mSessionState.mSeq);
} else {
removeSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId);
sendSessionTokenToClientLocked(mSessionState.mClient,
mSessionState.mIAppServiceId, null, mSessionState.mSeq);
mSessionState.mIAppServiceId, null, null, mSessionState.mSeq);
}
mInputChannels[0].dispose();
}
}