diff options
author | Jerome Gaillard <jgaillard@google.com> | 2023-11-23 16:10:11 +0000 |
---|---|---|
committer | Cherrypicker Worker <android-build-cherrypicker-worker@google.com> | 2023-11-30 10:59:26 +0000 |
commit | 1cb93bced2afc95be8401ddcc646db4e4780c278 (patch) | |
tree | a8d54da94f0acdcd48ff42226523cb60cf9cde6d | |
parent | cf1c3fe95c7a9200f975c22e4de1c9ccf8b3d703 (diff) | |
download | layoutlib-1cb93bced2afc95be8401ddcc646db4e4780c278.tar.gz |
Ensure accessibility works with dialogs
Dialogs normally display in a different window, but in layoutlib they
are simply added to the current view hierarchy. That causes a problem
for accessibility, as the root view for dialogs is a DecorView, which is
automatically considered to be the root for accessibility.
This confuses the accessibility node info creation, resulting in a
cycle. To prevent that, we set the decor view to be the root view as far
as ViewRootImpl is concerned, which is enough to solve the accessibility
cycle issue. But we need to make sure that Layout, which is the actual
root of the hierarchy still considers itself visible (which requires
overriding getChildVisibleRect and getGlobalVisibleRect, as those
normally query ViewRootImpl which is not the parent of Layout anymore
when there is a Dialog).
Bug: 312418057
Test: test added
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:69e377a789407277ebb748f2c4e01216f2e9a92d)
Merged-In: I0742c0bc9469fdf114853e686f9cf213ef676f2d
Change-Id: I0742c0bc9469fdf114853e686f9cf213ef676f2d
6 files changed, 102 insertions, 10 deletions
diff --git a/bridge/src/android/view/AttachInfo_Accessor.java b/bridge/src/android/view/AttachInfo_Accessor.java index 2e38b31793..b8c8f0e060 100644 --- a/bridge/src/android/view/AttachInfo_Accessor.java +++ b/bridge/src/android/view/AttachInfo_Accessor.java @@ -37,6 +37,8 @@ public class AttachInfo_Accessor { info.mInTouchMode = false; // this is so that we can display selections. info.mHardwareAccelerated = false; info.mApplicationScale = 1.0f; + ViewRootImpl_Accessor.setChild(root, view); + view.assignParent(root); view.dispatchAttachedToWindow(info, 0); } diff --git a/bridge/src/android/view/ViewRootImpl_Accessor.java b/bridge/src/android/view/ViewRootImpl_Accessor.java index 691f59ae46..d15952ad3d 100644 --- a/bridge/src/android/view/ViewRootImpl_Accessor.java +++ b/bridge/src/android/view/ViewRootImpl_Accessor.java @@ -26,9 +26,13 @@ public class ViewRootImpl_Accessor { public static void setChild(ViewRootImpl viewRoot, View child) { viewRoot.mView = child; - child.assignParent(viewRoot); - viewRoot.mWidth = child.getWidth(); - viewRoot.mHeight = child.getHeight(); + if (child != null) { + viewRoot.mWidth = child.getWidth(); + viewRoot.mHeight = child.getHeight(); + } else { + viewRoot.mWidth = -1; + viewRoot.mHeight = -1; + } } public static void detachFromWindow(ViewRootImpl viewRoot) { diff --git a/bridge/src/android/view/WindowManagerImpl.java b/bridge/src/android/view/WindowManagerImpl.java index eb1e22c736..285ca9e5e4 100644 --- a/bridge/src/android/view/WindowManagerImpl.java +++ b/bridge/src/android/view/WindowManagerImpl.java @@ -41,6 +41,8 @@ import com.android.internal.R; import com.android.internal.policy.DecorView; import com.android.layoutlib.bridge.Bridge; +import java.util.ArrayList; + public class WindowManagerImpl implements WindowManager { private final Context mContext; @@ -179,10 +181,12 @@ public class WindowManagerImpl implements WindowManager { } } mCurrentRootView.addView(arg0, frameLayoutParams); + ViewRootImpl_Accessor.setChild(mBaseRootView.getViewRootImpl(), arg0); } @Override public void removeView(View arg0) { + ViewRootImpl viewRootImpl = arg0.getViewRootImpl(); if (mCurrentRootView != null) { mCurrentRootView.removeView(arg0); if (mBaseRootView != null && mCurrentRootView.getChildCount() == 0) { @@ -190,6 +194,20 @@ public class WindowManagerImpl implements WindowManager { mCurrentRootView = null; } } + if (viewRootImpl != null && viewRootImpl.getView() == arg0) { + View newRoot = null; + if (mCurrentRootView != null && mCurrentRootView.getChildCount() > 0) { + ArrayList<View> childrenList = mCurrentRootView.buildOrderedChildList(); + newRoot = childrenList.get(childrenList.size() - 1); + } else if (mBaseRootView != null) { + View root = mBaseRootView; + while (root.getParent() instanceof View) { + root = (View)root.getParent(); + } + newRoot = root; + } + ViewRootImpl_Accessor.setChild(viewRootImpl, newRoot); + } } @Override diff --git a/bridge/src/com/android/layoutlib/bridge/impl/Layout.java b/bridge/src/com/android/layoutlib/bridge/impl/Layout.java index c8e5009888..1ca3b9c2cc 100644 --- a/bridge/src/com/android/layoutlib/bridge/impl/Layout.java +++ b/bridge/src/com/android/layoutlib/bridge/impl/Layout.java @@ -38,6 +38,8 @@ import com.android.resources.ScreenOrientation; import android.R.id; import android.annotation.NonNull; import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -196,6 +198,25 @@ class Layout extends FrameLayout { mBuilder = null; } + @Override + public boolean getChildVisibleRect(View child, Rect r, Point offset, boolean forceParentCheck) { + return r.intersect(0, 0, getWidth(), getHeight()); + } + + @Override + public boolean getGlobalVisibleRect(Rect r, Point globalOffset) { + int width = mRight - mLeft; + int height = mBottom - mTop; + if (width > 0 && height > 0) { + r.set(0, 0, width, height); + if (globalOffset != null) { + globalOffset.set(-mScrollX, -mScrollY); + } + return true; + } + return false; + } + @NonNull private static View createSysUiOverlay(@NonNull BridgeContext context) { SysUiOverlay overlay = new SysUiOverlay(context, 20, 10, 50, 40, 60); diff --git a/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index de7f651bab..034a92df75 100644 --- a/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -72,8 +72,6 @@ import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; -import android.view.ViewRootImpl; -import android.view.ViewRootImpl_Accessor; import android.view.WindowManagerImpl; import android.widget.ActionMenuView; import android.widget.FrameLayout; @@ -375,11 +373,6 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { mViewRoot.getViewRootImpl().mTmpFrames.displayFrame.set(mViewRoot.getLeft(), mViewRoot.getTop(), mViewRoot.getRight(), mViewRoot.getBottom()); - ViewRootImpl rootImpl = AttachInfo_Accessor.getRootView(mViewRoot); - if (rootImpl != null) { - ViewRootImpl_Accessor.setChild(rootImpl, mViewRoot); - } - mSystemViewInfoList = visitAllChildren(mViewRoot, 0, 0, params, false); diff --git a/bridge/tests/src/com/android/layoutlib/bridge/android/AccessibilityTest.java b/bridge/tests/src/com/android/layoutlib/bridge/android/AccessibilityTest.java index 149680868e..dc24d845ca 100644 --- a/bridge/tests/src/com/android/layoutlib/bridge/android/AccessibilityTest.java +++ b/bridge/tests/src/com/android/layoutlib/bridge/android/AccessibilityTest.java @@ -19,6 +19,7 @@ package com.android.layoutlib.bridge.android; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.Result; import com.android.ide.common.rendering.api.SessionParams; +import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.rendering.api.ViewInfo; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.intensive.LayoutLibTestCallback; @@ -117,4 +118,57 @@ public class AccessibilityTest extends RenderTestBase { session.dispose(); } } + + @Test + public void testDialogAccessibility() throws Exception { + String layout = + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " android:padding=\"16dp\"\n" + + " android:orientation=\"horizontal\"\n" + + " android:layout_width=\"fill_parent\"\n" + + " android:layout_height=\"fill_parent\">\n" + + " <com.android.layoutlib.test.myapplication.widgets.DialogView\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_width=\"wrap_content\" />\n" + + "</LinearLayout>\n"; + LayoutPullParser parser = LayoutPullParser.createFromString(layout); + // Create LayoutLibCallback. + LayoutLibTestCallback layoutLibCallback = + new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); + layoutLibCallback.initResources(); + SessionParams params = getSessionParamsBuilder() + .setParser(parser) + .setCallback(layoutLibCallback) + .setTheme("Theme.Material.Light.NoActionBar.Fullscreen", false) + .setRenderingMode(RenderingMode.V_SCROLL) + .disableDecoration() + .build(); + RenderSession session = sBridge.createSession(params); + session.setElapsedFrameTimeNanos(1); + try { + Result renderResult = session.render(50000); + assertTrue(renderResult.isSuccess()); + View rootView = + (View)((View) session.getSystemRootViews().get(1).getViewObject()).getParent(); + int[] counter = {0}; + session.execute(() -> { + AccessibilityNodeInfo rootNode = rootView.createAccessibilityNodeInfo(); + assertNotNull(rootNode); + rootNode.setQueryFromAppProcessEnabled(rootView, true); + traverseAccessibilityTree(rootNode, counter); + }); + assertEquals(17, counter[0]); + } finally { + session.dispose(); + } + } + + private void traverseAccessibilityTree(AccessibilityNodeInfo node, int[] counter) { + int childrenSize = node.getChildCount(); + for (int i = 0; i < childrenSize; i++) { + AccessibilityNodeInfo child = node.getChild(i); + counter[0]++; + traverseAccessibilityTree(child, counter); + } + } } |