未验证 提交 eaf70ca0 编写于 作者: C chunhtai 提交者: GitHub

Fixes inset padding in android accessibility bridge (#27083)

上级 3fd50a8b
......@@ -898,6 +898,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Virtual
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Predicate.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/ViewUtils.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterCallbackInformation.java
......
......@@ -241,6 +241,7 @@ android_java_sources = [
"io/flutter/util/PathUtils.java",
"io/flutter/util/Preconditions.java",
"io/flutter/util/Predicate.java",
"io/flutter/util/ViewUtils.java",
"io/flutter/view/AccessibilityBridge.java",
"io/flutter/view/AccessibilityViewEmbedder.java",
"io/flutter/view/FlutterCallbackInformation.java",
......@@ -504,6 +505,7 @@ action("robolectric_tests") {
"test/io/flutter/plugins/GeneratedPluginRegistrant.java",
"test/io/flutter/util/FakeKeyEvent.java",
"test/io/flutter/util/PreconditionsTest.java",
"test/io/flutter/util/ViewUtilsTest.java",
"test/io/flutter/view/AccessibilityBridgeTest.java",
]
......
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.util;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
public final class ViewUtils {
/**
* Retrieves the {@link Activity} from a given {@link Context}.
*
* <p>This method will recursively traverse up the context chain if it is a {@link ContextWrapper}
* until it finds the first instance of the base context that is an {@link Activity}.
*/
public static Activity getActivity(Context context) {
if (context == null) {
return null;
}
if (context instanceof Activity) {
return (Activity) context;
}
if (context instanceof ContextWrapper) {
// Recurse up chain of base contexts until we find an Activity.
return getActivity(((ContextWrapper) context).getBaseContext());
}
return null;
}
}
......@@ -6,7 +6,9 @@ package io.flutter.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.graphics.Rect;
import android.net.Uri;
......@@ -22,6 +24,7 @@ import android.text.style.TtsSpan;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
......@@ -35,6 +38,7 @@ import io.flutter.Log;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
import io.flutter.util.Predicate;
import io.flutter.util.ViewUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
......@@ -1507,18 +1511,29 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (rootObject != null) {
final float[] identity = new float[16];
Matrix.setIdentityM(identity, 0);
// in android devices API 23 and above, the system nav bar can be placed on the left side
// In Android devices API 23 and above, the system nav bar can be placed on the left side
// of the screen in landscape mode. We must handle the translation ourselves for the
// a11y nodes.
if (Build.VERSION.SDK_INT >= 23) {
WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
if (insets != null) {
if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
rootObject.globalGeometryDirty = true;
rootObject.inverseTransformDirty = true;
boolean needsToApplyLeftCutoutInset = true;
// In Android devices API 28 and above, the `layoutInDisplayCutoutMode` window attribute
// can be set to allow overlapping content within the cutout area. Query the attribute
// to figure out whether the content overlaps with the cutout and decide whether to
// apply cutout inset.
if (Build.VERSION.SDK_INT >= 28) {
needsToApplyLeftCutoutInset = doesLayoutInDisplayCutoutModeRequireLeftInset();
}
if (needsToApplyLeftCutoutInset) {
WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
if (insets != null) {
if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
rootObject.globalGeometryDirty = true;
rootObject.inverseTransformDirty = true;
}
lastLeftFrameInset = insets.getSystemWindowInsetLeft();
Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
}
lastLeftFrameInset = insets.getSystemWindowInsetLeft();
Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
}
}
rootObject.updateRecursively(identity, visitedObjects, false);
......@@ -1822,6 +1837,29 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
return event;
}
/**
* Reads the {@code layoutInDisplayCutoutMode} value from the window attribute and returns whether
* a left cutout inset is required.
*
* <p>The {@code layoutInDisplayCutoutMode} is added after API level 28.
*/
@TargetApi(28)
@RequiresApi(28)
private boolean doesLayoutInDisplayCutoutModeRequireLeftInset() {
Context context = rootAccessibilityView.getContext();
Activity activity = ViewUtils.getActivity(context);
if (activity == null || activity.getWindow() == null) {
// The activity is not visible, it does not matter whether to apply left inset
// or not.
return false;
}
int layoutInDisplayCutoutMode = activity.getWindow().getAttributes().layoutInDisplayCutoutMode;
return layoutInDisplayCutoutMode
== WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|| layoutInDisplayCutoutMode
== WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
}
/**
* Hook called just before a {@link SemanticsNode} is removed from the Android cache of Flutter's
* semantics tree.
......
......@@ -8,7 +8,6 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Insets;
......@@ -65,6 +64,7 @@ import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.mouse.MouseCursorPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.util.ViewUtils;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
......@@ -162,7 +162,7 @@ public class FlutterView extends SurfaceView
public FlutterView(Context context, AttributeSet attrs, FlutterNativeView nativeView) {
super(context, attrs);
Activity activity = getActivity(getContext());
Activity activity = ViewUtils.getActivity(getContext());
if (activity == null) {
throw new IllegalArgumentException("Bad context");
}
......@@ -257,20 +257,6 @@ public class FlutterView extends SurfaceView
sendUserPlatformSettingsToDart();
}
private static Activity getActivity(Context context) {
if (context == null) {
return null;
}
if (context instanceof Activity) {
return (Activity) context;
}
if (context instanceof ContextWrapper) {
// Recurse up chain of base contexts until we find an Activity.
return getActivity(((ContextWrapper) context).getBaseContext());
}
return null;
}
@NonNull
public DartExecutor getDartExecutor() {
return dartExecutor;
......
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.util;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
public class ViewUtilsTest {
@Test
public void canGetActivity() {
// Non activity context returns null
Context nonActivityContext = mock(Context.class);
assertEquals(null, ViewUtils.getActivity(nonActivityContext));
Activity activity = mock(Activity.class);
assertEquals(activity, ViewUtils.getActivity(activity));
ContextWrapper wrapper = new ContextWrapper(new ContextWrapper(activity));
assertEquals(activity, ViewUtils.getActivity(wrapper));
}
}
......@@ -20,6 +20,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Rect;
......@@ -30,6 +31,9 @@ import android.text.style.TtsSpan;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
......@@ -279,6 +283,200 @@ public class AccessibilityBridgeTest {
verify(mockNodeInfo2, times(1)).setTraversalAfter(eq(mockRootView), eq(1));
}
@TargetApi(28)
@Test
public void itSetCutoutInsetBasedonLayoutModeNever() {
int expectedInsetLeft = 5;
int top = 0;
int left = 0;
int right = 100;
int bottom = 200;
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
View mockRootView = mock(View.class);
Activity context = mock(Activity.class);
Window window = mock(Window.class);
WindowInsets insets = mock(WindowInsets.class);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
when(mockRootView.getContext()).thenReturn(context);
when(context.getWindow()).thenReturn(window);
when(window.getAttributes()).thenReturn(layoutParams);
when(mockRootView.getRootWindowInsets()).thenReturn(insets);
when(insets.getSystemWindowInsetLeft()).thenReturn(expectedInsetLeft);
when(context.getPackageName()).thenReturn("test");
AccessibilityBridge accessibilityBridge =
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.left = left;
root.top = top;
root.right = right;
root.bottom = bottom;
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockNodeInfo, times(1))
.setBoundsInScreen(
new Rect(left + expectedInsetLeft, top, right + expectedInsetLeft, bottom));
}
@TargetApi(28)
@Test
public void itSetCutoutInsetBasedonLayoutModeDefault() {
int expectedInsetLeft = 5;
int top = 0;
int left = 0;
int right = 100;
int bottom = 200;
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
View mockRootView = mock(View.class);
Activity context = mock(Activity.class);
Window window = mock(Window.class);
WindowInsets insets = mock(WindowInsets.class);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
when(mockRootView.getContext()).thenReturn(context);
when(context.getWindow()).thenReturn(window);
when(window.getAttributes()).thenReturn(layoutParams);
when(mockRootView.getRootWindowInsets()).thenReturn(insets);
when(insets.getSystemWindowInsetLeft()).thenReturn(expectedInsetLeft);
when(context.getPackageName()).thenReturn("test");
AccessibilityBridge accessibilityBridge =
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.left = left;
root.top = top;
root.right = right;
root.bottom = bottom;
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockNodeInfo, times(1))
.setBoundsInScreen(
new Rect(left + expectedInsetLeft, top, right + expectedInsetLeft, bottom));
}
@TargetApi(28)
@Test
public void itSetCutoutInsetBasedonLayoutModeShortEdges() {
int expectedInsetLeft = 5;
int top = 0;
int left = 0;
int right = 100;
int bottom = 200;
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
View mockRootView = mock(View.class);
Activity context = mock(Activity.class);
Window window = mock(Window.class);
WindowInsets insets = mock(WindowInsets.class);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
when(mockRootView.getContext()).thenReturn(context);
when(context.getWindow()).thenReturn(window);
when(window.getAttributes()).thenReturn(layoutParams);
when(mockRootView.getRootWindowInsets()).thenReturn(insets);
when(insets.getSystemWindowInsetLeft()).thenReturn(expectedInsetLeft);
when(context.getPackageName()).thenReturn("test");
AccessibilityBridge accessibilityBridge =
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.left = left;
root.top = top;
root.right = right;
root.bottom = bottom;
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
// Does not apply left inset if the layout mode is `short edges`.
verify(mockNodeInfo, times(1)).setBoundsInScreen(new Rect(left, top, right, bottom));
}
@TargetApi(30)
@Test
public void itSetCutoutInsetBasedonLayoutModeAlways() {
int expectedInsetLeft = 5;
int top = 0;
int left = 0;
int right = 100;
int bottom = 200;
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
View mockRootView = mock(View.class);
Activity context = mock(Activity.class);
Window window = mock(Window.class);
WindowInsets insets = mock(WindowInsets.class);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
when(mockRootView.getContext()).thenReturn(context);
when(context.getWindow()).thenReturn(window);
when(window.getAttributes()).thenReturn(layoutParams);
when(mockRootView.getRootWindowInsets()).thenReturn(insets);
when(insets.getSystemWindowInsetLeft()).thenReturn(expectedInsetLeft);
when(context.getPackageName()).thenReturn("test");
AccessibilityBridge accessibilityBridge =
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.left = left;
root.top = top;
root.right = right;
root.bottom = bottom;
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
// Does not apply left inset if the layout mode is `always`.
verify(mockNodeInfo, times(1)).setBoundsInScreen(new Rect(left, top, right, bottom));
}
@Test
public void itIgnoresUnfocusableNodeDuringHitTest() {
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
......@@ -1235,6 +1433,13 @@ public class AccessibilityBridgeTest {
float top = 0.0f;
float right = 0.0f;
float bottom = 0.0f;
float[] transform =
new float[] {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
final List<TestSemanticsNode> children = new ArrayList<TestSemanticsNode>();
public void addChild(TestSemanticsNode child) {
......@@ -1281,7 +1486,7 @@ public class AccessibilityBridgeTest {
bytes.putFloat(bottom);
// transform.
for (int i = 0; i < 16; i++) {
bytes.putFloat(0);
bytes.putFloat(transform[i]);
}
// children in traversal order.
bytes.putInt(children.size());
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册