Skip to content

Commit eaf70ca

Browse files
authored
Fixes inset padding in android accessibility bridge (flutter#27083)
1 parent 3fd50a8 commit eaf70ca

File tree

7 files changed

+321
-25
lines changed

7 files changed

+321
-25
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Virtual
898898
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
899899
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java
900900
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Predicate.java
901+
FILE: ../../../flutter/shell/platform/android/io/flutter/util/ViewUtils.java
901902
FILE: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java
902903
FILE: ../../../flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java
903904
FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterCallbackInformation.java

shell/platform/android/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ android_java_sources = [
241241
"io/flutter/util/PathUtils.java",
242242
"io/flutter/util/Preconditions.java",
243243
"io/flutter/util/Predicate.java",
244+
"io/flutter/util/ViewUtils.java",
244245
"io/flutter/view/AccessibilityBridge.java",
245246
"io/flutter/view/AccessibilityViewEmbedder.java",
246247
"io/flutter/view/FlutterCallbackInformation.java",
@@ -504,6 +505,7 @@ action("robolectric_tests") {
504505
"test/io/flutter/plugins/GeneratedPluginRegistrant.java",
505506
"test/io/flutter/util/FakeKeyEvent.java",
506507
"test/io/flutter/util/PreconditionsTest.java",
508+
"test/io/flutter/util/ViewUtilsTest.java",
507509
"test/io/flutter/view/AccessibilityBridgeTest.java",
508510
]
509511

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.util;
6+
7+
import android.app.Activity;
8+
import android.content.Context;
9+
import android.content.ContextWrapper;
10+
11+
public final class ViewUtils {
12+
/**
13+
* Retrieves the {@link Activity} from a given {@link Context}.
14+
*
15+
* <p>This method will recursively traverse up the context chain if it is a {@link ContextWrapper}
16+
* until it finds the first instance of the base context that is an {@link Activity}.
17+
*/
18+
public static Activity getActivity(Context context) {
19+
if (context == null) {
20+
return null;
21+
}
22+
if (context instanceof Activity) {
23+
return (Activity) context;
24+
}
25+
if (context instanceof ContextWrapper) {
26+
// Recurse up chain of base contexts until we find an Activity.
27+
return getActivity(((ContextWrapper) context).getBaseContext());
28+
}
29+
return null;
30+
}
31+
}

shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import android.annotation.SuppressLint;
88
import android.annotation.TargetApi;
9+
import android.app.Activity;
910
import android.content.ContentResolver;
11+
import android.content.Context;
1012
import android.database.ContentObserver;
1113
import android.graphics.Rect;
1214
import android.net.Uri;
@@ -22,6 +24,7 @@
2224
import android.view.MotionEvent;
2325
import android.view.View;
2426
import android.view.WindowInsets;
27+
import android.view.WindowManager;
2528
import android.view.accessibility.AccessibilityEvent;
2629
import android.view.accessibility.AccessibilityManager;
2730
import android.view.accessibility.AccessibilityNodeInfo;
@@ -35,6 +38,7 @@
3538
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
3639
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
3740
import io.flutter.util.Predicate;
41+
import io.flutter.util.ViewUtils;
3842
import java.nio.ByteBuffer;
3943
import java.nio.ByteOrder;
4044
import java.nio.charset.Charset;
@@ -1507,18 +1511,29 @@ void updateSemantics(
15071511
if (rootObject != null) {
15081512
final float[] identity = new float[16];
15091513
Matrix.setIdentityM(identity, 0);
1510-
// in android devices API 23 and above, the system nav bar can be placed on the left side
1514+
// In Android devices API 23 and above, the system nav bar can be placed on the left side
15111515
// of the screen in landscape mode. We must handle the translation ourselves for the
15121516
// a11y nodes.
15131517
if (Build.VERSION.SDK_INT >= 23) {
1514-
WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
1515-
if (insets != null) {
1516-
if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
1517-
rootObject.globalGeometryDirty = true;
1518-
rootObject.inverseTransformDirty = true;
1518+
boolean needsToApplyLeftCutoutInset = true;
1519+
// In Android devices API 28 and above, the `layoutInDisplayCutoutMode` window attribute
1520+
// can be set to allow overlapping content within the cutout area. Query the attribute
1521+
// to figure out whether the content overlaps with the cutout and decide whether to
1522+
// apply cutout inset.
1523+
if (Build.VERSION.SDK_INT >= 28) {
1524+
needsToApplyLeftCutoutInset = doesLayoutInDisplayCutoutModeRequireLeftInset();
1525+
}
1526+
1527+
if (needsToApplyLeftCutoutInset) {
1528+
WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
1529+
if (insets != null) {
1530+
if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
1531+
rootObject.globalGeometryDirty = true;
1532+
rootObject.inverseTransformDirty = true;
1533+
}
1534+
lastLeftFrameInset = insets.getSystemWindowInsetLeft();
1535+
Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
15191536
}
1520-
lastLeftFrameInset = insets.getSystemWindowInsetLeft();
1521-
Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
15221537
}
15231538
}
15241539
rootObject.updateRecursively(identity, visitedObjects, false);
@@ -1822,6 +1837,29 @@ private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int event
18221837
return event;
18231838
}
18241839

1840+
/**
1841+
* Reads the {@code layoutInDisplayCutoutMode} value from the window attribute and returns whether
1842+
* a left cutout inset is required.
1843+
*
1844+
* <p>The {@code layoutInDisplayCutoutMode} is added after API level 28.
1845+
*/
1846+
@TargetApi(28)
1847+
@RequiresApi(28)
1848+
private boolean doesLayoutInDisplayCutoutModeRequireLeftInset() {
1849+
Context context = rootAccessibilityView.getContext();
1850+
Activity activity = ViewUtils.getActivity(context);
1851+
if (activity == null || activity.getWindow() == null) {
1852+
// The activity is not visible, it does not matter whether to apply left inset
1853+
// or not.
1854+
return false;
1855+
}
1856+
int layoutInDisplayCutoutMode = activity.getWindow().getAttributes().layoutInDisplayCutoutMode;
1857+
return layoutInDisplayCutoutMode
1858+
== WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
1859+
|| layoutInDisplayCutoutMode
1860+
== WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
1861+
}
1862+
18251863
/**
18261864
* Hook called just before a {@link SemanticsNode} is removed from the Android cache of Flutter's
18271865
* semantics tree.

shell/platform/android/io/flutter/view/FlutterView.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import android.annotation.TargetApi;
99
import android.app.Activity;
1010
import android.content.Context;
11-
import android.content.ContextWrapper;
1211
import android.content.res.Configuration;
1312
import android.graphics.Bitmap;
1413
import android.graphics.Insets;
@@ -65,6 +64,7 @@
6564
import io.flutter.plugin.mouse.MouseCursorPlugin;
6665
import io.flutter.plugin.platform.PlatformPlugin;
6766
import io.flutter.plugin.platform.PlatformViewsController;
67+
import io.flutter.util.ViewUtils;
6868
import java.nio.ByteBuffer;
6969
import java.util.ArrayList;
7070
import java.util.List;
@@ -162,7 +162,7 @@ public FlutterView(Context context, AttributeSet attrs) {
162162
public FlutterView(Context context, AttributeSet attrs, FlutterNativeView nativeView) {
163163
super(context, attrs);
164164

165-
Activity activity = getActivity(getContext());
165+
Activity activity = ViewUtils.getActivity(getContext());
166166
if (activity == null) {
167167
throw new IllegalArgumentException("Bad context");
168168
}
@@ -257,20 +257,6 @@ public void onPostResume() {
257257
sendUserPlatformSettingsToDart();
258258
}
259259

260-
private static Activity getActivity(Context context) {
261-
if (context == null) {
262-
return null;
263-
}
264-
if (context instanceof Activity) {
265-
return (Activity) context;
266-
}
267-
if (context instanceof ContextWrapper) {
268-
// Recurse up chain of base contexts until we find an Activity.
269-
return getActivity(((ContextWrapper) context).getBaseContext());
270-
}
271-
return null;
272-
}
273-
274260
@NonNull
275261
public DartExecutor getDartExecutor() {
276262
return dartExecutor;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.util;
6+
7+
import static org.junit.Assert.assertEquals;
8+
import static org.mockito.Mockito.mock;
9+
10+
import android.app.Activity;
11+
import android.content.Context;
12+
import android.content.ContextWrapper;
13+
import org.junit.Test;
14+
import org.junit.runner.RunWith;
15+
import org.robolectric.RobolectricTestRunner;
16+
import org.robolectric.annotation.Config;
17+
18+
@Config(manifest = Config.NONE)
19+
@RunWith(RobolectricTestRunner.class)
20+
public class ViewUtilsTest {
21+
@Test
22+
public void canGetActivity() {
23+
// Non activity context returns null
24+
Context nonActivityContext = mock(Context.class);
25+
assertEquals(null, ViewUtils.getActivity(nonActivityContext));
26+
27+
Activity activity = mock(Activity.class);
28+
assertEquals(activity, ViewUtils.getActivity(activity));
29+
30+
ContextWrapper wrapper = new ContextWrapper(new ContextWrapper(activity));
31+
assertEquals(activity, ViewUtils.getActivity(wrapper));
32+
}
33+
}

0 commit comments

Comments
 (0)