diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 8b363eec43fb2..4106afc533dd0 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -1253,6 +1253,182 @@ void PopulateAOTSnapshotMappingCallbacks( } } +// Translates engine semantic nodes to embedder semantic nodes. +FlutterSemanticsNode CreateEmbedderSemanticsNode( + const flutter::SemanticsNode& node) { + SkMatrix transform = node.transform.asM33(); + FlutterTransformation flutter_transform{ + transform.get(SkMatrix::kMScaleX), transform.get(SkMatrix::kMSkewX), + transform.get(SkMatrix::kMTransX), transform.get(SkMatrix::kMSkewY), + transform.get(SkMatrix::kMScaleY), transform.get(SkMatrix::kMTransY), + transform.get(SkMatrix::kMPersp0), transform.get(SkMatrix::kMPersp1), + transform.get(SkMatrix::kMPersp2)}; + return { + sizeof(FlutterSemanticsNode), + node.id, + static_cast(node.flags), + static_cast(node.actions), + node.textSelectionBase, + node.textSelectionExtent, + node.scrollChildren, + node.scrollIndex, + node.scrollPosition, + node.scrollExtentMax, + node.scrollExtentMin, + node.elevation, + node.thickness, + node.label.c_str(), + node.hint.c_str(), + node.value.c_str(), + node.increasedValue.c_str(), + node.decreasedValue.c_str(), + static_cast(node.textDirection), + FlutterRect{node.rect.fLeft, node.rect.fTop, node.rect.fRight, + node.rect.fBottom}, + flutter_transform, + node.childrenInTraversalOrder.size(), + node.childrenInTraversalOrder.data(), + node.childrenInHitTestOrder.data(), + node.customAccessibilityActions.size(), + node.customAccessibilityActions.data(), + node.platformViewId, + }; +} + +// Translates engine semantic custom actions to embedder semantic custom +// actions. +FlutterSemanticsCustomAction CreateEmbedderSemanticsCustomAction( + const flutter::CustomAccessibilityAction& action) { + return { + sizeof(FlutterSemanticsCustomAction), + action.id, + static_cast(action.overrideId), + action.label.c_str(), + action.hint.c_str(), + }; +} + +// Create a callback to notify the embedder of semantic updates +// using the new embedder callback 'update_semantics_callback'. +flutter::PlatformViewEmbedder::UpdateSemanticsCallback +CreateNewEmbedderSemanticsUpdateCallback( + FlutterUpdateSemanticsCallback update_semantics_callback, + void* user_data) { + return [update_semantics_callback, user_data]( + const flutter::SemanticsNodeUpdates& nodes, + const flutter::CustomAccessibilityActionUpdates& actions) { + std::vector embedder_nodes; + for (const auto& value : nodes) { + embedder_nodes.push_back(CreateEmbedderSemanticsNode(value.second)); + } + + std::vector embedder_custom_actions; + for (const auto& value : actions) { + embedder_custom_actions.push_back( + CreateEmbedderSemanticsCustomAction(value.second)); + } + + FlutterSemanticsUpdate update{ + .struct_size = sizeof(FlutterSemanticsUpdate), + .nodes_count = embedder_nodes.size(), + .nodes = embedder_nodes.data(), + .custom_actions_count = embedder_custom_actions.size(), + .custom_actions = embedder_custom_actions.data(), + }; + + update_semantics_callback(&update, user_data); + }; +} + +// Create a callback to notify the embedder of semantic updates +// using the legacy embedder callbacks 'update_semantics_node_callback' and +// 'update_semantics_custom_action_callback'. +flutter::PlatformViewEmbedder::UpdateSemanticsCallback +CreateLegacyEmbedderSemanticsUpdateCallback( + FlutterUpdateSemanticsNodeCallback update_semantics_node_callback, + FlutterUpdateSemanticsCustomActionCallback + update_semantics_custom_action_callback, + void* user_data) { + return [update_semantics_node_callback, + update_semantics_custom_action_callback, + user_data](const flutter::SemanticsNodeUpdates& nodes, + const flutter::CustomAccessibilityActionUpdates& actions) { + // First, queue all node and custom action updates. + if (update_semantics_node_callback != nullptr) { + for (const auto& value : nodes) { + const FlutterSemanticsNode embedder_node = + CreateEmbedderSemanticsNode(value.second); + update_semantics_node_callback(&embedder_node, user_data); + } + } + + if (update_semantics_custom_action_callback != nullptr) { + for (const auto& value : actions) { + const FlutterSemanticsCustomAction embedder_action = + CreateEmbedderSemanticsCustomAction(value.second); + update_semantics_custom_action_callback(&embedder_action, user_data); + } + } + + // Second, mark node and action batches completed now that all + // updates are queued. + if (update_semantics_node_callback != nullptr) { + const FlutterSemanticsNode batch_end_sentinel = { + sizeof(FlutterSemanticsNode), + kFlutterSemanticsNodeIdBatchEnd, + }; + update_semantics_node_callback(&batch_end_sentinel, user_data); + } + + if (update_semantics_custom_action_callback != nullptr) { + const FlutterSemanticsCustomAction batch_end_sentinel = { + sizeof(FlutterSemanticsCustomAction), + kFlutterSemanticsCustomActionIdBatchEnd, + }; + update_semantics_custom_action_callback(&batch_end_sentinel, user_data); + } + }; +} + +// Creates a callback that receives semantic updates from the engine +// and notifies the embedder's callback(s). Returns null if the embedder +// did not register any callbacks. +flutter::PlatformViewEmbedder::UpdateSemanticsCallback +CreateEmbedderSemanticsUpdateCallback(const FlutterProjectArgs* args, + void* user_data) { + // The embedder can register the new callback, or the legacy callbacks, or + // nothing at all. Handle the case where the embedder registered the 'new' + // callback. + if (SAFE_ACCESS(args, update_semantics_callback, nullptr) != nullptr) { + return CreateNewEmbedderSemanticsUpdateCallback( + args->update_semantics_callback, user_data); + } + + // Handle the case where the embedder registered 'legacy' callbacks. + FlutterUpdateSemanticsNodeCallback update_semantics_node_callback = nullptr; + if (SAFE_ACCESS(args, update_semantics_node_callback, nullptr) != nullptr) { + update_semantics_node_callback = args->update_semantics_node_callback; + } + + FlutterUpdateSemanticsCustomActionCallback + update_semantics_custom_action_callback = nullptr; + if (SAFE_ACCESS(args, update_semantics_custom_action_callback, nullptr) != + nullptr) { + update_semantics_custom_action_callback = + args->update_semantics_custom_action_callback; + } + + if (update_semantics_node_callback != nullptr || + update_semantics_custom_action_callback != nullptr) { + return CreateLegacyEmbedderSemanticsUpdateCallback( + update_semantics_node_callback, update_semantics_custom_action_callback, + user_data); + } + + // Handle the case where the embedder registered no callbacks. + return nullptr; +} + FlutterEngineResult FlutterEngineRun(size_t version, const FlutterRendererConfig* config, const FlutterProjectArgs* args, @@ -1400,113 +1576,20 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, settings.log_tag = SAFE_ACCESS(args, log_tag, nullptr); } - FlutterUpdateSemanticsNodeCallback update_semantics_node_callback = nullptr; - if (SAFE_ACCESS(args, update_semantics_node_callback, nullptr) != nullptr) { - update_semantics_node_callback = args->update_semantics_node_callback; - } - - FlutterUpdateSemanticsCustomActionCallback - update_semantics_custom_action_callback = nullptr; - if (SAFE_ACCESS(args, update_semantics_custom_action_callback, nullptr) != - nullptr) { - update_semantics_custom_action_callback = - args->update_semantics_custom_action_callback; + if (args->update_semantics_callback != nullptr && + (args->update_semantics_node_callback != nullptr || + args->update_semantics_custom_action_callback != nullptr)) { + return LOG_EMBEDDER_ERROR( + kInvalidArguments, + "Multiple semantics update callbacks provided. " + "Embedders should provide either `update_semantics_callback` " + "or both `update_semantics_nodes_callback` and " + "`update_semantics_custom_actions_callback`."); } flutter::PlatformViewEmbedder::UpdateSemanticsCallback - update_semantics_callback = nullptr; - if (update_semantics_node_callback != nullptr || - update_semantics_custom_action_callback != nullptr) { - update_semantics_callback = - [update_semantics_node_callback, - update_semantics_custom_action_callback, - user_data](const flutter::SemanticsNodeUpdates& update, - const flutter::CustomAccessibilityActionUpdates& actions) { - // First, queue all node and custom action updates. - if (update_semantics_node_callback != nullptr) { - for (const auto& value : update) { - const auto& node = value.second; - SkMatrix transform = node.transform.asM33(); - FlutterTransformation flutter_transform{ - transform.get(SkMatrix::kMScaleX), - transform.get(SkMatrix::kMSkewX), - transform.get(SkMatrix::kMTransX), - transform.get(SkMatrix::kMSkewY), - transform.get(SkMatrix::kMScaleY), - transform.get(SkMatrix::kMTransY), - transform.get(SkMatrix::kMPersp0), - transform.get(SkMatrix::kMPersp1), - transform.get(SkMatrix::kMPersp2)}; - const FlutterSemanticsNode embedder_node{ - sizeof(FlutterSemanticsNode), - node.id, - static_cast(node.flags), - static_cast(node.actions), - node.textSelectionBase, - node.textSelectionExtent, - node.scrollChildren, - node.scrollIndex, - node.scrollPosition, - node.scrollExtentMax, - node.scrollExtentMin, - node.elevation, - node.thickness, - node.label.c_str(), - node.hint.c_str(), - node.value.c_str(), - node.increasedValue.c_str(), - node.decreasedValue.c_str(), - static_cast(node.textDirection), - FlutterRect{node.rect.fLeft, node.rect.fTop, node.rect.fRight, - node.rect.fBottom}, - flutter_transform, - node.childrenInTraversalOrder.size(), - node.childrenInTraversalOrder.data(), - node.childrenInHitTestOrder.data(), - node.customAccessibilityActions.size(), - node.customAccessibilityActions.data(), - node.platformViewId, - node.tooltip.c_str(), - }; - update_semantics_node_callback(&embedder_node, user_data); - } - } - - if (update_semantics_custom_action_callback != nullptr) { - for (const auto& value : actions) { - const auto& action = value.second; - const FlutterSemanticsCustomAction embedder_action = { - sizeof(FlutterSemanticsCustomAction), - action.id, - static_cast(action.overrideId), - action.label.c_str(), - action.hint.c_str(), - }; - update_semantics_custom_action_callback(&embedder_action, - user_data); - } - } - - // Second, mark node and action batches completed now that all - // updates are queued. - if (update_semantics_node_callback != nullptr) { - const FlutterSemanticsNode batch_end_sentinel = { - sizeof(FlutterSemanticsNode), - kFlutterSemanticsNodeIdBatchEnd, - }; - update_semantics_node_callback(&batch_end_sentinel, user_data); - } - - if (update_semantics_custom_action_callback != nullptr) { - const FlutterSemanticsCustomAction batch_end_sentinel = { - sizeof(FlutterSemanticsCustomAction), - kFlutterSemanticsCustomActionIdBatchEnd, - }; - update_semantics_custom_action_callback(&batch_end_sentinel, - user_data); - } - }; - } + update_semantics_callback = + CreateEmbedderSemanticsUpdateCallback(args, user_data); flutter::PlatformViewEmbedder::PlatformMessageResponseCallback platform_message_response_callback = nullptr; diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index c9e7bc27bb5a5..7bef2e521be5d 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -1031,7 +1031,8 @@ typedef void (*FlutterDataCallback)(const uint8_t* /* data */, typedef int64_t FlutterPlatformViewIdentifier; /// `FlutterSemanticsNode` ID used as a sentinel to signal the end of a batch of -/// semantics node updates. +/// semantics node updates. This is unused if using +/// `FlutterUpdateSemanticsCallback`. FLUTTER_EXPORT extern const int32_t kFlutterSemanticsNodeIdBatchEnd; @@ -1040,7 +1041,7 @@ extern const int32_t kFlutterSemanticsNodeIdBatchEnd; /// The semantics tree is maintained during the semantics phase of the pipeline /// (i.e., during PipelineOwner.flushSemantics), which happens after /// compositing. Updates are then pushed to embedders via the registered -/// `FlutterUpdateSemanticsNodeCallback`. +/// `FlutterUpdateSemanticsCallback`. typedef struct { /// The size of this struct. Must be sizeof(FlutterSemanticsNode). size_t struct_size; @@ -1109,7 +1110,8 @@ typedef struct { } FlutterSemanticsNode; /// `FlutterSemanticsCustomAction` ID used as a sentinel to signal the end of a -/// batch of semantics custom action updates. +/// batch of semantics custom action updates. This is unused if using +/// `FlutterUpdateSemanticsCallback`. FLUTTER_EXPORT extern const int32_t kFlutterSemanticsCustomActionIdBatchEnd; @@ -1136,6 +1138,20 @@ typedef struct { const char* hint; } FlutterSemanticsCustomAction; +/// A batch of updates to semantics nodes and custom actions. +typedef struct { + /// The size of the struct. Must be sizeof(FlutterSemanticsUpdate). + size_t struct_size; + /// The number of semantics node updates. + size_t nodes_count; + // Array of semantics nodes. Has length `nodes_count`. + FlutterSemanticsNode* nodes; + /// The number of semantics custom action updates. + size_t custom_actions_count; + /// Array of semantics custom actions. Has length `custom_actions_count`. + FlutterSemanticsCustomAction* custom_actions; +} FlutterSemanticsUpdate; + typedef void (*FlutterUpdateSemanticsNodeCallback)( const FlutterSemanticsNode* /* semantics node */, void* /* user data */); @@ -1144,6 +1160,10 @@ typedef void (*FlutterUpdateSemanticsCustomActionCallback)( const FlutterSemanticsCustomAction* /* semantics custom action */, void* /* user data */); +typedef void (*FlutterUpdateSemanticsCallback)( + const FlutterSemanticsUpdate* /* semantics update */, + void* /* user data*/); + typedef struct _FlutterTaskRunner* FlutterTaskRunner; typedef struct { @@ -1739,24 +1759,32 @@ typedef struct { /// The callback invoked by the engine in root isolate scope. Called /// immediately after the root isolate has been created and marked runnable. VoidCallback root_isolate_create_callback; - /// The callback invoked by the engine in order to give the embedder the - /// chance to respond to semantics node updates from the Dart application. + /// The legacy callback invoked by the engine in order to give the embedder + /// the chance to respond to semantics node updates from the Dart application. /// Semantics node updates are sent in batches terminated by a 'batch end' /// callback that is passed a sentinel `FlutterSemanticsNode` whose `id` field /// has the value `kFlutterSemanticsNodeIdBatchEnd`. /// /// The callback will be invoked on the thread on which the `FlutterEngineRun` /// call is made. + /// + /// @deprecated Prefer using `update_semantics_callback` instead. If this + /// calback is provided, `update_semantics_callback` must not + /// be provided. FlutterUpdateSemanticsNodeCallback update_semantics_node_callback; - /// The callback invoked by the engine in order to give the embedder the - /// chance to respond to updates to semantics custom actions from the Dart - /// application. Custom action updates are sent in batches terminated by a + /// The legacy callback invoked by the engine in order to give the embedder + /// the chance to respond to updates to semantics custom actions from the Dart + /// application. Custom action updates are sent in batches terminated by a /// 'batch end' callback that is passed a sentinel /// `FlutterSemanticsCustomAction` whose `id` field has the value /// `kFlutterSemanticsCustomActionIdBatchEnd`. /// /// The callback will be invoked on the thread on which the `FlutterEngineRun` /// call is made. + /// + /// @deprecated Prefer using `update_semantics_callback` instead. If this + /// calback is provided, `update_semantics_callback` must not + /// be provided. FlutterUpdateSemanticsCustomActionCallback update_semantics_custom_action_callback; /// Path to a directory used to store data that is cached across runs of a @@ -1893,6 +1921,17 @@ typedef struct { // // The first argument is the `user_data` from `FlutterEngineInitialize`. OnPreEngineRestartCallback on_pre_engine_restart_callback; + + /// The callback invoked by the engine in order to give the embedder the + /// chance to respond to updates to semantics nodes and custom actions from + /// the Dart application. + /// + /// The callback will be invoked on the thread on which the `FlutterEngineRun` + /// call is made. + /// + /// If this callback is provided, update_semantics_node_callback and + /// update_semantics_custom_action_callback must not be provided. + FlutterUpdateSemanticsCallback update_semantics_callback; } FlutterProjectArgs; #ifndef FLUTTER_ENGINE_NO_PROTOTYPES @@ -2234,8 +2273,8 @@ FlutterEngineResult FlutterEngineMarkExternalTextureFrameAvailable( /// @param[in] engine A running engine instance. /// @param[in] enabled When enabled, changes to the semantic contents of the /// window are sent via the -/// `FlutterUpdateSemanticsNodeCallback` registered to -/// `update_semantics_node_callback` in +/// `FlutterUpdateSemanticsCallback` registered to +/// `update_semantics_callback` in /// `FlutterProjectArgs`. /// /// @return The result of the call. diff --git a/shell/platform/embedder/tests/embedder_a11y_unittests.cc b/shell/platform/embedder/tests/embedder_a11y_unittests.cc index 87f0c8f38e67d..21db35a31e12c 100644 --- a/shell/platform/embedder/tests/embedder_a11y_unittests.cc +++ b/shell/platform/embedder/tests/embedder_a11y_unittests.cc @@ -24,6 +24,21 @@ namespace testing { using EmbedderA11yTest = testing::EmbedderTest; +TEST_F(EmbedderTest, CannotProvideNewAndLegacySemanticsCallback) { + EmbedderConfigBuilder builder( + GetEmbedderContext(EmbedderTestContextType::kSoftwareContext)); + builder.SetSoftwareRendererConfig(); + builder.GetProjectArgs().update_semantics_callback = + [](const FlutterSemanticsUpdate* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_node_callback = + [](const FlutterSemanticsNode* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_custom_action_callback = + [](const FlutterSemanticsCustomAction* update, void* user_data) {}; + auto engine = builder.InitializeEngine(); + ASSERT_FALSE(engine.is_valid()); + engine.reset(); +} + TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { #if defined(OS_FUCHSIA) GTEST_SKIP() << "This test crashes on Fuchsia. https://fxbug.dev/87493 "; @@ -68,6 +83,35 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { notify_semantics_action_callback(args); }))); + fml::AutoResetWaitableEvent semantics_update_latch; + context.SetSemanticsUpdateCallback([&](const FlutterSemanticsUpdate* update) { + ASSERT_EQ(size_t(4), update->nodes_count); + ASSERT_EQ(size_t(1), update->custom_actions_count); + + for (size_t i = 0; i < update->nodes_count; i++) { + const FlutterSemanticsNode* node = update->nodes + i; + + ASSERT_EQ(1.0, node->transform.scaleX); + ASSERT_EQ(2.0, node->transform.skewX); + ASSERT_EQ(3.0, node->transform.transX); + ASSERT_EQ(4.0, node->transform.skewY); + ASSERT_EQ(5.0, node->transform.scaleY); + ASSERT_EQ(6.0, node->transform.transY); + ASSERT_EQ(7.0, node->transform.pers0); + ASSERT_EQ(8.0, node->transform.pers1); + ASSERT_EQ(9.0, node->transform.pers2); + + if (node->id == 128) { + ASSERT_EQ(0x3f3, node->platform_view_id); + } else { + ASSERT_NE(kFlutterSemanticsNodeIdBatchEnd, node->id); + ASSERT_EQ(0, node->platform_view_id); + } + } + + semantics_update_latch.Signal(); + }); + EmbedderConfigBuilder builder(context); builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("a11y_main"); @@ -124,6 +168,93 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { latch.Wait(); // Wait for UpdateSemantics callback on platform (current) thread. + latch.Wait(); + fml::MessageLoop::GetCurrent().RunExpiredTasksNow(); + semantics_update_latch.Wait(); + + // Dispatch a tap to semantics node 42. Wait for NotifySemanticsAction. + notify_semantics_action_callback = [&](Dart_NativeArguments args) { + int64_t node_id = 0; + Dart_GetNativeIntegerArgument(args, 0, &node_id); + ASSERT_EQ(42, node_id); + + int64_t action_id; + auto handle = Dart_GetNativeIntegerArgument(args, 1, &action_id); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), action_id); + + Dart_Handle semantic_args = Dart_GetNativeArgument(args, 2); + int64_t data; + Dart_Handle dart_int = Dart_ListGetAt(semantic_args, 0); + Dart_IntegerToInt64(dart_int, &data); + ASSERT_EQ(2, data); + + dart_int = Dart_ListGetAt(semantic_args, 1); + Dart_IntegerToInt64(dart_int, &data); + ASSERT_EQ(1, data); + latch.Signal(); + }; + std::vector bytes({2, 1}); + result = FlutterEngineDispatchSemanticsAction( + engine.get(), 42, kFlutterSemanticsActionTap, &bytes[0], bytes.size()); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + latch.Wait(); + + // Disable semantics. Wait for NotifySemanticsEnabled(false). + notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { + bool enabled = true; + Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(enabled); + latch.Signal(); + }; + result = FlutterEngineUpdateSemanticsEnabled(engine.get(), false); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + latch.Wait(); +} + +TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingLegacyCallbacks) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + fml::AutoResetWaitableEvent latch; + + // Called by the Dart text fixture on the UI thread to signal that the C++ + // unittest should resume. + context.AddNativeCallback( + "SignalNativeTest", CREATE_NATIVE_ENTRY(([&latch](Dart_NativeArguments) { + latch.Signal(); + }))); + + // Called by test fixture on UI thread to pass data back to this test. + NativeEntry notify_semantics_enabled_callback; + context.AddNativeCallback( + "NotifySemanticsEnabled", + CREATE_NATIVE_ENTRY( + ([¬ify_semantics_enabled_callback](Dart_NativeArguments args) { + ASSERT_NE(notify_semantics_enabled_callback, nullptr); + notify_semantics_enabled_callback(args); + }))); + + NativeEntry notify_accessibility_features_callback; + context.AddNativeCallback( + "NotifyAccessibilityFeatures", + CREATE_NATIVE_ENTRY(( + [¬ify_accessibility_features_callback](Dart_NativeArguments args) { + ASSERT_NE(notify_accessibility_features_callback, nullptr); + notify_accessibility_features_callback(args); + }))); + + NativeEntry notify_semantics_action_callback; + context.AddNativeCallback( + "NotifySemanticsAction", + CREATE_NATIVE_ENTRY( + ([¬ify_semantics_action_callback](Dart_NativeArguments args) { + ASSERT_NE(notify_semantics_action_callback, nullptr); + notify_semantics_action_callback(args); + }))); + + fml::AutoResetWaitableEvent semantics_node_latch; + fml::AutoResetWaitableEvent semantics_action_latch; + int node_batch_end_count = 0; int action_batch_end_count = 0; @@ -131,6 +262,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { context.SetSemanticsNodeCallback([&](const FlutterSemanticsNode* node) { if (node->id == kFlutterSemanticsNodeIdBatchEnd) { ++node_batch_end_count; + semantics_node_latch.Signal(); } else { // Batches should be completed after all nodes are received. ASSERT_EQ(0, node_batch_end_count); @@ -160,6 +292,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { [&](const FlutterSemanticsCustomAction* action) { if (action->id == kFlutterSemanticsCustomActionIdBatchEnd) { ++action_batch_end_count; + semantics_action_latch.Signal(); } else { // Batches should be completed after all actions are received. ASSERT_EQ(0, node_batch_end_count); @@ -169,8 +302,66 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { } }); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("a11y_main"); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Wait for initial NotifySemanticsEnabled(false). + notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { + bool enabled = true; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_FALSE(enabled); + latch.Signal(); + }; + latch.Wait(); + + // Prepare to NotifyAccessibilityFeatures call + fml::AutoResetWaitableEvent notify_features_latch; + notify_accessibility_features_callback = [&](Dart_NativeArguments args) { + bool enabled = true; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_FALSE(enabled); + notify_features_latch.Signal(); + }; + + // Enable semantics. Wait for NotifySemanticsEnabled(true). + notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { + bool enabled = false; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_TRUE(enabled); + latch.Signal(); + }; + auto result = FlutterEngineUpdateSemanticsEnabled(engine.get(), true); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + latch.Wait(); + + // Wait for initial accessibility features (reduce_motion == false) + notify_features_latch.Wait(); + + // Set accessibility features: (reduce_motion == true) + notify_accessibility_features_callback = [&](Dart_NativeArguments args) { + bool enabled = false; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_TRUE(enabled); + latch.Signal(); + }; + result = FlutterEngineUpdateAccessibilityFeatures( + engine.get(), kFlutterAccessibilityFeatureReduceMotion); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + latch.Wait(); + + // Wait for UpdateSemantics callback on platform (current) thread. latch.Wait(); fml::MessageLoop::GetCurrent().RunExpiredTasksNow(); + semantics_node_latch.Wait(); + semantics_action_latch.Wait(); ASSERT_EQ(4, node_count); ASSERT_EQ(1, node_batch_end_count); ASSERT_EQ(1, action_count); diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 92ff07a81291a..d61b5ffb63370 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -247,10 +247,12 @@ void EmbedderConfigBuilder::SetIsolateCreateCallbackHook() { } void EmbedderConfigBuilder::SetSemanticsCallbackHooks() { + project_args_.update_semantics_callback = + context_.GetUpdateSemanticsCallbackHook(); project_args_.update_semantics_node_callback = - EmbedderTestContext::GetUpdateSemanticsNodeCallbackHook(); + context_.GetUpdateSemanticsNodeCallbackHook(); project_args_.update_semantics_custom_action_callback = - EmbedderTestContext::GetUpdateSemanticsCustomActionCallbackHook(); + context_.GetUpdateSemanticsCustomActionCallbackHook(); } void EmbedderConfigBuilder::SetLogMessageCallbackHook() { diff --git a/shell/platform/embedder/tests/embedder_test_context.cc b/shell/platform/embedder/tests/embedder_test_context.cc index 261eb0f6b980f..904eed9cb77c5 100644 --- a/shell/platform/embedder/tests/embedder_test_context.cc +++ b/shell/platform/embedder/tests/embedder_test_context.cc @@ -121,6 +121,11 @@ void EmbedderTestContext::AddNativeCallback(const char* name, native_resolver_->AddNativeCallback({name}, function); } +void EmbedderTestContext::SetSemanticsUpdateCallback( + SemanticsUpdateCallback update_semantics_callback) { + update_semantics_callback_ = std::move(update_semantics_callback); +} + void EmbedderTestContext::SetSemanticsNodeCallback( SemanticsNodeCallback update_semantics_node_callback) { update_semantics_node_callback_ = std::move(update_semantics_node_callback); @@ -149,22 +154,44 @@ void EmbedderTestContext::SetLogMessageCallback( log_message_callback_ = callback; } +FlutterUpdateSemanticsCallback +EmbedderTestContext::GetUpdateSemanticsCallbackHook() { + if (update_semantics_callback_ == nullptr) { + return nullptr; + } + + return [](const FlutterSemanticsUpdate* update, void* user_data) { + auto context = reinterpret_cast(user_data); + if (context->update_semantics_callback_) { + context->update_semantics_callback_(update); + } + }; +} + FlutterUpdateSemanticsNodeCallback EmbedderTestContext::GetUpdateSemanticsNodeCallbackHook() { + if (update_semantics_node_callback_ == nullptr) { + return nullptr; + } + return [](const FlutterSemanticsNode* semantics_node, void* user_data) { auto context = reinterpret_cast(user_data); - if (auto callback = context->update_semantics_node_callback_) { - callback(semantics_node); + if (context->update_semantics_node_callback_) { + context->update_semantics_node_callback_(semantics_node); } }; } FlutterUpdateSemanticsCustomActionCallback EmbedderTestContext::GetUpdateSemanticsCustomActionCallbackHook() { + if (update_semantics_custom_action_callback_ == nullptr) { + return nullptr; + } + return [](const FlutterSemanticsCustomAction* action, void* user_data) { auto context = reinterpret_cast(user_data); - if (auto callback = context->update_semantics_custom_action_callback_) { - callback(action); + if (context->update_semantics_custom_action_callback_) { + context->update_semantics_custom_action_callback_(action); } }; } @@ -172,8 +199,8 @@ EmbedderTestContext::GetUpdateSemanticsCustomActionCallbackHook() { FlutterLogMessageCallback EmbedderTestContext::GetLogMessageCallbackHook() { return [](const char* tag, const char* message, void* user_data) { auto context = reinterpret_cast(user_data); - if (auto callback = context->log_message_callback_) { - callback(tag, message); + if (context->log_message_callback_) { + context->log_message_callback_(tag, message); } }; } diff --git a/shell/platform/embedder/tests/embedder_test_context.h b/shell/platform/embedder/tests/embedder_test_context.h index 8577abbdbb301..53c117447e467 100644 --- a/shell/platform/embedder/tests/embedder_test_context.h +++ b/shell/platform/embedder/tests/embedder_test_context.h @@ -24,6 +24,8 @@ namespace flutter { namespace testing { +using SemanticsUpdateCallback = + std::function; using SemanticsNodeCallback = std::function; using SemanticsActionCallback = std::function; @@ -69,6 +71,8 @@ class EmbedderTestContext { void AddIsolateCreateCallback(const fml::closure& closure); + void SetSemanticsUpdateCallback(SemanticsUpdateCallback update_semantics); + void AddNativeCallback(const char* name, Dart_NativeFunction function); void SetSemanticsNodeCallback(SemanticsNodeCallback update_semantics_node); @@ -120,6 +124,7 @@ class EmbedderTestContext { UniqueAOTData aot_data_; std::vector isolate_create_callbacks_; std::shared_ptr native_resolver_; + SemanticsUpdateCallback update_semantics_callback_; SemanticsNodeCallback update_semantics_node_callback_; SemanticsActionCallback update_semantics_custom_action_callback_; std::function platform_message_callback_; @@ -131,10 +136,11 @@ class EmbedderTestContext { static VoidCallback GetIsolateCreateCallbackHook(); - static FlutterUpdateSemanticsNodeCallback - GetUpdateSemanticsNodeCallbackHook(); + FlutterUpdateSemanticsCallback GetUpdateSemanticsCallbackHook(); + + FlutterUpdateSemanticsNodeCallback GetUpdateSemanticsNodeCallbackHook(); - static FlutterUpdateSemanticsCustomActionCallback + FlutterUpdateSemanticsCustomActionCallback GetUpdateSemanticsCustomActionCallbackHook(); static FlutterLogMessageCallback GetLogMessageCallbackHook(); diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 1007bbed80ac1..24b17a28ac6af 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -318,25 +318,23 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { auto host = static_cast(user_data); host->view()->OnPreEngineRestart(); }; - args.update_semantics_node_callback = [](const FlutterSemanticsNode* node, - void* user_data) { + args.update_semantics_callback = [](const FlutterSemanticsUpdate* update, + void* user_data) { auto host = static_cast(user_data); - if (!node || node->id == kFlutterSemanticsNodeIdBatchEnd) { - host->accessibility_bridge_->CommitUpdates(); - return; + + for (size_t i = 0; i < update->nodes_count; i++) { + const FlutterSemanticsNode* node = &update->nodes[i]; + host->accessibility_bridge_->AddFlutterSemanticsNodeUpdate(node); + } + + for (size_t i = 0; i < update->custom_actions_count; i++) { + const FlutterSemanticsCustomAction* action = &update->custom_actions[i]; + host->accessibility_bridge_->AddFlutterSemanticsCustomActionUpdate( + action); } - host->accessibility_bridge_->AddFlutterSemanticsNodeUpdate(node); + + host->accessibility_bridge_->CommitUpdates(); }; - args.update_semantics_custom_action_callback = - [](const FlutterSemanticsCustomAction* action, void* user_data) { - auto host = static_cast(user_data); - if (!action || action->id == kFlutterSemanticsNodeIdBatchEnd) { - host->accessibility_bridge_->CommitUpdates(); - return; - } - host->accessibility_bridge_->AddFlutterSemanticsCustomActionUpdate( - action); - }; args.root_isolate_create_callback = [](void* user_data) { auto host = static_cast(user_data); if (host->root_isolate_create_callback_) { diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc index a89322f0c51b9..5649d583d2b37 100644 --- a/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -77,6 +77,9 @@ TEST_F(FlutterWindowsEngineTest, RunDoesExpectedInitialization) { EXPECT_NE(args->custom_task_runners->thread_priority_setter, nullptr); EXPECT_EQ(args->custom_dart_entrypoint, nullptr); EXPECT_NE(args->vsync_callback, nullptr); + EXPECT_NE(args->update_semantics_callback, nullptr); + EXPECT_EQ(args->update_semantics_node_callback, nullptr); + EXPECT_EQ(args->update_semantics_custom_action_callback, nullptr); args->custom_task_runners->thread_priority_setter( FlutterThreadPriority::kRaster);