fix AccessibilityBridgeMacDelegate to grab nswindow from appdelegate (flutter/engine#25698)

This commit is contained in:
chunhtai 2021-04-26 17:19:02 -07:00 committed by GitHub
parent 5c59e9637d
commit 5effcd39cd
7 changed files with 310 additions and 3 deletions

View File

@ -1111,6 +1111,7 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterView
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Info.plist
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegateTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm

View File

@ -167,6 +167,7 @@ executable("flutter_desktop_darwin_unittests") {
testonly = true
sources = [
"framework/Source/AccessibilityBridgeMacDelegateTest.mm",
"framework/Source/FlutterChannelKeyResponderUnittests.mm",
"framework/Source/FlutterEmbedderExternalTextureUnittests.mm",
"framework/Source/FlutterEmbedderKeyResponderUnittests.mm",

View File

@ -48,8 +48,8 @@ class AccessibilityBridgeMacDelegate : public AccessibilityBridge::Accessibility
/// accessibility notification system.
/// @param[in] native_node The event target, must not be nil.
/// @param[in] mac_notification The event name, must not be nil.
void DispatchMacOSNotification(gfx::NativeViewAccessible native_node,
NSAccessibilityNotificationName mac_notification);
virtual void DispatchMacOSNotification(gfx::NativeViewAccessible native_node,
NSAccessibilityNotificationName mac_notification);
//---------------------------------------------------------------------------
/// @brief Posts the given event against the given node with the

View File

@ -6,6 +6,7 @@
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
#include "flutter/shell/platform/embedder/embedder.h"
namespace flutter {
@ -26,6 +27,10 @@ AccessibilityBridgeMacDelegate::AccessibilityBridgeMacDelegate(__weak FlutterEng
void AccessibilityBridgeMacDelegate::OnAccessibilityEvent(
ui::AXEventGenerator::TargetedEvent targeted_event) {
if (!flutter_engine_.viewController.viewLoaded || !flutter_engine_.viewController.view.window) {
// We don't need to send accessibility events if the there is no view or window.
return;
}
ui::AXNode* ax_node = targeted_event.node;
std::vector<AccessibilityBridgeMacDelegate::NSAccessibilityEvent> events =
MacOSEventsFromAXEvent(targeted_event.event_params.event, *ax_node);
@ -265,9 +270,10 @@ AccessibilityBridgeMacDelegate::MacOSEventsFromAXEvent(ui::AXEventGenerator::Eve
case ui::AXEventGenerator::Event::CHILDREN_CHANGED: {
// NSAccessibilityCreatedNotification seems to be the only way to let
// Voiceover pick up layout changes.
NSCAssert(flutter_engine_.viewController, @"The viewController must not be nil");
events.push_back({
.name = NSAccessibilityCreatedNotification,
.target = [NSApp mainWindow],
.target = flutter_engine_.viewController.view.window,
.user_info = nil,
});
break;
@ -335,6 +341,9 @@ void AccessibilityBridgeMacDelegate::DispatchAccessibilityAction(ui::AXNode::AXI
FlutterSemanticsAction action,
const std::vector<uint8_t>& data) {
NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated");
NSCAssert(flutter_engine_.viewController.viewLoaded && flutter_engine_.viewController.view.window,
@"The accessibility bridge should not receive accessibility actions if the flutter view"
@"is not loaded or attached to a NSWindow.");
[flutter_engine_ dispatchSemanticsAction:action toTarget:target withData:data];
}

View File

@ -0,0 +1,194 @@
// 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.
#include "flutter/testing/testing.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
namespace flutter::testing {
namespace {
class AccessibilityBridgeMacDelegateSpy : public AccessibilityBridgeMacDelegate {
public:
AccessibilityBridgeMacDelegateSpy(__weak FlutterEngine* flutter_engine)
: AccessibilityBridgeMacDelegate(flutter_engine) {}
std::unordered_map<std::string, gfx::NativeViewAccessible> actual_notifications;
private:
void DispatchMacOSNotification(gfx::NativeViewAccessible native_node,
NSAccessibilityNotificationName mac_notification) override {
actual_notifications[[mac_notification UTF8String]] = native_node;
}
};
// Returns an engine configured for the text fixture resource configuration.
FlutterEngine* CreateTestEngine() {
NSString* fixtures = @(testing::GetFixturesPath());
FlutterDartProject* project = [[FlutterDartProject alloc]
initWithAssetsPath:fixtures
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true];
}
} // namespace
TEST(AccessibilityBridgeMacDelegateTest,
sendsAccessibilityCreateNotificationToWindowOfFlutterView) {
FlutterEngine* engine = CreateTestEngine();
NSString* fixtures = @(testing::GetFixturesPath());
FlutterDartProject* project = [[FlutterDartProject alloc]
initWithAssetsPath:fixtures
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
[viewController loadView];
[engine setViewController:viewController];
NSWindow* expectedTarget = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
expectedTarget.contentView = viewController.view;
// Setting up bridge so that the AccessibilityBridgeMacDelegateSpy
// can query semantics information from.
engine.semanticsEnabled = YES;
auto bridge = engine.accessibilityBridge.lock();
FlutterSemanticsNode root;
root.id = 0;
root.flags = static_cast<FlutterSemanticsFlag>(0);
root.actions = static_cast<FlutterSemanticsAction>(0);
root.text_selection_base = -1;
root.text_selection_extent = -1;
root.label = "root";
root.hint = "";
root.value = "";
root.increased_value = "";
root.decreased_value = "";
root.child_count = 0;
root.custom_accessibility_actions_count = 0;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
AccessibilityBridgeMacDelegateSpy spy(engine);
// Creates a targeted event.
ui::AXTree tree;
ui::AXNode ax_node(&tree, nullptr, 0, 0);
ui::AXNodeData node_data;
node_data.id = 0;
ax_node.SetData(node_data);
std::vector<ui::AXEventIntent> intent;
ui::AXEventGenerator::EventParams event_params(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
ax::mojom::EventFrom::kNone, intent);
ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params);
spy.OnAccessibilityEvent(targeted_event);
EXPECT_EQ(spy.actual_notifications.size(), 1u);
EXPECT_EQ(spy.actual_notifications.find([NSAccessibilityCreatedNotification UTF8String])->second,
expectedTarget);
[engine shutDownEngine];
}
TEST(AccessibilityBridgeMacDelegateTest, doesNotSendAccessibilityCreateNotificationWhenHeadless) {
FlutterEngine* engine = CreateTestEngine();
// Setting up bridge so that the AccessibilityBridgeMacDelegateSpy
// can query semantics information from.
engine.semanticsEnabled = YES;
auto bridge = engine.accessibilityBridge.lock();
FlutterSemanticsNode root;
root.id = 0;
root.flags = static_cast<FlutterSemanticsFlag>(0);
root.actions = static_cast<FlutterSemanticsAction>(0);
root.text_selection_base = -1;
root.text_selection_extent = -1;
root.label = "root";
root.hint = "";
root.value = "";
root.increased_value = "";
root.decreased_value = "";
root.child_count = 0;
root.custom_accessibility_actions_count = 0;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
AccessibilityBridgeMacDelegateSpy spy(engine);
// Creates a targeted event.
ui::AXTree tree;
ui::AXNode ax_node(&tree, nullptr, 0, 0);
ui::AXNodeData node_data;
node_data.id = 0;
ax_node.SetData(node_data);
std::vector<ui::AXEventIntent> intent;
ui::AXEventGenerator::EventParams event_params(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
ax::mojom::EventFrom::kNone, intent);
ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params);
spy.OnAccessibilityEvent(targeted_event);
// Does not send any notification if the engine is headless.
EXPECT_EQ(spy.actual_notifications.size(), 0u);
[engine shutDownEngine];
}
TEST(AccessibilityBridgeMacDelegateTest, doesNotSendAccessibilityCreateNotificationWhenNoWindow) {
FlutterEngine* engine = CreateTestEngine();
// Create a view controller without attaching it to a window.
NSString* fixtures = @(testing::GetFixturesPath());
FlutterDartProject* project = [[FlutterDartProject alloc]
initWithAssetsPath:fixtures
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
[viewController loadView];
[engine setViewController:viewController];
// Setting up bridge so that the AccessibilityBridgeMacDelegateSpy
// can query semantics information from.
engine.semanticsEnabled = YES;
auto bridge = engine.accessibilityBridge.lock();
FlutterSemanticsNode root;
root.id = 0;
root.flags = static_cast<FlutterSemanticsFlag>(0);
root.actions = static_cast<FlutterSemanticsAction>(0);
root.text_selection_base = -1;
root.text_selection_extent = -1;
root.label = "root";
root.hint = "";
root.value = "";
root.increased_value = "";
root.decreased_value = "";
root.child_count = 0;
root.custom_accessibility_actions_count = 0;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
AccessibilityBridgeMacDelegateSpy spy(engine);
// Creates a targeted event.
ui::AXTree tree;
ui::AXNode ax_node(&tree, nullptr, 0, 0);
ui::AXNodeData node_data;
node_data.id = 0;
ax_node.SetData(node_data);
std::vector<ui::AXEventIntent> intent;
ui::AXEventGenerator::EventParams event_params(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
ax::mojom::EventFrom::kNone, intent);
ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params);
spy.OnAccessibilityEvent(targeted_event);
// Does not send any notification if the flutter view is not attached to a NSWindow.
EXPECT_EQ(spy.actual_notifications.size(), 0u);
[engine shutDownEngine];
}
} // flutter::testing

View File

@ -156,4 +156,87 @@ TEST(FlutterEngine, CanToggleAccessibility) {
[engine shutDownEngine];
}
TEST(FlutterEngine, CanToggleAccessibilityWhenHeadless) {
FlutterEngine* engine = CreateTestEngine();
// Capture the update callbacks before the embedder API initializes.
auto original_init = engine.embedderAPI.Initialize;
std::function<void(const FlutterSemanticsNode*, void*)> update_node_callback;
std::function<void(const FlutterSemanticsCustomAction*, void*)> update_action_callback;
engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
Initialize, ([&update_action_callback, &update_node_callback, &original_init](
size_t version, const FlutterRendererConfig* config,
const FlutterProjectArgs* args, void* user_data, auto engine_out) {
update_node_callback = args->update_semantics_node_callback;
update_action_callback = args->update_semantics_custom_action_callback;
return original_init(version, config, args, user_data, engine_out);
}));
EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
// Enable the semantics without attaching a view controller.
bool enabled_called = false;
engine.embedderAPI.UpdateSemanticsEnabled =
MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
enabled_called = enabled;
return kSuccess;
}));
engine.semanticsEnabled = YES;
EXPECT_TRUE(enabled_called);
// Send flutter semantics updates.
FlutterSemanticsNode root;
root.id = 0;
root.flags = static_cast<FlutterSemanticsFlag>(0);
root.actions = static_cast<FlutterSemanticsAction>(0);
root.text_selection_base = -1;
root.text_selection_extent = -1;
root.label = "root";
root.hint = "";
root.value = "";
root.increased_value = "";
root.decreased_value = "";
root.child_count = 1;
int32_t children[] = {1};
root.children_in_traversal_order = children;
root.custom_accessibility_actions_count = 0;
update_node_callback(&root, (void*)CFBridgingRetain(engine));
FlutterSemanticsNode child1;
child1.id = 1;
child1.flags = static_cast<FlutterSemanticsFlag>(0);
child1.actions = static_cast<FlutterSemanticsAction>(0);
child1.text_selection_base = -1;
child1.text_selection_extent = -1;
child1.label = "child 1";
child1.hint = "";
child1.value = "";
child1.increased_value = "";
child1.decreased_value = "";
child1.child_count = 0;
child1.custom_accessibility_actions_count = 0;
update_node_callback(&child1, (void*)CFBridgingRetain(engine));
FlutterSemanticsNode node_batch_end;
node_batch_end.id = kFlutterSemanticsNodeIdBatchEnd;
update_node_callback(&node_batch_end, (void*)CFBridgingRetain(engine));
FlutterSemanticsCustomAction action_batch_end;
action_batch_end.id = kFlutterSemanticsNodeIdBatchEnd;
update_action_callback(&action_batch_end, (void*)CFBridgingRetain(engine));
// No crashes.
EXPECT_EQ(engine.viewController, nil);
// Disable the semantics.
bool semanticsEnabled = true;
engine.embedderAPI.UpdateSemanticsEnabled =
MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
semanticsEnabled = enabled;
return kSuccess;
}));
engine.semanticsEnabled = NO;
EXPECT_FALSE(semanticsEnabled);
// Still no crashes
EXPECT_EQ(engine.viewController, nil);
[engine shutDownEngine];
}
} // namespace flutter::testing

View File

@ -9,6 +9,7 @@
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
#include "flutter/third_party/accessibility/ax/ax_action_data.h"
@ -135,6 +136,22 @@ TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRan
TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
FlutterEngine* engine = CreateTestEngine();
// Set up view controller.
NSString* fixtures = @(testing::GetFixturesPath());
FlutterDartProject* project = [[FlutterDartProject alloc]
initWithAssetsPath:fixtures
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
[engine setViewController:viewController];
// Attach the view to a NSWindow.
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
window.contentView = viewController.view;
engine.semanticsEnabled = YES;
auto bridge = engine.accessibilityBridge.lock();
// Initialize ax node data.
@ -186,6 +203,8 @@ TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
EXPECT_EQ(called_action, FlutterSemanticsAction::kFlutterSemanticsActionTap);
EXPECT_EQ(called_id, 1u);
[engine setViewController:nil];
[engine shutDownEngine];
}