diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 49a5c9f78230b..f2ea060b16946 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -568,14 +568,15 @@ public abstract class ActivityManagerInternal { public abstract void unregisterProcessObserver(IProcessObserver processObserver); /** - * Checks if there is an unfinished instrumentation that targets the given uid. + * Gets the uid of the instrumentation source if there is an unfinished instrumentation that + * targets the given uid. * * @param uid The uid to be checked for * - * @return True, if there is an instrumentation whose target application uid matches the given - * uid, false otherwise + * @return the uid of the instrumentation source, if there is an instrumentation whose target + * application uid matches the given uid, and {@link android.os.Process#INVALID_UID} otherwise. */ - public abstract boolean isUidCurrentlyInstrumented(int uid); + public abstract int getInstrumentationSourceUid(int uid); /** Is this a device owner app? */ public abstract boolean isDeviceOwner(int uid); diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 99d7c63cde42f..8984c4292023b 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1058,10 +1058,11 @@ public class Instrumentation { } /** - * Sends the key events corresponding to the text to the app being - * instrumented. - * - * @param text The text to be sent. + * Sends the key events that result in the given text being typed into the currently focused + * window, and waits for it to be processed. + * + * @param text The text to be sent. + * @see #sendKeySync(KeyEvent) */ public void sendStringSync(String text) { if (text == null) { @@ -1084,11 +1085,12 @@ public class Instrumentation { } /** - * Send a key event to the currently focused window/view and wait for it to - * be processed. Finished at some point after the recipient has returned - * from its event processing, though it may not have completely - * finished reacting from the event -- for example, if it needs to update - * its display as a result, it may still be in the process of doing that. + * Sends a key event to the currently focused window, and waits for it to be processed. + *
+ * This method blocks until the recipient has finished handling the event. Note that the + * recipient may not have completely finished reacting from the event when this method + * returns. For example, it may still be in the process of updating its display or UI contents + * upon reacting to the injected event. * * @param event The event to send to the current focus. */ @@ -1116,34 +1118,42 @@ public class Instrumentation { } /** - * Sends an up and down key event sync to the currently focused window. + * Sends up and down key events with the given key code to the currently focused window, and + * waits for it to be processed. * - * @param key The integer keycode for the event. + * @param keyCode The key code for the events to send. + * @see #sendKeySync(KeyEvent) */ - public void sendKeyDownUpSync(int key) { - sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key)); - sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key)); - } - - /** - * Higher-level method for sending both the down and up key events for a - * particular character key code. Equivalent to creating both KeyEvent - * objects by hand and calling {@link #sendKeySync}. The event appears - * as if it came from keyboard 0, the built in one. - * - * @param keyCode The key code of the character to send. - */ - public void sendCharacterSync(int keyCode) { + public void sendKeyDownUpSync(int keyCode) { sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); } - + /** - * Dispatch a pointer event. Finished at some point after the recipient has - * returned from its event processing, though it may not have - * completely finished reacting from the event -- for example, if it needs - * to update its display as a result, it may still be in the process of - * doing that. + * Sends up and down key events with the given key code to the currently focused window, and + * waits for it to be processed. + *
+ * Equivalent to {@link #sendKeyDownUpSync(int)}. + * + * @param keyCode The key code of the character to send. + * @see #sendKeySync(KeyEvent) + */ + public void sendCharacterSync(int keyCode) { + sendKeyDownUpSync(keyCode); + } + + /** + * Dispatches a pointer event into a window owned by the instrumented application, and waits for + * it to be processed. + *
+ * If the motion event being injected is targeted at a window that is not owned by the + * instrumented application, the input injection will fail. See {@link #getUiAutomation()} for + * injecting events into all windows. + *
+ * This method blocks until the recipient has finished handling the event. Note that the + * recipient may not have completely finished reacting from the event when this method + * returns. For example, it may still be in the process of updating its display or UI contents + * upon reacting to the injected event. * * @param event A motion event describing the pointer action. (As noted in * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use @@ -1155,10 +1165,10 @@ public class Instrumentation { event.setSource(InputDevice.SOURCE_TOUCHSCREEN); } - syncInputTransactionsAndInjectEvent(event); + syncInputTransactionsAndInjectEventIntoSelf(event); } - private void syncInputTransactionsAndInjectEvent(MotionEvent event) { + private void syncInputTransactionsAndInjectEventIntoSelf(MotionEvent event) { final boolean syncBefore = event.getAction() == MotionEvent.ACTION_DOWN || event.isFromSource(InputDevice.SOURCE_MOUSE); final boolean syncAfter = event.getAction() == MotionEvent.ACTION_UP; @@ -1169,8 +1179,9 @@ public class Instrumentation { .syncInputTransactions(true /*waitForAnimations*/); } + // Direct the injected event into windows owned by the instrumentation target. InputManager.getInstance().injectInputEvent( - event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); + event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, Process.myUid()); if (syncAfter) { WindowManagerGlobal.getWindowManagerService() @@ -1182,19 +1193,21 @@ public class Instrumentation { } /** - * Dispatch a trackball event. Finished at some point after the recipient has - * returned from its event processing, though it may not have - * completely finished reacting from the event -- for example, if it needs - * to update its display as a result, it may still be in the process of - * doing that. - * + * Dispatches a trackball event into the currently focused window, and waits for it to be + * processed. + *
+ * This method blocks until the recipient has finished handling the event. Note that the + * recipient may not have completely finished reacting from the event when this method + * returns. For example, it may still be in the process of updating its display or UI contents + * upon reacting to the injected event. + * * @param event A motion event describing the trackball action. (As noted in * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use * {@link SystemClock#uptimeMillis()} as the timebase. */ public void sendTrackballEventSync(MotionEvent event) { validateNotAppThread(); - if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) { + if (!event.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) { event.setSource(InputDevice.SOURCE_TRACKBALL); } InputManager.getInstance().injectInputEvent(event, diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index e1ffd4a6761df..2da12e6c5c9d8 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -57,11 +57,16 @@ interface IInputManager { // Temporarily changes the pointer speed. void tryPointerSpeed(int speed); - // Injects an input event into the system. To inject into windows owned by other - // applications, the caller must have the INJECT_EVENTS permission. + // Injects an input event into the system. The caller must have the INJECT_EVENTS permssion. + // This method exists only for compatibility purposes and may be removed in a future release. @UnsupportedAppUsage boolean injectInputEvent(in InputEvent ev, int mode); + // Injects an input event into the system. The caller must have the INJECT_EVENTS permission. + // The caller can target windows owned by a certain UID by providing a valid UID, or by + // providing {@link android.os.Process#INVALID_UID} to target all windows. + boolean injectInputEventToTarget(in InputEvent ev, int mode, int targetUid); + VerifiedInputEvent verifyInputEvent(in InputEvent ev); // Calibrate input device position diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index cc5b275bbf5ab..d17a9523ab37d 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -45,6 +45,7 @@ import android.os.IVibratorStateListener; import android.os.InputEventInjectionSync; import android.os.Looper; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -1107,14 +1108,58 @@ public final class InputManager { } } + /** + * Injects an input event into the event system, targeting windows owned by the provided uid. + * + * If a valid targetUid is provided, the system will only consider injecting the input event + * into windows owned by the provided uid. If the input event is targeted at a window that is + * not owned by the provided uid, input injection will fail and a RemoteException will be + * thrown. + * + * The synchronization mode determines whether the method blocks while waiting for + * input injection to proceed. + *
+ * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission. + *
+ * Make sure you correctly set the event time and input source of the event + * before calling this method. + *
+ * + * @param event The event to inject. + * @param mode The synchronization mode. One of: + * {@link android.os.InputEventInjectionSync.NONE}, + * {@link android.os.InputEventInjectionSync.WAIT_FOR_RESULT}, or + * {@link android.os.InputEventInjectionSync.WAIT_FOR_FINISHED}. + * @param targetUid The uid to target, or {@link android.os.Process#INVALID_UID} to target all + * windows. + * @return True if input event injection succeeded. + * + * @hide + */ + @RequiresPermission(Manifest.permission.INJECT_EVENTS) + public boolean injectInputEvent(InputEvent event, int mode, int targetUid) { + if (event == null) { + throw new IllegalArgumentException("event must not be null"); + } + if (mode != InputEventInjectionSync.NONE + && mode != InputEventInjectionSync.WAIT_FOR_FINISHED + && mode != InputEventInjectionSync.WAIT_FOR_RESULT) { + throw new IllegalArgumentException("mode is invalid"); + } + + try { + return mIm.injectInputEventToTarget(event, mode, targetUid); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } /** * Injects an input event into the event system on behalf of an application. * The synchronization mode determines whether the method blocks while waiting for * input injection to proceed. *- * Requires {@link android.Manifest.permission.INJECT_EVENTS} to inject into - * windows that are owned by other applications. + * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission. *
* Make sure you correctly set the event time and input source of the event
* before calling this method.
@@ -1129,22 +1174,10 @@ public final class InputManager {
*
* @hide
*/
+ @RequiresPermission(Manifest.permission.INJECT_EVENTS)
@UnsupportedAppUsage
public boolean injectInputEvent(InputEvent event, int mode) {
- if (event == null) {
- throw new IllegalArgumentException("event must not be null");
- }
- if (mode != InputEventInjectionSync.NONE
- && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
- && mode != InputEventInjectionSync.WAIT_FOR_RESULT) {
- throw new IllegalArgumentException("mode is invalid");
- }
-
- try {
- return mIm.injectInputEvent(event, mode);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
+ return injectInputEvent(event, mode, Process.INVALID_UID);
}
/**
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index dea8cc0152723..7e624e05405cb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -17195,17 +17195,17 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public boolean isUidCurrentlyInstrumented(int uid) {
+ public int getInstrumentationSourceUid(int uid) {
synchronized (mProcLock) {
for (int i = mActiveInstrumentation.size() - 1; i >= 0; i--) {
ActiveInstrumentation activeInst = mActiveInstrumentation.get(i);
if (!activeInst.mFinished && activeInst.mTargetInfo != null
&& activeInst.mTargetInfo.uid == uid) {
- return true;
+ return activeInst.mSourceUid;
}
}
}
- return false;
+ return INVALID_UID;
}
@Override
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 36afb36774380..8cef0b0130f64 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2368,7 +2368,8 @@ public class AppOpsService extends IAppOpsService.Stub {
ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid();
if (!isSelfRequest) {
- boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid());
+ boolean isCallerInstrumented =
+ ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
boolean isCallerPermissionController;
try {
@@ -6924,7 +6925,8 @@ public class AppOpsService extends IAppOpsService.Stub {
@Override
public @Nullable RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage() {
ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
- boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid());
+ boolean isCallerInstrumented =
+ ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
if (!isCallerSystem && !isCallerInstrumented) {
return null;
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index e4e9d1d49a6e9..b624d438a4f75 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -20,6 +20,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManagerInternal;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -860,6 +861,19 @@ public class InputManagerService extends IInputManager.Stub
@Override // Binder call
public boolean injectInputEvent(InputEvent event, int mode) {
+ return injectInputEventToTarget(event, mode, Process.INVALID_UID);
+ }
+
+ @Override // Binder call
+ public boolean injectInputEventToTarget(InputEvent event, int mode, int targetUid) {
+ if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+ "injectInputEvent()", true /*checkInstrumentationSource*/)) {
+ throw new SecurityException(
+ "Injecting input events requires the caller (or the source of the "
+ + "instrumentation, if any) to have the INJECT_EVENTS permission.");
+ }
+ // We are not checking if targetUid matches the callingUid, since having the permission
+ // already means you can inject into any window.
Objects.requireNonNull(event, "event must not be null");
if (mode != InputEventInjectionSync.NONE
&& mode != InputEventInjectionSync.WAIT_FOR_FINISHED
@@ -868,22 +882,39 @@ public class InputManagerService extends IInputManager.Stub
}
final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
+ final boolean injectIntoUid = targetUid != Process.INVALID_UID;
final int result;
try {
- result = mNative.injectInputEvent(event, pid, uid, mode,
- INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
+ result = mNative.injectInputEvent(event, injectIntoUid,
+ targetUid, mode, INJECTION_TIMEOUT_MILLIS,
+ WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
} finally {
Binder.restoreCallingIdentity(ident);
}
switch (result) {
- case InputEventInjectionResult.PERMISSION_DENIED:
- Slog.w(TAG, "Input event injection from pid " + pid + " permission denied.");
- throw new SecurityException(
- "Injecting to another application requires INJECT_EVENTS permission");
case InputEventInjectionResult.SUCCEEDED:
return true;
+ case InputEventInjectionResult.TARGET_MISMATCH:
+ if (!injectIntoUid) {
+ throw new IllegalStateException("Injection should not result in TARGET_MISMATCH"
+ + " when it is not targeted into to a specific uid.");
+ }
+ // TODO(b/228161340): Remove the fallback of targeting injection into all windows
+ // when the caller has the injection permission.
+ // Explicitly maintain the same behavior as previous versions of Android, where
+ // injection is allowed into all windows if the caller has the INJECT_EVENTS
+ // permission, even if it is targeting a certain uid.
+ if (checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+ "injectInputEvent-target-mismatch-fallback")) {
+ Slog.w(TAG, "Targeted input event was not directed at a window owned by uid "
+ + targetUid + ". Falling back to injecting into all windows.");
+ return injectInputEventToTarget(event, mode, Process.INVALID_UID);
+ }
+ throw new IllegalArgumentException(
+ "Targeted input event injection from pid " + pid
+ + " was not directed at a window owned by uid "
+ + targetUid + ".");
case InputEventInjectionResult.TIMED_OUT:
Slog.w(TAG, "Input event injection from pid " + pid + " timed out.");
return false;
@@ -2780,8 +2811,12 @@ public class InputManagerService extends IInputManager.Stub
}
}
}
-
private boolean checkCallingPermission(String permission, String func) {
+ return checkCallingPermission(permission, func, false /*checkInstrumentationSource*/);
+ }
+
+ private boolean checkCallingPermission(String permission, String func,
+ boolean checkInstrumentationSource) {
// Quick check: if the calling permission is me, it's all okay.
if (Binder.getCallingPid() == Process.myPid()) {
return true;
@@ -2790,6 +2825,28 @@ public class InputManagerService extends IInputManager.Stub
if (mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
return true;
}
+
+ if (checkInstrumentationSource) {
+ final ActivityManagerInternal ami =
+ LocalServices.getService(ActivityManagerInternal.class);
+ Objects.requireNonNull(ami, "ActivityManagerInternal should not be null.");
+ final int instrumentationUid = ami.getInstrumentationSourceUid(Binder.getCallingUid());
+ if (instrumentationUid != Process.INVALID_UID) {
+ // Clear the calling identity when checking if the instrumentation source has
+ // permission because PackageManager will deny all permissions to some callers,
+ // such as instant apps.
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (mContext.checkPermission(permission, -1 /*pid*/, instrumentationUid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
String msg = "Permission Denial: " + func + " from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid()
@@ -3033,13 +3090,6 @@ public class InputManagerService extends IInputManager.Stub
return mWindowManagerCallbacks.dispatchUnhandledKey(focus, event, policyFlags);
}
- // Native callback.
- @SuppressWarnings("unused")
- private boolean checkInjectEventsPermission(int injectorPid, int injectorUid) {
- return mContext.checkPermission(android.Manifest.permission.INJECT_EVENTS,
- injectorPid, injectorUid) == PackageManager.PERMISSION_GRANTED;
- }
-
// Native callback.
@SuppressWarnings("unused")
private void onPointerDownOutsideFocus(IBinder touchedToken) {
@@ -3501,12 +3551,17 @@ public class InputManagerService extends IInputManager.Stub
@Override
public void sendInputEvent(InputEvent event, int policyFlags) {
+ if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+ "sendInputEvent()")) {
+ throw new SecurityException(
+ "The INJECT_EVENTS permission is required for injecting input events.");
+ }
Objects.requireNonNull(event, "event must not be null");
synchronized (mInputFilterLock) {
if (!mDisconnected) {
- mNative.injectInputEvent(event, 0, 0,
- InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0,
+ mNative.injectInputEvent(event, false /* injectIntoUid */, -1 /* uid */,
+ InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0 /* timeout */,
policyFlags | WindowManagerPolicy.FLAG_FILTERED);
}
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 81882d277a992..9cf80737e67d3 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -70,7 +70,18 @@ public interface NativeInputManagerService {
void setBlockUntrustedTouchesMode(int mode);
- int injectInputEvent(InputEvent event, int pid, int uid, int syncMode,
+ /**
+ * Inject an input event into the system.
+ *
+ * @param event the input event to inject
+ * @param injectIntoUid true if the event should target windows owned by uid, false otherwise
+ * @param uid the uid whose windows should be targeted, if any
+ * @param syncMode {@link android.os.InputEventInjectionSync}
+ * @param timeoutMillis timeout to wait for input injection to complete, in milliseconds
+ * @param policyFlags defined in {@link android.view.WindowManagerPolicyConstants}
+ * @return {@link android.os.InputEventInjectionResult}
+ */
+ int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid, int syncMode,
int timeoutMillis, int policyFlags);
VerifiedInputEvent verifyInputEvent(InputEvent event);
@@ -240,7 +251,8 @@ public interface NativeInputManagerService {
public native void setBlockUntrustedTouchesMode(int mode);
@Override
- public native int injectInputEvent(InputEvent event, int pid, int uid, int syncMode,
+ public native int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid,
+ int syncMode,
int timeoutMillis, int policyFlags);
@Override
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index 21beb964529a0..1bcc21e66302b 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -410,7 +410,8 @@ public final class LogcatManagerService extends SystemService {
}
private void processNewLogAccessRequest(LogAccessClient client) {
- boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid);
+ boolean isInstrumented = mActivityManagerInternal.getInstrumentationSourceUid(client.mUid)
+ != android.os.Process.INVALID_UID;
// The instrumented apks only run for testing, so we don't check user permission.
if (isInstrumented) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 32adac7f282b0..287fb82196508 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -106,7 +106,6 @@ static struct {
jmethodID interceptMotionBeforeQueueingNonInteractive;
jmethodID interceptKeyBeforeDispatching;
jmethodID dispatchUnhandledKey;
- jmethodID checkInjectEventsPermission;
jmethodID onPointerDisplayIdChanged;
jmethodID onPointerDownOutsideFocus;
jmethodID getVirtualKeyQuietTimeMillis;
@@ -333,7 +332,6 @@ public:
bool dispatchUnhandledKey(const sp