Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import static android.view.View.OnFocusChangeListener;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -21,6 +23,7 @@
* A view that applies the {@link io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack} to
* its children.
*/
@TargetApi(19)
public class FlutterMutatorView extends FrameLayout {
private FlutterMutatorsStack mutatorsStack;
private float screenDensity;
Expand Down Expand Up @@ -160,6 +163,21 @@ public boolean onInterceptTouchEvent(MotionEvent event) {
return true;
}

@Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
final View embeddedView = getChildAt(0);
if (embeddedView != null
&& embeddedView.getImportantForAccessibility()
== View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
// Forward the request only if the embedded view is in the Flutter accessibility tree.
// The embedded view may be ignored when the framework doesn't populate a SemanticNode
// for the current platform view.
// See AccessibilityBridge for more.
return super.requestSendAccessibilityEvent(child, event);
}

@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -242,6 +243,21 @@ public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
return true;
}

@Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
final View embeddedView = getChildAt(0);
if (embeddedView != null
&& embeddedView.getImportantForAccessibility()
== View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
// Forward the request only if the embedded view is in the Flutter accessibility tree.
// The embedded view may be ignored when the framework doesn't populate a SemanticNode
// for the current platform view.
// See AccessibilityBridge for more.
return super.requestSendAccessibilityEvent(child, event);
}

/** Used on Android O+, {@link invalidateChildInParent} used for previous versions. */
@SuppressLint("NewApi")
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,17 +223,28 @@ public long createForTextureLayer(
layoutParams.leftMargin = physicalLeft;
wrapperView.setLayoutParams(layoutParams);

final View view = platformView.getView();
if (view == null) {
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
} else if (view.getParent() != null) {
} else if (embeddedView.getParent() != null) {
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a parent view.");
}
view.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight));
view.setLayoutDirection(request.direction);
wrapperView.addView(view);
embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight));
embeddedView.setLayoutDirection(request.direction);

// Accessibility in the embedded view is initially disabled because if a Flutter app
// disabled accessibility in the first frame, the embedding won't receive an update to
// disable accessibility since the embedding never received an update to enable it.
// The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas
// when the framework sends a new a11y tree to the embedding.
// To prevent races, the framework populate the SemanticsNode after the platform view has
// been created.
embeddedView.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);

