Show ANR dialog for unresponsive gesture monitors
If a gesture monitor is unresponsive, today there will not be an ANR
dialog. Since the input channel token for the gesture monitor is not
provided to WindowManager, there is no way for the proper ANR to occur.
That means, the user will not know that the gesture monitor isn't
working.
To fix the issue, we record the pid of the caller when first registering
a gesture monitor. This will be stored in InputManagerService.
Next, when an ANR for this gesture monitor occurs, we will provide this
pid to the WM.
WM will use this pid to properly blame that process.
Bug: 161904619
Bug: 160903019
Test: atest AnrTest
Test: adb shell input dump
Change-Id: Ie1a16352a116914ba6550958ad41de07cff063be
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 74ed815..5a423793 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.input;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -66,6 +67,7 @@
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -181,8 +183,7 @@
// State for vibrator tokens.
private Object mVibratorLock = new Object();
- private HashMap<IBinder, VibratorToken> mVibratorTokens =
- new HashMap<IBinder, VibratorToken>();
+ private Map<IBinder, VibratorToken> mVibratorTokens = new ArrayMap<IBinder, VibratorToken>();
private int mNextVibratorTokenValue;
// State for the currently installed input filter.
@@ -190,12 +191,16 @@
IInputFilter mInputFilter; // guarded by mInputFilterLock
InputFilterHost mInputFilterHost; // guarded by mInputFilterLock
+ private final Object mGestureMonitorPidsLock = new Object();
+ @GuardedBy("mGestureMonitorPidsLock")
+ private final ArrayMap<IBinder, Integer> mGestureMonitorPidsByToken = new ArrayMap<>();
+
// The associations of input devices to displays by port. Maps from input device port (String)
// to display id (int). Currently only accessed by InputReader.
private final Map<String, Integer> mStaticAssociations;
private final Object mAssociationsLock = new Object();
@GuardedBy("mAssociationLock")
- private final Map<String, Integer> mRuntimeAssociations = new HashMap<String, Integer>();
+ private final Map<String, Integer> mRuntimeAssociations = new ArrayMap<String, Integer>();
private static native long nativeInit(InputManagerService service,
Context context, MessageQueue messageQueue);
@@ -540,13 +545,17 @@
if (displayId < Display.DEFAULT_DISPLAY) {
throw new IllegalArgumentException("displayId must >= 0.");
}
+ final int pid = Binder.getCallingPid();
final long ident = Binder.clearCallingIdentity();
try {
InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
InputMonitorHost host = new InputMonitorHost(inputChannels[0]);
- nativeRegisterInputMonitor(mPtr, inputChannels[0], displayId,
- true /*isGestureMonitor*/);
+ nativeRegisterInputMonitor(
+ mPtr, inputChannels[0], displayId, true /*isGestureMonitor*/);
+ synchronized (mGestureMonitorPidsLock) {
+ mGestureMonitorPidsByToken.put(inputChannels[1].getToken(), pid);
+ }
return new InputMonitor(inputChannels[1], host);
} finally {
Binder.restoreCallingIdentity(ident);
@@ -575,6 +584,9 @@
if (inputChannel == null) {
throw new IllegalArgumentException("inputChannel must not be null.");
}
+ synchronized (mGestureMonitorPidsLock) {
+ mGestureMonitorPidsByToken.remove(inputChannel.getToken());
+ }
nativeUnregisterInputChannel(mPtr, inputChannel);
}
@@ -1838,6 +1850,7 @@
if (dumpStr != null) {
pw.println(dumpStr);
dumpAssociations(pw);
+ dumpGestureMonitorPidsByToken(pw);
}
}
@@ -1861,6 +1874,19 @@
}
}
+ private void dumpGestureMonitorPidsByToken(PrintWriter pw) {
+ synchronized (mGestureMonitorPidsLock) {
+ if (!mGestureMonitorPidsByToken.isEmpty()) {
+ pw.println("Gesture monitor pids by token:");
+ for (int i = 0; i < mGestureMonitorPidsByToken.size(); i++) {
+ pw.print(" " + i + ": ");
+ pw.print(" token: " + mGestureMonitorPidsByToken.keyAt(i));
+ pw.println(" pid: " + mGestureMonitorPidsByToken.valueAt(i));
+ }
+ }
+ }
+ }
+
private boolean checkCallingPermission(String permission, String func) {
// Quick check: if the calling permission is me, it's all okay.
if (Binder.getCallingPid() == Process.myPid()) {
@@ -1883,6 +1909,7 @@
public void monitor() {
synchronized (mInputFilterLock) { }
synchronized (mAssociationsLock) { /* Test if blocked by associations lock. */}
+ synchronized (mGestureMonitorPidsLock) { /* Test if blocked by gesture monitor pids lock */}
nativeMonitor(mPtr);
}
@@ -1944,6 +1971,9 @@
// Native callback.
private void notifyInputChannelBroken(IBinder token) {
+ synchronized (mGestureMonitorPidsLock) {
+ mGestureMonitorPidsByToken.remove(token);
+ }
mWindowManagerCallbacks.notifyInputChannelBroken(token);
}
@@ -1959,8 +1989,12 @@
// Native callback.
private long notifyANR(InputApplicationHandle inputApplicationHandle, IBinder token,
String reason) {
- return mWindowManagerCallbacks.notifyANR(inputApplicationHandle,
- token, reason);
+ Integer gestureMonitorPid;
+ synchronized (mGestureMonitorPidsLock) {
+ gestureMonitorPid = mGestureMonitorPidsByToken.get(token);
+ }
+ return mWindowManagerCallbacks.notifyANR(inputApplicationHandle, token, gestureMonitorPid,
+ reason);
}
// Native callback.
@@ -2206,22 +2240,48 @@
* Callback interface implemented by the Window Manager.
*/
public interface WindowManagerCallbacks {
+ /**
+ * This callback is invoked when the confuguration changes.
+ */
public void notifyConfigurationChanged();
+ /**
+ * This callback is invoked when the lid switch changes state.
+ * @param whenNanos the time when the change occurred
+ * @param lidOpen true if the lid is open
+ */
public void notifyLidSwitchChanged(long whenNanos, boolean lidOpen);
+ /**
+ * This callback is invoked when the camera lens cover switch changes state.
+ * @param whenNanos the time when the change occurred
+ * @param lensCovered true is the lens is covered
+ */
public void notifyCameraLensCoverSwitchChanged(long whenNanos, boolean lensCovered);
+ /**
+ * This callback is invoked when an input channel is closed unexpectedly.
+ * @param token the connection token of the broken channel
+ */
public void notifyInputChannelBroken(IBinder token);
/**
- * Notifies the window manager about an application that is not responding.
- * Returns a new timeout to continue waiting in nanoseconds, or 0 to abort dispatch.
+ * Notify the window manager about an application that is not responding.
+ * Return a new timeout to continue waiting in nanoseconds, or 0 to abort dispatch.
*/
long notifyANR(InputApplicationHandle inputApplicationHandle, IBinder token,
- String reason);
+ @Nullable Integer pid, String reason);
- public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags);
+ /**
+ * This callback is invoked when an event first arrives to InputDispatcher and before it is
+ * placed onto InputDispatcher's queue. If this event is intercepted, it will never be
+ * processed by InputDispacher.
+ * @param event The key event that's arriving to InputDispatcher
+ * @param policyFlags The policy flags
+ * @return the flags that tell InputDispatcher how to handle the event (for example, whether
+ * to pass it to the user)
+ */
+ int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags);
/**
* Provides an opportunity for the window manager policy to intercept early motion event
@@ -2231,11 +2291,23 @@
int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
int policyFlags);
- public long interceptKeyBeforeDispatching(IBinder token,
- KeyEvent event, int policyFlags);
+ /**
+ * This callback is invoked just before the key is about to be sent to an application.
+ * This allows the policy to make some last minute decisions on whether to intercept this
+ * key.
+ * @param token the window token that's about to receive this event
+ * @param event the key event that's being dispatched
+ * @param policyFlags the policy flags
+ * @return negative value if the key should be skipped (not sent to the app). 0 if the key
+ * should proceed getting dispatched to the app. positive value to indicate the additional
+ * time delay, in nanoseconds, to wait before sending this key to the app.
+ */
+ long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags);
- public KeyEvent dispatchUnhandledKey(IBinder token,
- KeyEvent event, int policyFlags);
+ /**
+ * Dispatch unhandled key
+ */
+ KeyEvent dispatchUnhandledKey(IBinder token, KeyEvent event, int policyFlags);
public int getPointerLayer();
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 5e1cbc3..e166bfc 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -10,6 +10,7 @@
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.H.ON_POINTER_DOWN_OUTSIDE_FOCUS;
+import android.annotation.Nullable;
import android.os.Build;
import android.os.Debug;
import android.os.IBinder;
@@ -173,23 +174,23 @@
*/
@Override
public long notifyANR(InputApplicationHandle inputApplicationHandle, IBinder token,
- String reason) {
+ @Nullable Integer pid, String reason) {
final long startTime = SystemClock.uptimeMillis();
try {
- return notifyANRInner(inputApplicationHandle, token, reason);
+ return notifyANRInner(inputApplicationHandle, token, pid, reason);
} finally {
// Log the time because the method is called from InputDispatcher thread. It shouldn't
- // take too long that may affect input response time.
+ // take too long because it blocks input while executing.
Slog.d(TAG_WM, "notifyANR took " + (SystemClock.uptimeMillis() - startTime) + "ms");
}
}
private long notifyANRInner(InputApplicationHandle inputApplicationHandle, IBinder token,
- String reason) {
+ @Nullable Integer pid, String reason) {
ActivityRecord activity = null;
WindowState windowState = null;
boolean aboveSystem = false;
- int windowPid = INVALID_PID;
+ int windowPid = pid != null ? pid : INVALID_PID;
preDumpIfLockTooSlow();
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 7843663..0202c88 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -710,9 +710,8 @@
jobject tokenObj = javaObjectForIBinder(env, token);
jstring reasonObj = env->NewStringUTF(reason.c_str());
- jlong newTimeout = env->CallLongMethod(mServiceObj,
- gServiceClassInfo.notifyANR, inputApplicationHandleObj, tokenObj,
- reasonObj);
+ jlong newTimeout = env->CallLongMethod(mServiceObj, gServiceClassInfo.notifyANR,
+ inputApplicationHandleObj, tokenObj, reasonObj);
if (checkAndClearExceptionFromCallback(env, "notifyANR")) {
newTimeout = 0; // abort dispatch
} else {
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
new file mode 100644
index 0000000..9d35cbc
--- /dev/null
+++ b/tests/Input/Android.bp
@@ -0,0 +1,12 @@
+android_test {
+ name: "InputTests",
+ srcs: ["src/**/*.kt"],
+ platform_apis: true,
+ certificate: "platform",
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "android-support-test",
+ "ub-uiautomator",
+ ],
+}
diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml
new file mode 100644
index 0000000..4195df7
--- /dev/null
+++ b/tests/Input/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.test.input">
+ <uses-permission android:name="android.permission.MONITOR_INPUT"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+
+ <application android:label="InputTest">
+
+ <activity android:name=".UnresponsiveGestureMonitorActivity"
+ android:label="Unresponsive gesture monitor"
+ android:process=":externalProcess">
+ </activity>
+
+
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.test.input"
+ android:label="Input Tests"/>
+</manifest>
diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml
new file mode 100644
index 0000000..c62db1ea
--- /dev/null
+++ b/tests/Input/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2020 Google Inc. All Rights Reserved.
+ -->
+<configuration description="Runs Input Tests">
+ <option name="test-tag" value="InputTests" />
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- keeps the screen on during tests -->
+ <option name="screen-always-on" value="on" />
+ <!-- prevents the phone from restarting -->
+ <option name="force-skip-system-props" value="true" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="InputTests.apk"/>
+
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.test.input"/>
+ <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" />
+ <option name="shell-timeout" value="660s" />
+ <option name="test-timeout" value="600s" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt
new file mode 100644
index 0000000..4da3eca
--- /dev/null
+++ b/tests/Input/src/com/android/test/input/AnrTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test.input
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.filters.MediumTest
+
+import android.graphics.Rect
+import android.os.SystemClock
+import android.provider.Settings
+import android.provider.Settings.Global.HIDE_ERROR_DIALOGS
+import android.support.test.uiautomator.By
+import android.support.test.uiautomator.UiDevice
+import android.support.test.uiautomator.UiObject2
+import android.support.test.uiautomator.Until
+import android.view.InputDevice
+import android.view.MotionEvent
+
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This test makes sure that an unresponsive gesture monitor gets an ANR.
+ *
+ * The gesture monitor must be registered from a different process than the instrumented process.
+ * Otherwise, when the test runs, you will get:
+ * Test failed to run to completion.
+ * Reason: 'Instrumentation run failed due to 'keyDispatchingTimedOut''.
+ * Check device logcat for details
+ * RUNNER ERROR: Instrumentation run failed due to 'keyDispatchingTimedOut'
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AnrTest {
+ companion object {
+ private const val TAG = "AnrTest"
+ }
+
+ val mInstrumentation = InstrumentationRegistry.getInstrumentation()
+ var mHideErrorDialogs = 0
+
+ @Before
+ fun setUp() {
+ val contentResolver = mInstrumentation.targetContext.contentResolver
+ mHideErrorDialogs = Settings.Global.getInt(contentResolver, HIDE_ERROR_DIALOGS, 0)
+ Settings.Global.putInt(contentResolver, HIDE_ERROR_DIALOGS, 0)
+ }
+
+ @After
+ fun tearDown() {
+ val contentResolver = mInstrumentation.targetContext.contentResolver
+ Settings.Global.putInt(contentResolver, HIDE_ERROR_DIALOGS, mHideErrorDialogs)
+ }
+
+ @Test
+ fun testGestureMonitorAnr() {
+ startUnresponsiveActivity()
+ val uiDevice: UiDevice = UiDevice.getInstance(mInstrumentation)
+ val obj: UiObject2? = uiDevice.wait(Until.findObject(
+ By.text("Unresponsive gesture monitor")), 10000)
+
+ if (obj == null) {
+ fail("Could not find unresponsive activity")
+ return
+ }
+
+ val rect: Rect = obj.visibleBounds
+ val downTime = SystemClock.uptimeMillis()
+ val downEvent = MotionEvent.obtain(downTime, downTime,
+ MotionEvent.ACTION_DOWN, rect.left.toFloat(), rect.top.toFloat(), 0 /* metaState */)
+ downEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+
+ mInstrumentation.uiAutomation.injectInputEvent(downEvent, false /* sync*/)
+
+ // Todo: replace using timeout from android.hardware.input.IInputManager
+ SystemClock.sleep(5000) // default ANR timeout for gesture monitors
+
+ clickCloseAppOnAnrDialog()
+ }
+
+ private fun clickCloseAppOnAnrDialog() {
+ // Find anr dialog and kill app
+ val uiDevice: UiDevice = UiDevice.getInstance(mInstrumentation)
+ val closeAppButton: UiObject2? =
+ uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000)
+ if (closeAppButton == null) {
+ fail("Could not find anr dialog")
+ return
+ }
+ closeAppButton.click()
+ }
+
+ private fun startUnresponsiveActivity() {
+ val flags = " -W -n "
+ val startCmd = "am start $flags com.android.test.input/.UnresponsiveGestureMonitorActivity"
+ mInstrumentation.uiAutomation.executeShellCommand(startCmd)
+ }
+}
\ No newline at end of file
diff --git a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt
new file mode 100644
index 0000000..d83a457
--- /dev/null
+++ b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.input
+
+import android.app.Activity
+import android.hardware.input.InputManager
+import android.os.Bundle
+import android.os.Looper
+import android.util.Log
+import android.view.InputChannel
+import android.view.InputEvent
+import android.view.InputEventReceiver
+import android.view.InputMonitor
+
+class UnresponsiveReceiver(channel: InputChannel, looper: Looper) :
+ InputEventReceiver(channel, looper) {
+ companion object {
+ const val TAG = "UnresponsiveReceiver"
+ }
+ override fun onInputEvent(event: InputEvent) {
+ Log.i(TAG, "Received $event")
+ // Not calling 'finishInputEvent' in order to trigger the ANR
+ }
+}
+
+class UnresponsiveGestureMonitorActivity : Activity() {
+ companion object {
+ const val MONITOR_NAME = "unresponsive gesture monitor"
+ }
+ private lateinit var mInputEventReceiver: InputEventReceiver
+ private lateinit var mInputMonitor: InputMonitor
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(MONITOR_NAME, displayId)
+ mInputEventReceiver = UnresponsiveReceiver(
+ mInputMonitor.getInputChannel(), Looper.myLooper())
+ }
+}