summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/os/Binder.java20
-rw-r--r--core/tests/coretests/Android.bp2
-rw-r--r--core/tests/coretests/AndroidTest.xml1
-rw-r--r--core/tests/coretests/AppThatCallsBinderMethods/Android.bp20
-rw-r--r--core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml26
-rw-r--r--core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt57
-rw-r--r--core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt23
-rw-r--r--core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl27
-rw-r--r--core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt247
9 files changed, 423 insertions, 0 deletions
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 97e9f34064ba..ed75491b8e21 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -1111,6 +1111,21 @@ public class Binder implements IBinder {
}
/**
+ * Called whenever the stub implementation throws an exception which isn't propagated to the
+ * remote caller by the binder. If this method isn't overridden, this exception is swallowed,
+ * and some default return values are propagated to the caller.
+ *
+ * <br> <b> This should not throw. </b> Doing so would defeat the purpose of this handler, and
+ * suppress the exception it is handling.
+ *
+ * @param code The transaction code being handled
+ * @param e The exception which was thrown.
+ * @hide
+ */
+ protected void onUnhandledException(int code, int flags, Exception e) {
+ }
+
+ /**
* @param in The raw file descriptor that an input data stream can be read from.
* @param out The raw file descriptor that normal command messages should be written to.
* @param err The raw file descriptor that command error messages should be written to.
@@ -1408,10 +1423,15 @@ public class Binder implements IBinder {
} else {
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
}
+ onUnhandledException(code, flags, e);
} else {
// Clear the parcel before writing the exception.
reply.setDataSize(0);
reply.setDataPosition(0);
+ // The writeException below won't do anything useful if this is the case.
+ if (Parcel.getExceptionCode(e) == 0) {
+ onUnhandledException(code, flags, e);
+ }
reply.writeException(e);
}
res = true;
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 23203f029992..a9ac67db2cbc 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -25,6 +25,7 @@ filegroup {
"BinderProxyCountingTestApp/src/**/*.java",
"BinderProxyCountingTestService/src/**/*.java",
"BinderDeathRecipientHelperApp/src/**/*.java",
+ "AppThatCallsBinderMethods/src/**/*.kt",
],
visibility: ["//visibility:private"],
}
@@ -141,6 +142,7 @@ android_test {
":BinderFrozenStateChangeCallbackTestApp",
":BinderProxyCountingTestApp",
":BinderProxyCountingTestService",
+ ":AppThatCallsBinderMethods",
],
}
diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index 3fdd72935217..dbe0b49f2c32 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -25,6 +25,7 @@
<option name="test-file-name" value="BinderFrozenStateChangeCallbackTestApp.apk" />
<option name="test-file-name" value="BinderProxyCountingTestApp.apk" />
<option name="test-file-name" value="BinderProxyCountingTestService.apk" />
+ <option name="test-file-name" value="AppThatCallsBinderMethods.apk" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/Android.bp b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
new file mode 100644
index 000000000000..dcc0d4f76bf2
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
@@ -0,0 +1,20 @@
+// Copyright (C) 2024 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.
+
+android_test_helper_app {
+ name: "AppThatCallsBinderMethods",
+ srcs: ["src/**/*.kt"],
+ platform_apis: true,
+ static_libs: ["coretests-aidl"],
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
new file mode 100644
index 000000000000..b2f6d7897681
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2024 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.frameworks.coretests.methodcallerhelperapp">
+ <application>
+ <receiver android:name="com.android.frameworks.coretests.methodcallerhelperapp.CallMethodsReceiver"
+ android:exported="true"/>
+ </application>
+
+</manifest>
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
new file mode 100644
index 000000000000..638cc3b7692f
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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.frameworks.coretests.methodcallerhelperapp
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+
+/**
+ * Receiver used to call methods when a binder is received
+ * {@link android.os.BinderUncaughtExceptionHandlerTest}.
+ */
+class CallMethodsReceiver : BroadcastReceiver() {
+ private val TAG = "CallMethodsReceiver"
+
+ override fun onReceive(context: Context, intent: Intent) {
+ try {
+ when (intent.getAction()) {
+ ACTION_CALL_METHOD -> intent.getExtras()!!.let {
+ Log.i(TAG, "Received ACTION_CALL_METHOD with extras: $it")
+ val iface = it.getBinder(EXTRA_BINDER)!!.let(ITestInterface.Stub::asInterface)!!
+ val name = it.getString(EXTRA_METHOD_NAME)!!
+ try {
+ when (name) {
+ "foo" -> iface.foo(5)
+ "onewayFoo" -> iface.onewayFoo(5)
+ "bar" -> iface.bar(5)
+ else -> Log.e(TAG, "Unknown method name")
+ }
+ } catch (e: Exception) {
+ // Exceptions expected
+ }
+ }
+ else -> Log.e(TAG, "Unknown action " + intent.getAction())
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception: ", e)
+ }
+ }
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
new file mode 100644
index 000000000000..37c6268164a7
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.frameworks.coretests.methodcallerhelperapp
+
+const val PACKAGE_NAME = "com.android.frameworks.coretests.methodcallerhelperapp"
+const val RECEIVER_NAME = "CallMethodsReceiver"
+const val ACTION_CALL_METHOD = PACKAGE_NAME + ".ACTION_CALL_METHOD"
+const val EXTRA_METHOD_NAME = PACKAGE_NAME + ".EXTRA_METHOD_NAME"
+const val EXTRA_BINDER = PACKAGE_NAME + ".EXTRA_BINDER"
diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
new file mode 100644
index 000000000000..ffcf178beda4
--- /dev/null
+++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.frameworks.coretests.aidl;
+
+/**
+ * Just an interface with a oneway, void and non-oneway method.
+ */
+interface ITestInterface {
+ // Method order matters, since we verify transaction codes
+ int foo(int a);
+ oneway void onewayFoo(int a);
+ void bar(int a);
+}
diff --git a/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
new file mode 100644
index 000000000000..791c209e4473
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024 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 android.os
+
+import android.content.Intent
+import android.platform.test.annotations.DisabledOnRavenwood
+import android.platform.test.annotations.Presubmit
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+import com.android.frameworks.coretests.methodcallerhelperapp.*
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.mockito.ArgumentMatcher
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.intThat
+import org.mockito.Mockito.after
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.quality.Strictness.STRICT_STUBS
+
+private const val TIMEOUT_DURATION_MS = 2000L
+private const val FALSE_NEG_DURATION_MS = 500L
+private const val FLAG_ONEWAY = 1
+// From ITestInterface.Stub class, these values are package private
+private const val TRANSACTION_foo = 1
+private const val TRANSACTION_onewayFoo = 2
+private const val TRANSACTION_bar = 3
+
+/** Tests functionality of {@link android.os.Binder.onUnhandledException}. */
+@DisabledOnRavenwood(reason = "multi-app")
+@Presubmit
+@RunWith(AndroidJUnit4::class)
+class BinderUncaughtExceptionHandlerTest {
+
+ val mContext = InstrumentationRegistry.getInstrumentation().getTargetContext()
+
+ @Rule @JvmField val rule = MockitoJUnit.rule().strictness(STRICT_STUBS)
+
+ @Spy var mInterfaceImpl: ITestImpl = ITestImpl()
+
+ // This subclass is needed for visibility issues (via protected), since the method we are
+ // verifying lives on the boot classpath, it is not enough to be in the same package.
+ open class ITestImpl : ITestInterface.Stub() {
+ override fun onUnhandledException(code: Int, flags: Int, e: Exception?) =
+ onUnhandledExceptionVisible(code, flags, e)
+
+ public open fun onUnhandledExceptionVisible(code: Int, flags: Int, e: Exception?) {}
+
+ @Throws(RemoteException::class)
+ override open fun foo(x: Int): Int = throw UnsupportedOperationException()
+
+ @Throws(RemoteException::class)
+ override open fun onewayFoo(x: Int): Unit = throw UnsupportedOperationException()
+
+ @Throws(RemoteException::class)
+ override open fun bar(x: Int): Unit = throw UnsupportedOperationException()
+ }
+
+ class OnewayMatcher(private val isOneway: Boolean) : ArgumentMatcher<Int> {
+ override fun matches(argument: Int?) =
+ (argument!! and FLAG_ONEWAY) == if (isOneway) 1 else 0
+
+ override fun toString() = "Expected oneway: $isOneway"
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_foo),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_foo),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsSecurityException_HandlerNotCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ // No unexpected calls
+ verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+ .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_bar),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_bar),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsSecurityException_HandlerNotCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ // No unexpected calls
+ verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+ .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testOnewayMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).doNothing().`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testOnewayMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ // All exceptions are uncaught for oneway
+ @Test
+ fun testOnewayMethod_ifThrowsSecurityException_HandlerCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ private fun dispatchActionCall(methodName: String) =
+ Intent(ACTION_CALL_METHOD).apply {
+ putExtras(
+ Bundle().apply {
+ putBinder(EXTRA_BINDER, mInterfaceImpl as IBinder)
+ putString(EXTRA_METHOD_NAME, methodName)
+ }
+ )
+ setClassName(PACKAGE_NAME, CallMethodsReceiver::class.java.getName())
+ }.let { mContext.sendBroadcast(it) }
+}