wrapperView.addView(embeddedView);
wrapperView.setOnDescendantFocusChangeListener(
(v, hasFocus) -> {
if (hasFocus) {
Expand Down Expand Up @@ -738,6 +749,7 @@ private void initializeRootImageViewIfNeeded() {
* testing.
*/
@VisibleForTesting
@TargetApi(Build.VERSION_CODES.KITKAT)
void initializePlatformViewIfNeeded(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Expand All @@ -747,11 +759,12 @@ void initializePlatformViewIfNeeded(int viewId) {
if (platformViewParent.get(viewId) != null) {
return;
}
if (platformView.getView() == null) {
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
}
if (platformView.getView().getParent() != null) {
if (embeddedView.getParent() != null) {
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a parent view.");
}
Expand All @@ -769,7 +782,17 @@ void initializePlatformViewIfNeeded(int viewId) {
});

platformViewParent.put(viewId, parentView);
parentView.addView(platformView.getView());

// Accessibility in the embedded view is initially disabled because if a Flutter app disabled
// accessibility in the first frame, the embedding won't receive an update to disable
// accessibility since the embedding never received an update to enable it.
// The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas when
// the framework sends a new a11y tree to the embedding.
// To prevent races, the framework populate the SemanticsNode after the platform view has been
// created.
embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);

parentView.addView(embeddedView);
flutterView.addView(parentView);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
import android.content.Context;
import android.graphics.Matrix;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.AndroidTouchProcessor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Config(manifest = Config.NONE)
@RunWith(AndroidJUnit4.class)
Expand Down Expand Up @@ -206,4 +211,54 @@ public ViewTreeObserver getViewTreeObserver() {
view.unsetOnDescendantFocusChangeListener();
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
}

@Test
@Config(
shadows = {
ShadowViewGroup.class,
})
public void ignoreAccessibilityEvents() {
final FlutterMutatorView wrapperView = new FlutterMutatorView(ctx);

final View embeddedView = mock(View.class);
wrapperView.addView(embeddedView);

when(embeddedView.getImportantForAccessibility())
.thenReturn(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
final boolean eventSent =
wrapperView.requestSendAccessibilityEvent(embeddedView, mock(AccessibilityEvent.class));
assertFalse(eventSent);
}

@Test
@Config(
shadows = {
ShadowViewGroup.class,
})
public void sendAccessibilityEvents() {
final FlutterMutatorView wrapperView = new FlutterMutatorView(ctx);

final View embeddedView = mock(View.class);
wrapperView.addView(embeddedView);

when(embeddedView.getImportantForAccessibility())
.thenReturn(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
boolean eventSent =
wrapperView.requestSendAccessibilityEvent(embeddedView, mock(AccessibilityEvent.class));
assertTrue(eventSent);

when(embeddedView.getImportantForAccessibility())
.thenReturn(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
eventSent =
wrapperView.requestSendAccessibilityEvent(embeddedView, mock(AccessibilityEvent.class));
assertTrue(eventSent);
}

@Implements(ViewGroup.class)
public static class ShadowViewGroup extends org.robolectric.shadows.ShadowView {
@Implementation
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@TargetApi(31)
Expand Down Expand Up @@ -295,6 +298,56 @@ public ViewTreeObserver getViewTreeObserver() {
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
}

@Test
@Config(
shadows = {
ShadowViewGroup.class,
})
public void ignoreAccessibilityEvents() {
final PlatformViewWrapper wrapperView = new PlatformViewWrapper(ctx);

final View embeddedView = mock(View.class);
wrapperView.addView(embeddedView);

when(embeddedView.getImportantForAccessibility())
.thenReturn(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
final boolean eventSent =
wrapperView.requestSendAccessibilityEvent(embeddedView, mock(AccessibilityEvent.class));
assertFalse(eventSent);
}

@Test
@Config(
shadows = {
ShadowViewGroup.class,
})
public void sendAccessibilityEvents() {
final PlatformViewWrapper wrapperView = new PlatformViewWrapper(ctx);

final View embeddedView = mock(View.class);
wrapperView.addView(embeddedView);

when(embeddedView.getImportantForAccessibility())
.thenReturn(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
boolean eventSent =
wrapperView.requestSendAccessibilityEvent(embeddedView, mock(AccessibilityEvent.class));
assertTrue(eventSent);

when(embeddedView.getImportantForAccessibility())
.thenReturn(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
eventSent =
wrapperView.requestSendAccessibilityEvent(embeddedView, mock(AccessibilityEvent.class));
assertTrue(eventSent);
}

@Implements(View.class)
public static class ShadowView {}

@Implements(ViewGroup.class)
public static class ShadowViewGroup extends org.robolectric.shadows.ShadowView {
@Implementation
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,33 @@ public void createPlatformViewMessage__setsAndroidViewSize() {
assertEquals(capturedLayoutParams.get(0).height, 1);
}

@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage__disablesAccessibility() {
PlatformViewsController platformViewsController = new PlatformViewsController();
platformViewsController.setSoftwareRendering(true);

int platformViewId = 0;
assertNull(platformViewsController.getPlatformViewById(platformViewId));

PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);

View androidView = mock(View.class);
when(platformView.getView()).thenReturn(androidView);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
platformViewsController.getRegistry().registerViewFactory("testType", viewFactory);

FlutterJNI jni = new FlutterJNI();
attach(jni, platformViewsController);

// Simulate create call from the framework.
createPlatformView(
jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false);
verify(androidView, times(1))
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}

@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage__throwsIfViewIsNull() {
Expand Down