diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index fa1050e0c7f9..0f4cefd0a884 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1182,7 +1182,9 @@ public boolean onAccessibilityHoverEvent(MotionEvent event) { SemanticsNode semanticsNodeUnderCursor = getRootSemanticsNode().hitTest(new float[] {event.getX(), event.getY(), 0, 1}); - if (semanticsNodeUnderCursor.platformViewId != -1) { + // semanticsNodeUnderCursor can be null when hovering over non-flutter UI such as + // the Android navigation bar due to hitTest() bounds checking. + if (semanticsNodeUnderCursor != null && semanticsNodeUnderCursor.platformViewId != -1) { return accessibilityViewEmbedder.onAccessibilityHoverEvent( semanticsNodeUnderCursor.id, event); } diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 5bbc7b999a7d..7a3e4289be6f 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -13,6 +13,7 @@ import android.content.ContentResolver; import android.content.Context; +import android.view.MotionEvent; import android.view.View; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; @@ -130,6 +131,32 @@ public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() { assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); } + @Test + public void itHoverOverOutOfBoundsDoesNotCrash() { + // SementicsNode.hitTest() returns null when out of bounds. + AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class); + AccessibilityManager mockManager = mock(AccessibilityManager.class); + View mockRootView = mock(View.class); + Context context = mock(Context.class); + when(mockRootView.getContext()).thenReturn(context); + when(context.getPackageName()).thenReturn("test"); + AccessibilityBridge accessibilityBridge = + setUpBridge(mockRootView, mockManager, mockViewEmbedder); + + // Sent a11y tree with platform view. + TestSemanticsNode root = new TestSemanticsNode(); + root.id = 0; + TestSemanticsNode platformView = new TestSemanticsNode(); + platformView.id = 1; + platformView.platformViewId = 42; + root.children.add(platformView); + TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); + accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings); + + // Pass an out of bounds MotionEvent. + accessibilityBridge.onAccessibilityHoverEvent(MotionEvent.obtain(1, 1, 1, -10, -10, 0)); + } + AccessibilityBridge setUpBridge() { return setUpBridge(null, null, null, null, null, null); }