Loading core/java/android/os/Looper.java +8 −1 Original line number Diff line number Diff line Loading @@ -57,6 +57,7 @@ import java.util.Objects; * }</pre> */ @android.ravenwood.annotation.RavenwoodKeepWholeClass @android.ravenwood.annotation.RavenwoodRedirectionClass("Looper_ravenwood") public final class Looper { /* * API Implementation Note: Loading Loading @@ -247,7 +248,7 @@ public final class Looper { } long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid); try { msg.target.dispatchMessage(msg); dispatchMessage(msg); if (observer != null) { observer.messageDispatched(token, msg); } Loading Loading @@ -308,6 +309,12 @@ public final class Looper { return true; } /** Allow ravenwood to hook any "dispatch". */ @android.ravenwood.annotation.RavenwoodRedirect private static void dispatchMessage(Message msg) { msg.target.dispatchMessage(msg); } /** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. Loading core/java/android/os/Looper_ravenwood.java 0 → 100644 +30 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 java.util.function.Consumer; @android.ravenwood.annotation.RavenwoodKeepWholeClass public class Looper_ravenwood { public static volatile Consumer<Message> sDispatcher = (msg) -> { // Default implementation msg.target.dispatchMessage(msg); }; public static void dispatchMessage(Message msg) { sDispatcher.accept(msg); } } ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +8 −3 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; import android.os.Looper_ravenwood; import android.os.Message; import android.os.Process_ravenwood; import android.os.ServiceManager; Loading Loading @@ -133,6 +134,9 @@ public class RavenwoodRuntimeEnvironmentController { private static final boolean TOLERATE_LOOPER_ASSERTS = !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS")); private static final boolean TOLERATE_LOOPER_EXCEPTIONS = "1".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_EXCEPTIONS")); static final int DEFAULT_TIMEOUT_SECONDS = 10; private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000; Loading Loading @@ -360,6 +364,7 @@ public class RavenwoodRuntimeEnvironmentController { final var main = new HandlerThread(MAIN_THREAD_NAME); sMainThread = main; main.start(); Looper_ravenwood.sDispatcher = RavenwoodRuntimeEnvironmentController::dispatchMessage; Looper.setMainLooperForTest(main.getLooper()); final boolean isSelfInstrumenting = Loading Loading @@ -606,15 +611,15 @@ public class RavenwoodRuntimeEnvironmentController { return outer; } // TODO: use it to tolerate assert failures on the main thread static void dispatchMessage(Message msg) { private static void dispatchMessage(Message msg) { try { msg.getTarget().dispatchMessage(msg); } catch (Throwable th) { var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(), Thread.currentThread()); sStdErr.println(desc); if (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th)) { if (TOLERATE_LOOPER_EXCEPTIONS || (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th))) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(th)); return; Loading ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java +20 −16 Original line number Diff line number Diff line Loading @@ -27,9 +27,14 @@ import org.junit.Test; import java.util.concurrent.atomic.AtomicReference; /** * Tests related to the main thread. * * Some tests require $RAVENWOOD_RUN_SLOW_TESTS to set to "1". */ public class RavenwoodMainThreadTest { private static final boolean RUN_UNSAFE_TESTS = "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS")); private static final boolean RUN_SLOW_TESTS = "1".equals(System.getenv("RAVENWOOD_RUN_SLOW_TESTS")); @Test public void testRunOnMainThread() { Loading @@ -44,18 +49,18 @@ public class RavenwoodMainThreadTest { /** * Sleep a long time on the main thread. This test would then "pass", but Ravenwood * should show the stack traces. * should show the "SLOW TEST DETECTED" stack traces. * * This is "unsafe" because this test is slow. * This test requires `RAVENWOOD_RUN_SLOW_TESTS=1`. */ @Test public void testUnsafeMainThreadHang() { assumeTrue(RUN_UNSAFE_TESTS); @androidx.test.filters.LargeTest public void testMainThreadSlow() { assumeTrue(RUN_SLOW_TESTS); // The test should time out. RavenwoodUtils.runOnMainThreadSync(() -> { try { Thread.sleep(30_000); Thread.sleep(12_000); } catch (InterruptedException e) { fail("Interrupted"); } Loading @@ -63,19 +68,18 @@ public class RavenwoodMainThreadTest { } /** * AssertionError on the main thread would be swallowed and reported "normally". * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills * the process) * runOnMainThreadSync() should report back the inner exception, if any. * * This is "unsafe" only because this feature can be disabled via the env var. * Note this test does _not_ involves "recoverable exception" check in * RavenwoodRuntimeEnvironmentController because the exception is caught in side the * Runnable that's executed on the main handler. This purely tests runOnMainThreadSync()'s * exception propagation. */ @Test public void testUnsafeAssertFailureOnMainThread() { assumeTrue(RUN_UNSAFE_TESTS); public void testRunOnMainThreadSync() { assertThrows(AssertionError.class, () -> { RavenwoodUtils.runOnMainThreadSync(() -> { fail(); fail("Assertion failure on main thread!"); }); }); } Loading ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -15,13 +15,20 @@ */ package com.android.ravenwoodtest.runnercallbacktests; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.NoRavenizer; import android.platform.test.ravenwood.RavenwoodAwareTestRunner; import android.platform.test.ravenwood.RavenwoodUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.AfterClass; import org.junit.Assert; Loading @@ -38,6 +45,7 @@ import org.junit.runners.model.Statement; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; Loading Loading @@ -455,4 +463,35 @@ public class RavenwoodRunnerCallbackTest extends RavenwoodRunnerTestBase { public void test2() { } } /** */ @RunWith(AndroidJUnit4.class) // CHECKSTYLE:OFF @Expected(""" testRunStarted: classes testSuiteStarted: classes testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing the test because it's recoverable *** testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest testSuiteFinished: classes testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON public static class MainThreadAssertionFailureTest { @Test public void test1() { var h = new Handler(Looper.getMainLooper()); h.post(() -> fail("If testMainThreadAssertionFailure() fails with this, that's expected.")); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); // The looper should still be alive, so this should work. (if the looper has finished, // this would hang.) var value = new AtomicInteger(0); RavenwoodUtils.runOnMainThreadSync(() -> value.set(1) ); assertThat(value.get()).isEqualTo(1); } } } Loading
core/java/android/os/Looper.java +8 −1 Original line number Diff line number Diff line Loading @@ -57,6 +57,7 @@ import java.util.Objects; * }</pre> */ @android.ravenwood.annotation.RavenwoodKeepWholeClass @android.ravenwood.annotation.RavenwoodRedirectionClass("Looper_ravenwood") public final class Looper { /* * API Implementation Note: Loading Loading @@ -247,7 +248,7 @@ public final class Looper { } long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid); try { msg.target.dispatchMessage(msg); dispatchMessage(msg); if (observer != null) { observer.messageDispatched(token, msg); } Loading Loading @@ -308,6 +309,12 @@ public final class Looper { return true; } /** Allow ravenwood to hook any "dispatch". */ @android.ravenwood.annotation.RavenwoodRedirect private static void dispatchMessage(Message msg) { msg.target.dispatchMessage(msg); } /** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. Loading
core/java/android/os/Looper_ravenwood.java 0 → 100644 +30 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 java.util.function.Consumer; @android.ravenwood.annotation.RavenwoodKeepWholeClass public class Looper_ravenwood { public static volatile Consumer<Message> sDispatcher = (msg) -> { // Default implementation msg.target.dispatchMessage(msg); }; public static void dispatchMessage(Message msg) { sDispatcher.accept(msg); } }
ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +8 −3 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; import android.os.Looper_ravenwood; import android.os.Message; import android.os.Process_ravenwood; import android.os.ServiceManager; Loading Loading @@ -133,6 +134,9 @@ public class RavenwoodRuntimeEnvironmentController { private static final boolean TOLERATE_LOOPER_ASSERTS = !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS")); private static final boolean TOLERATE_LOOPER_EXCEPTIONS = "1".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_EXCEPTIONS")); static final int DEFAULT_TIMEOUT_SECONDS = 10; private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000; Loading Loading @@ -360,6 +364,7 @@ public class RavenwoodRuntimeEnvironmentController { final var main = new HandlerThread(MAIN_THREAD_NAME); sMainThread = main; main.start(); Looper_ravenwood.sDispatcher = RavenwoodRuntimeEnvironmentController::dispatchMessage; Looper.setMainLooperForTest(main.getLooper()); final boolean isSelfInstrumenting = Loading Loading @@ -606,15 +611,15 @@ public class RavenwoodRuntimeEnvironmentController { return outer; } // TODO: use it to tolerate assert failures on the main thread static void dispatchMessage(Message msg) { private static void dispatchMessage(Message msg) { try { msg.getTarget().dispatchMessage(msg); } catch (Throwable th) { var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(), Thread.currentThread()); sStdErr.println(desc); if (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th)) { if (TOLERATE_LOOPER_EXCEPTIONS || (TOLERATE_LOOPER_ASSERTS && isThrowableRecoverable(th))) { sPendingRecoverableUncaughtException.compareAndSet(null, makeRecoverableExceptionInstance(th)); return; Loading
ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java +20 −16 Original line number Diff line number Diff line Loading @@ -27,9 +27,14 @@ import org.junit.Test; import java.util.concurrent.atomic.AtomicReference; /** * Tests related to the main thread. * * Some tests require $RAVENWOOD_RUN_SLOW_TESTS to set to "1". */ public class RavenwoodMainThreadTest { private static final boolean RUN_UNSAFE_TESTS = "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS")); private static final boolean RUN_SLOW_TESTS = "1".equals(System.getenv("RAVENWOOD_RUN_SLOW_TESTS")); @Test public void testRunOnMainThread() { Loading @@ -44,18 +49,18 @@ public class RavenwoodMainThreadTest { /** * Sleep a long time on the main thread. This test would then "pass", but Ravenwood * should show the stack traces. * should show the "SLOW TEST DETECTED" stack traces. * * This is "unsafe" because this test is slow. * This test requires `RAVENWOOD_RUN_SLOW_TESTS=1`. */ @Test public void testUnsafeMainThreadHang() { assumeTrue(RUN_UNSAFE_TESTS); @androidx.test.filters.LargeTest public void testMainThreadSlow() { assumeTrue(RUN_SLOW_TESTS); // The test should time out. RavenwoodUtils.runOnMainThreadSync(() -> { try { Thread.sleep(30_000); Thread.sleep(12_000); } catch (InterruptedException e) { fail("Interrupted"); } Loading @@ -63,19 +68,18 @@ public class RavenwoodMainThreadTest { } /** * AssertionError on the main thread would be swallowed and reported "normally". * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills * the process) * runOnMainThreadSync() should report back the inner exception, if any. * * This is "unsafe" only because this feature can be disabled via the env var. * Note this test does _not_ involves "recoverable exception" check in * RavenwoodRuntimeEnvironmentController because the exception is caught in side the * Runnable that's executed on the main handler. This purely tests runOnMainThreadSync()'s * exception propagation. */ @Test public void testUnsafeAssertFailureOnMainThread() { assumeTrue(RUN_UNSAFE_TESTS); public void testRunOnMainThreadSync() { assertThrows(AssertionError.class, () -> { RavenwoodUtils.runOnMainThreadSync(() -> { fail(); fail("Assertion failure on main thread!"); }); }); } Loading
ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -15,13 +15,20 @@ */ package com.android.ravenwoodtest.runnercallbacktests; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.NoRavenizer; import android.platform.test.ravenwood.RavenwoodAwareTestRunner; import android.platform.test.ravenwood.RavenwoodUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.AfterClass; import org.junit.Assert; Loading @@ -38,6 +45,7 @@ import org.junit.runners.model.Statement; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; Loading Loading @@ -455,4 +463,35 @@ public class RavenwoodRunnerCallbackTest extends RavenwoodRunnerTestBase { public void test2() { } } /** */ @RunWith(AndroidJUnit4.class) // CHECKSTYLE:OFF @Expected(""" testRunStarted: classes testSuiteStarted: classes testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest) testFailure: Exception detected on thread Ravenwood:Main: *** Continuing the test because it's recoverable *** testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest) testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$MainThreadAssertionFailureTest testSuiteFinished: classes testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON public static class MainThreadAssertionFailureTest { @Test public void test1() { var h = new Handler(Looper.getMainLooper()); h.post(() -> fail("If testMainThreadAssertionFailure() fails with this, that's expected.")); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); // The looper should still be alive, so this should work. (if the looper has finished, // this would hang.) var value = new AtomicInteger(0); RavenwoodUtils.runOnMainThreadSync(() -> value.set(1) ); assertThat(value.get()).isEqualTo(1); } } }