ViewHierarchyExceptionHandler calls View methods on non-UI thread
This stack comes up in failing chrome tests from time to time:
C 11:28:54.816 87.729s Main java.lang.AssertionError: Class was initialized on the UI thread, but current operation was performed on a background thread: Thread[Instr: org.chromium.base.test.BaseChromiumAndroidJUnitRunner,5,main]
C 11:28:54.816 87.730s Main at org.chromium.base.ThreadUtils$ThreadChecker.assertOnValidThreadHelper(ThreadUtils.java:97)
C 11:28:54.816 87.730s Main at org.chromium.base.ThreadUtils$ThreadChecker.assertOnValidThread(ThreadUtils.java:71)
C 11:28:54.816 87.730s Main at org.chromium.base.UserDataHost.checkThreadAndState(UserDataHost.java:64)
C 11:28:54.816 87.730s Main at org.chromium.base.UserDataHost.getUserData(UserDataHost.java:96)
C 11:28:54.816 87.730s Main at org.chromium.content.browser.webcontents.WebContentsImpl.getOrSetUserData(WebContentsImpl.java:1042)
C 11:28:54.816 87.730s Main at org.chromium.content.browser.input.ImeAdapterImpl.fromWebContents(ImeAdapterImpl.java:253)
C 11:28:54.816 87.730s Main at org.chromium.content_public.browser.ImeAdapter.fromWebContents(ImeAdapter.java:32)
C 11:28:54.816 87.730s Main at org.chromium.components.embedder_support.view.ContentView.onCreateInputConnection(ContentView.java:360)
C 11:28:54.816 87.730s Main at androidx.test.espresso.util.HumanReadables.describe(HumanReadables.java:247)
C 11:28:54.816 87.730s Main at androidx.test.espresso.util.HumanReadables.getViewHierarchyErrorMessage(HumanReadables.java:116)
C 11:28:54.816 87.730s Main at androidx.test.espresso.util.HumanReadables.getViewHierarchyErrorMessage(HumanReadables.java:73)
C 11:28:54.816 87.730s Main at androidx.test.espresso.base.ViewHierarchyExceptionHandler.dumpFullViewHierarchyToFile(ViewHierarchyExceptionHandler.java:96)
C 11:28:54.816 87.730s Main at androidx.test.espresso.base.ViewHierarchyExceptionHandler.handleSafely(ViewHierarchyExceptionHandler.java:65)
C 11:28:54.816 87.730s Main at androidx.test.espresso.base.ViewHierarchyExceptionHandler.handleSafely(ViewHierarchyExceptionHandler.java:38)
C 11:28:54.816 87.730s Main at androidx.test.espresso.base.DefaultFailureHandler$TypedFailureHandler.handle(DefaultFailureHandler.java:158)
C 11:28:54.816 87.730s Main at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:120)
C 11:28:54.816 87.730s Main at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:385)
C 11:28:54.816 87.730s Main at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:366)
C 11:28:54.816 87.730s Main at org.chromium.chrome.browser.tasks.tab_management.ArchivedTabsDialogCoordinatorTest.testContentDescription(ArchivedTabsDialogCoordinatorTest.java:754)
Espresso normally jumps to the UI thread to interact with Views, but I think that got missed for ViewHierarchyExceptionHandler. It should do a thread hop before accessing Views.
Note: adding a thread hop to fix the above exception just leads to another one:
C 13:11:43.055 48.889s Main at org.chromium.base.ThreadUtils$ThreadChecker.assertOnValidThreadHelper(ThreadUtils.java:97)
C 13:11:43.055 48.889s Main at org.chromium.base.ThreadUtils$ThreadChecker.assertOnValidThread(ThreadUtils.java:71)
C 13:11:43.055 48.889s Main at org.chromium.base.UserDataHost.checkThreadAndState(UserDataHost.java:64)
C 13:11:43.055 48.889s Main at org.chromium.base.UserDataHost.getUserData(UserDataHost.java:96)
C 13:11:43.055 48.889s Main at org.chromium.content.browser.webcontents.WebContentsImpl.getOrSetUserData(WebContentsImpl.java:1042)
C 13:11:43.055 48.889s Main at org.chromium.content.browser.input.ImeAdapterImpl.fromWebContents(ImeAdapterImpl.java:253)
C 13:11:43.055 48.889s Main at org.chromium.content_public.browser.ImeAdapter.fromWebContents(ImeAdapter.java:32)
C 13:11:43.055 48.889s Main at org.chromium.components.embedder_support.view.ContentView.onCreateInputConnection(ContentView.java:360)
C 13:11:43.055 48.889s Main at androidx.test.espresso.util.HumanReadables.describe(HumanReadables.java:247)
C 13:11:43.055 48.889s Main at androidx.test.espresso.util.HumanReadables.getViewHierarchyErrorMessage(HumanReadables.java:116)
C 13:11:43.055 48.889s Main at androidx.test.espresso.NoMatchingViewException.getErrorMessage(NoMatchingViewException.java:88)
C 13:11:43.055 48.889s Main at androidx.test.espresso.NoMatchingViewException.<init>(NoMatchingViewException.java:53)
C 13:11:43.055 48.889s Main at androidx.test.espresso.NoMatchingViewException.<init>(Unknown Source:0)
C 13:11:43.055 48.889s Main at androidx.test.espresso.NoMatchingViewException$Builder.build(NoMatchingViewException.java:185)
C 13:11:43.055 48.889s Main at androidx.test.espresso.base.DefaultFailureHandler.lambda$getNoMatchingViewExceptionTruncater$0(DefaultFailureHandler.java:93)
C 13:11:43.055 48.889s Main at androidx.test.espresso.base.DefaultFailureHandler$$ExternalSyntheticLambda1.truncateExceptionMessage(D8$$SyntheticClass:0)
C 13:11:43.055 48.889s Main at androidx.test.espresso.base.ViewHierarchyExceptionHandler.handleSafely(ViewHierarchyExceptionHandler.java:75)
C 13:11:43.055 48.889s Main at androidx.test.espresso.base.ViewHierarchyExceptionHandler.handleSafely(ViewHierarchyExceptionHandler.java:41)
C 13:11:43.055 48.889s Main at androidx.test.espresso.base.DefaultFailureHandler$TypedFailureHandler.handle(DefaultFailureHandler.java:158)
C 13:11:43.055 48.889s Main at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:120)
C 13:11:43.055 48.889s Main at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:385)
C 13:11:43.055 48.890s Main at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:366)
This also shows that HumanReadables.getViewHierarchyErrorMessage() is called twice for the same exception, which maybe is a thing to fix as well.
This patch fixes things locally in Chrome:
19a20
> import android.os.Looper;
31a33,34
> import java.util.concurrent.atomic.AtomicReference;
> import java.util.function.Supplier;
72c75
< Throwable error = truncater.truncateExceptionMessage(exception, msgLen, viewHierarchyFile);
---
> Throwable error = runOnUiThread(exception, () -> truncater.truncateExceptionMessage(exception, msgLen, viewHierarchyFile));
92a96,119
> private static <T> T runOnUiThread(RootViewException e, Supplier<T> func) {
> if (Looper.myLooper() == Looper.getMainLooper()) {
> return func.get();
> }
>
> AtomicReference<T> ret = new AtomicReference<>();
> e.getRootView().post(() -> {
> ret.set(func.get());
> synchronized (ret) {
> ret.notifyAll();
> }
> });
> synchronized (ret) {
> while (ret.get() == null) {
> try {
> ret.wait();
> } catch (InterruptedException unused) {
> // Ignore.
> }
> }
> }
> return ret.get();
> }
>
95c122
< String viewHierarchyMsg =
---
> String viewHierarchyMsg = runOnUiThread(error, () ->
100c127
< /* problemViewSuffix= */ null);
---
> /* problemViewSuffix= */ null));
Probably a "proper" fix would use dagger to inject the @MainThread Executor, but since I just patched the one file in a chromium checkout, I don't have the ability to run the dagger annotation processor in order to play around with that.
Also not sure how to best go about not dumping the view hierarchy twice...