From 26037dff87ade9d9dee04cc2ef90b3fe5cf0faf3 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:11:13 -0700 Subject: [PATCH] Add set semantics enabled API and wire iOS a11y bridge (#161265) fixes https://github.com/flutter/flutter/issues/158399 old pr https://github.com/flutter/engine/pull/56691 previously the only correct way to enable semantics is that ios embedding receive signal from native OS, it call SetSemanticsEnabled to shell and then to dart to enable semantics tree generation. If for some reason framework decide to enable semantics first, e.g. through SemanticsBinding.instance.ensureSemantics(typically due to integration test or ci that wants to test semantics), the update will be dropped in shell. Even if it later on receive signal from native OS to turn on semantics, it can't construct the complete accessibility tree in embedding because the updatesemantics sends diff update and previous updates are gone forever. It will end up in a broken state. This pr changes so that the only source of truth will be in the framework side. When framework starts generating the the semantics tree, it will call SetSemanticsTreeEnabled through dart:ui, and the embedding needs to prepare itself to accept semantics update after receiving the message. This however require some refactoring on iOS embedding because it will only create a11y bridge when receiving OS notification when assitive technologies turns on. This requires three phase transition add an empty dart:ui API setSemanticsTreeEnabled makes framework calls the empty API. merge this pr with actual implementation of setSemanticsTreeEnabled I will do the android part in a separate pr ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../ios_host_app/flutterapp/lib/main | 1 + .../flutter/ci/licenses_golden/excluded_files | 1 + .../ci/licenses_golden/licenses_flutter | 2 + engine/src/flutter/lib/ui/dart_ui.cc | 1 + .../flutter/lib/ui/platform_dispatcher.dart | 20 +++ engine/src/flutter/lib/ui/window.dart | 5 +- .../lib/ui/window/platform_configuration.cc | 8 + .../lib/ui/window/platform_configuration.h | 9 ++ .../lib/web_ui/lib/platform_dispatcher.dart | 2 + .../lib/src/engine/platform_dispatcher.dart | 9 ++ engine/src/flutter/runtime/BUILD.gn | 2 + .../flutter/runtime/dart_isolate_unittests.cc | 1 + .../runtime/fixtures/runtime_test.dart | 101 ++++++++++++ .../src/flutter/runtime/runtime_controller.cc | 10 +- .../src/flutter/runtime/runtime_controller.h | 9 +- .../runtime/runtime_controller_unittests.cc | 149 ++++++++++++++++++ engine/src/flutter/runtime/runtime_delegate.h | 2 + engine/src/flutter/shell/common/engine.cc | 4 + engine/src/flutter/shell/common/engine.h | 17 ++ .../shell/common/engine_animator_unittests.cc | 1 + .../flutter/shell/common/engine_unittests.cc | 2 + .../src/flutter/shell/common/platform_view.cc | 4 + .../src/flutter/shell/common/platform_view.h | 9 ++ engine/src/flutter/shell/common/shell.cc | 14 ++ engine/src/flutter/shell/common/shell.h | 3 + .../shell/platform/darwin/ios/BUILD.gn | 1 + .../platform/darwin/ios/platform_view_ios.h | 28 ++-- .../platform/darwin/ios/platform_view_ios.mm | 63 +++----- .../darwin/ios/platform_view_ios_test.mm | 105 ++++++++++++ .../platform/embedder/fixtures/main.dart | 3 + .../shell/platform/windows/fixtures/main.dart | 1 + .../lib/src/locale_initialization.dart | 2 +- .../flutter/lib/src/semantics/binding.dart | 5 + ...nding_set_semantics_tree_enabled_test.dart | 35 ++++ 34 files changed, 557 insertions(+), 72 deletions(-) create mode 100644 engine/src/flutter/runtime/runtime_controller_unittests.cc create mode 100644 engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm create mode 100644 packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart diff --git a/dev/integration_tests/ios_host_app/flutterapp/lib/main b/dev/integration_tests/ios_host_app/flutterapp/lib/main index ec78451126b..87557d41728 100644 --- a/dev/integration_tests/ios_host_app/flutterapp/lib/main +++ b/dev/integration_tests/ios_host_app/flutterapp/lib/main @@ -32,6 +32,7 @@ const BasicMessageChannel _kReloadChannel = void main() { // Ensures bindings are initialized before doing anything. WidgetsFlutterBinding.ensureInitialized(); + ui.PlatformDispatcher.instance.setSemanticsTreeEnabled(true); // Start listening immediately for messages from the iOS side. ObjC calls // will be made to let us know when we should be changing the app state. _kReloadChannel.setMessageHandler(run); diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index a7b219ad069..711ca379ea2 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -283,6 +283,7 @@ ../../../flutter/runtime/fixtures ../../../flutter/runtime/no_dart_plugin_registrant_unittests.cc ../../../flutter/runtime/platform_isolate_manager_unittests.cc +../../../flutter/runtime/runtime_controller_unittests.cc ../../../flutter/runtime/type_conversions_unittests.cc ../../../flutter/shell/common/animator_unittests.cc ../../../flutter/shell/common/base64_unittests.cc diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 000285ab84e..89bb8ecb09b 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -52880,6 +52880,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios. ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h + ../../../flutter/LICENSE @@ -55876,6 +55877,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios.mm FILE: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.h FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm +FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h diff --git a/engine/src/flutter/lib/ui/dart_ui.cc b/engine/src/flutter/lib/ui/dart_ui.cc index 7d1864308ca..292b7e30537 100644 --- a/engine/src/flutter/lib/ui/dart_ui.cc +++ b/engine/src/flutter/lib/ui/dart_ui.cc @@ -99,6 +99,7 @@ typedef CanvasPath Path; V(PlatformConfigurationNativeApi::UpdateSemantics) \ V(PlatformConfigurationNativeApi::SetNeedsReportTimings) \ V(PlatformConfigurationNativeApi::SetIsolateDebugName) \ + V(PlatformConfigurationNativeApi::SetSemanticsTreeEnabled) \ V(PlatformConfigurationNativeApi::RequestDartPerformanceMode) \ V(PlatformConfigurationNativeApi::GetPersistentIsolateData) \ V(PlatformConfigurationNativeApi::ComputePlatformResolvedLocale) \ diff --git a/engine/src/flutter/lib/ui/platform_dispatcher.dart b/engine/src/flutter/lib/ui/platform_dispatcher.dart index 3d65c6b6ce2..ce01a20ec2e 100644 --- a/engine/src/flutter/lib/ui/platform_dispatcher.dart +++ b/engine/src/flutter/lib/ui/platform_dispatcher.dart @@ -719,6 +719,26 @@ class PlatformDispatcher { @Native(symbol: 'PlatformConfigurationNativeApi::RegisterBackgroundIsolate') external static void __registerBackgroundIsolate(int rootIsolateId); + /// Informs the engine whether the framework is generating a semantics tree. + /// + /// Only framework knows when semantics tree should be generated. It uses this + /// method to notify the engine whether the framework will generate a semantics tree. + /// + /// In the case where platforms want to enable semantics, e.g. when + /// assistive technologies are enabled, it notifies framework through + /// [onSemanticsEnabledChanged]. + /// + /// After this has been set to true, platforms are expected to prepare for accepting + /// semantics update sent via [FlutterView.updateSemantics]. When this is set to false, platforms + /// may dispose any resources associated with processing semantics as no further + /// semantics updates will be sent via [FlutterView.updateSemantics]. + /// + /// One must call this method with true before sending update through [updateSemantics]. + void setSemanticsTreeEnabled(bool enabled) => _setSemanticsTreeEnabled(enabled); + + @Native(symbol: 'PlatformConfigurationNativeApi::SetSemanticsTreeEnabled') + external static void _setSemanticsTreeEnabled(bool update); + /// Deprecated. Migrate to [ChannelBuffers.setListener] instead. /// /// Called whenever this platform dispatcher receives a message from a diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index 48a18692cf8..3140a6d0332 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -390,9 +390,8 @@ class FlutterView { /// Change the retained semantics data about this [FlutterView]. /// - /// If [PlatformDispatcher.semanticsEnabled] is true, the user has requested that this function - /// be called whenever the semantic content of this [FlutterView] - /// changes. + /// [PlatformDispatcher.setSemanticsTreeEnabled] must be called with true + /// before sending update through this method. /// /// This function disposes the given update, which means the semantics update /// cannot be used further. diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.cc b/engine/src/flutter/lib/ui/window/platform_configuration.cc index c47a45ea0af..46b0d745a59 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.cc +++ b/engine/src/flutter/lib/ui/window/platform_configuration.cc @@ -669,6 +669,14 @@ void PlatformConfigurationNativeApi::UpdateSemantics(int64_t view_id, view_id, update); } +void PlatformConfigurationNativeApi::SetSemanticsTreeEnabled(bool enabled) { + UIDartState::ThrowIfUIOperationsProhibited(); + UIDartState::Current() + ->platform_configuration() + ->client() + ->SetSemanticsTreeEnabled(enabled); +} + Dart_Handle PlatformConfigurationNativeApi::ComputePlatformResolvedLocale( Dart_Handle supportedLocalesHandle) { UIDartState::ThrowIfUIOperationsProhibited(); diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.h b/engine/src/flutter/lib/ui/window/platform_configuration.h index 29ee8ca4fff..0219a5a0f93 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.h +++ b/engine/src/flutter/lib/ui/window/platform_configuration.h @@ -97,6 +97,13 @@ class PlatformConfigurationClient { /// virtual void UpdateSemantics(int64_t viewId, SemanticsUpdate* update) = 0; + //-------------------------------------------------------------------------- + /// @brief Notifies whether Framework starts generating semantics tree. + /// + /// @param[in] enabled True if Framework starts generating semantics tree. + /// + virtual void SetSemanticsTreeEnabled(bool enabled) = 0; + //-------------------------------------------------------------------------- /// @brief When the Flutter application has a message to send to the /// underlying platform, the message needs to be forwarded to @@ -625,6 +632,8 @@ class PlatformConfigurationNativeApi { static void UpdateSemantics(int64_t viewId, SemanticsUpdate* update); + static void SetSemanticsTreeEnabled(bool enabled); + static void SetNeedsReportTimings(bool value); static Dart_Handle GetPersistentIsolateData(); diff --git a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart index b4dd689d5a9..36b74594828 100644 --- a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart @@ -85,6 +85,8 @@ abstract class PlatformDispatcher { void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame}); + void setSemanticsTreeEnabled(bool enabled) {} + AccessibilityFeatures get accessibilityFeatures; VoidCallback? get onAccessibilityFeaturesChanged; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 528b57bf8db..c7c1618076b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -700,6 +700,15 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { FrameService.instance.scheduleWarmUpFrame(beginFrame: beginFrame, drawFrame: drawFrame); } + @override + void setSemanticsTreeEnabled(bool enabled) { + if (!enabled) { + for (final EngineFlutterView view in views) { + view.semantics.reset(); + } + } + } + /// Updates the application's rendering on the GPU with the newly provided /// [Scene]. This function must be called within the scope of the /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function diff --git a/engine/src/flutter/runtime/BUILD.gn b/engine/src/flutter/runtime/BUILD.gn index a0e8610a6e3..5167d321482 100644 --- a/engine/src/flutter/runtime/BUILD.gn +++ b/engine/src/flutter/runtime/BUILD.gn @@ -140,6 +140,7 @@ if (enable_unittests) { "dart_service_isolate_unittests.cc", "dart_vm_unittests.cc", "platform_isolate_manager_unittests.cc", + "runtime_controller_unittests.cc", "type_conversions_unittests.cc", ] @@ -153,6 +154,7 @@ if (enable_unittests) { "//flutter/common", "//flutter/fml", "//flutter/lib/snapshot", + "//flutter/shell/common:shell_test_fixture_sources", "//flutter/skia", "//flutter/testing", "//flutter/testing:dart", diff --git a/engine/src/flutter/runtime/dart_isolate_unittests.cc b/engine/src/flutter/runtime/dart_isolate_unittests.cc index 602011c1c5d..3be980588e1 100644 --- a/engine/src/flutter/runtime/dart_isolate_unittests.cc +++ b/engine/src/flutter/runtime/dart_isolate_unittests.cc @@ -712,6 +712,7 @@ class FakePlatformConfigurationClient : public PlatformConfigurationClient { double width, double height) override {} void UpdateSemantics(int64_t view_id, SemanticsUpdate* update) override {} + void SetSemanticsTreeEnabled(bool enabled) override {} void HandlePlatformMessage( std::unique_ptr message) override {} FontCollection& GetFontCollection() override { diff --git a/engine/src/flutter/runtime/fixtures/runtime_test.dart b/engine/src/flutter/runtime/fixtures/runtime_test.dart index 0a85f5f97d9..748298b7533 100644 --- a/engine/src/flutter/runtime/fixtures/runtime_test.dart +++ b/engine/src/flutter/runtime/fixtures/runtime_test.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'dart:isolate'; +import 'dart:typed_data'; +import 'dart:ui'; import 'split_lib_test.dart' deferred as splitlib; @@ -219,3 +221,102 @@ Function createEntryPointForPlatIsoSendAndRecvTest() { void mainForPlatformIsolatesThrowError() { throw AssertionError('Error from platform isolate'); } + +@pragma('vm:entry-point') +void sendSemanticsUpdate() { + final SemanticsUpdateBuilder builder = SemanticsUpdateBuilder(); + const String identifier = 'identifier'; + const String label = 'label'; + final List labelAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), + ]; + + const String value = 'value'; + final List valueAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 2, end: 3)), + ]; + + const String increasedValue = 'increasedValue'; + final List increasedValueAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 4, end: 5)), + ]; + + const String decreasedValue = 'decreasedValue'; + final List decreasedValueAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 5, end: 6)), + ]; + + const String hint = 'hint'; + final List hintAttributes = [ + LocaleStringAttribute( + locale: const Locale('en', 'MX'), + range: const TextRange(start: 0, end: 1), + ), + ]; + + const String tooltip = 'tooltip'; + + final Float64List transform = Float64List(16); + final Int32List childrenInTraversalOrder = Int32List(0); + final Int32List childrenInHitTestOrder = Int32List(0); + final Int32List additionalActions = Int32List(0); + transform[0] = 1; + transform[1] = 0; + transform[2] = 0; + transform[3] = 0; + + transform[4] = 0; + transform[5] = 1; + transform[6] = 0; + transform[7] = 0; + + transform[8] = 0; + transform[9] = 0; + transform[10] = 1; + transform[11] = 0; + + transform[12] = 0; + transform[13] = 0; + transform[14] = 0; + transform[15] = 0; + builder.updateNode( + id: 0, + flags: 0, + actions: 0, + maxValueLength: 0, + currentValueLength: 0, + textSelectionBase: -1, + textSelectionExtent: -1, + platformViewId: -1, + scrollChildren: 0, + scrollIndex: 0, + scrollPosition: 0, + scrollExtentMax: 0, + scrollExtentMin: 0, + rect: const Rect.fromLTRB(0, 0, 10, 10), + elevation: 0, + thickness: 0, + identifier: identifier, + label: label, + labelAttributes: labelAttributes, + value: value, + valueAttributes: valueAttributes, + increasedValue: increasedValue, + increasedValueAttributes: increasedValueAttributes, + decreasedValue: decreasedValue, + decreasedValueAttributes: decreasedValueAttributes, + hint: hint, + hintAttributes: hintAttributes, + tooltip: tooltip, + textDirection: TextDirection.ltr, + transform: transform, + childrenInTraversalOrder: childrenInTraversalOrder, + childrenInHitTestOrder: childrenInHitTestOrder, + additionalActions: additionalActions, + controlsNodes: null, + ); + _semanticsUpdate(builder.build()); +} + +@pragma('vm:external-name', 'SemanticsUpdate') +external void _semanticsUpdate(SemanticsUpdate update); diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index 113e1fb8c93..feba155b00d 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -450,10 +450,12 @@ void RuntimeController::CheckIfAllViewsRendered() { // |PlatformConfigurationClient| void RuntimeController::UpdateSemantics(int64_t view_id, SemanticsUpdate* update) { - if (platform_data_.semantics_enabled) { - client_.UpdateSemantics(view_id, update->takeNodes(), - update->takeActions()); - } + client_.UpdateSemantics(view_id, update->takeNodes(), update->takeActions()); +} + +// |PlatformConfigurationClient| +void RuntimeController::SetSemanticsTreeEnabled(bool enabled) { + client_.SetSemanticsTreeEnabled(enabled); } // |PlatformConfigurationClient| diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index 5b11cf2b819..adc6c85334f 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -643,6 +643,12 @@ class RuntimeController : public PlatformConfigurationClient, // |PlatformConfigurationClient| std::shared_ptr GetPersistentIsolateData() override; + // |PlatformConfigurationClient| + void UpdateSemantics(int64_t view_id, SemanticsUpdate* update) override; + + // |PlatformConfigurationClient| + void SetSemanticsTreeEnabled(bool enabled) override; + const fml::WeakPtr& GetIOManager() const { return context_.io_manager; } @@ -768,9 +774,6 @@ class RuntimeController : public PlatformConfigurationClient, double width, double height) override; - // |PlatformConfigurationClient| - void UpdateSemantics(int64_t view_id, SemanticsUpdate* update) override; - // |PlatformConfigurationClient| void HandlePlatformMessage(std::unique_ptr message) override; diff --git a/engine/src/flutter/runtime/runtime_controller_unittests.cc b/engine/src/flutter/runtime/runtime_controller_unittests.cc new file mode 100644 index 00000000000..a8e27bae1e3 --- /dev/null +++ b/engine/src/flutter/runtime/runtime_controller_unittests.cc @@ -0,0 +1,149 @@ +// 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/runtime/runtime_controller.h" +#include "flutter/runtime/runtime_delegate.h" + +#include "flutter/lib/ui/semantics/semantics_update.h" +#include "flutter/shell/common/shell_test.h" +#include "flutter/testing/testing.h" + +namespace flutter::testing { +// For namespacing when running tests. +using RuntimeControllerTest = ShellTest; + +class MockRuntimeDelegate : public RuntimeDelegate { + public: + FontCollection font; + std::vector updates; + std::vector actions; + std::string DefaultRouteName() override { return ""; } + + void ScheduleFrame(bool regenerate_layer_trees = true) override {} + + void OnAllViewsRendered() override {} + + void Render(int64_t view_id, + std::unique_ptr layer_tree, + float device_pixel_ratio) override {} + + void UpdateSemantics(int64_t view_id, + SemanticsNodeUpdates update, + CustomAccessibilityActionUpdates actions) override { + this->updates.push_back(update); + this->actions.push_back(actions); + } + + void SetSemanticsTreeEnabled(bool enabled) override {} + + void HandlePlatformMessage( + std::unique_ptr message) override {} + + FontCollection& GetFontCollection() override { return font; } + + std::shared_ptr GetAssetManager() override { return nullptr; } + + void OnRootIsolateCreated() override {}; + + void UpdateIsolateDescription(const std::string isolate_name, + int64_t isolate_port) override {}; + + void SetNeedsReportTimings(bool value) override {}; + + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override { + return nullptr; + } + + void RequestDartDeferredLibrary(intptr_t loading_unit_id) override {} + + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override {} + + std::weak_ptr GetPlatformMessageHandler() + const override { + return {}; + } + + void SendChannelUpdate(std::string name, bool listening) override {} + + double GetScaledFontSize(double unscaled_font_size, + int configuration_id) const override { + return 0.0; + } +}; + +class RuntimeControllerTester { + public: + explicit RuntimeControllerTester(UIDartState::Context& context) + : context_(context), + runtime_controller_(delegate_, + nullptr, + {}, + {}, + {}, + {}, + {}, + nullptr, + context_) {} + + void CanUpdateSemanticsWhenSetSemanticsTreeEnabled(SemanticsUpdate* update) { + ASSERT_TRUE(delegate_.updates.empty()); + ASSERT_TRUE(delegate_.actions.empty()); + runtime_controller_.SetSemanticsTreeEnabled(true); + runtime_controller_.UpdateSemantics(0, update); + ASSERT_FALSE(delegate_.updates.empty()); + ASSERT_FALSE(delegate_.actions.empty()); + } + + private: + MockRuntimeDelegate delegate_; + UIDartState::Context& context_; + RuntimeController runtime_controller_; +}; + +TEST_F(RuntimeControllerTest, CanUpdateSemanticsWhenSetSemanticsTreeEnabled) { + fml::AutoResetWaitableEvent message_latch; + // The code in this test is mostly setup code to get a SemanticsUpdate object. + // The real test is in RuntimeControllerTester::CanUpdateSemantics. + TaskRunners task_runners("test", // label + GetCurrentTaskRunner(), // platform + CreateNewThread(), // raster + CreateNewThread(), // ui + CreateNewThread() // io + ); + UIDartState::Context context(task_runners); + auto tester = std::make_shared(context); + + auto native_semantics_update = [tester, + &message_latch](Dart_NativeArguments args) { + auto handle = Dart_GetNativeArgument(args, 0); + intptr_t peer = 0; + Dart_Handle result = Dart_GetNativeInstanceField( + handle, tonic::DartWrappable::kPeerIndex, &peer); + ASSERT_FALSE(Dart_IsError(result)); + SemanticsUpdate* update = reinterpret_cast(peer); + + tester->CanUpdateSemanticsWhenSetSemanticsTreeEnabled(update); + message_latch.Signal(); + }; + + Settings settings = CreateSettingsForFixture(); + AddNativeCallback("SemanticsUpdate", + CREATE_NATIVE_ENTRY(native_semantics_update)); + + std::unique_ptr shell = CreateShell(settings, task_runners); + + ASSERT_TRUE(shell->IsSetup()); + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("sendSemanticsUpdate"); + + shell->RunEngine(std::move(configuration), [](auto result) { + ASSERT_EQ(result, Engine::RunStatus::Success); + }); + + message_latch.Wait(); + DestroyShell(std::move(shell), task_runners); +} + +} // namespace flutter::testing diff --git a/engine/src/flutter/runtime/runtime_delegate.h b/engine/src/flutter/runtime/runtime_delegate.h index d5df4597918..84ca390db09 100644 --- a/engine/src/flutter/runtime/runtime_delegate.h +++ b/engine/src/flutter/runtime/runtime_delegate.h @@ -36,6 +36,8 @@ class RuntimeDelegate { SemanticsNodeUpdates update, CustomAccessibilityActionUpdates actions) = 0; + virtual void SetSemanticsTreeEnabled(bool enabled) = 0; + virtual void HandlePlatformMessage( std::unique_ptr message) = 0; diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index 56e894335aa..ae5d68e168c 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -503,6 +503,10 @@ void Engine::UpdateSemantics(int64_t view_id, std::move(actions)); } +void Engine::SetSemanticsTreeEnabled(bool enabled) { + delegate_.OnEngineSetSemanticsTreeEnabled(enabled); +} + void Engine::HandlePlatformMessage(std::unique_ptr message) { if (message->channel() == kAssetChannel) { HandleAssetPlatformMessage(std::move(message)); diff --git a/engine/src/flutter/shell/common/engine.h b/engine/src/flutter/shell/common/engine.h index 8d911741931..b7cd105c47f 100644 --- a/engine/src/flutter/shell/common/engine.h +++ b/engine/src/flutter/shell/common/engine.h @@ -161,6 +161,20 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { SemanticsNodeUpdates updates, CustomAccessibilityActionUpdates actions) = 0; + //-------------------------------------------------------------------------- + /// @brief When the Framework starts or stops generating semantics + /// tree, + /// this new information needs to be conveyed to the underlying + /// platform so that they can prepare to accept semantics + /// update. The engine delegates this task to the shell via this + /// call. + /// + /// @see `OnEngineUpdateSemantics` + /// + /// @param[in] enabled whether Framework starts generating semantics tree. + /// + virtual void OnEngineSetSemanticsTreeEnabled(bool enabled) = 0; + //-------------------------------------------------------------------------- /// @brief When the Flutter application has a message to send to the /// underlying platform, the message needs to be forwarded to @@ -1016,6 +1030,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { SemanticsNodeUpdates update, CustomAccessibilityActionUpdates actions) override; + // |RuntimeDelegate| + void SetSemanticsTreeEnabled(bool enabled) override; + // |RuntimeDelegate| void HandlePlatformMessage(std::unique_ptr message) override; diff --git a/engine/src/flutter/shell/common/engine_animator_unittests.cc b/engine/src/flutter/shell/common/engine_animator_unittests.cc index fc97a8c0e24..479e26f4a13 100644 --- a/engine/src/flutter/shell/common/engine_animator_unittests.cc +++ b/engine/src/flutter/shell/common/engine_animator_unittests.cc @@ -56,6 +56,7 @@ class MockDelegate : public Engine::Delegate { OnEngineUpdateSemantics, (int64_t, SemanticsNodeUpdates, CustomAccessibilityActionUpdates), (override)); + MOCK_METHOD(void, OnEngineSetSemanticsTreeEnabled, (bool), (override)); MOCK_METHOD(void, OnEngineHandlePlatformMessage, (std::unique_ptr), diff --git a/engine/src/flutter/shell/common/engine_unittests.cc b/engine/src/flutter/shell/common/engine_unittests.cc index 131db5bff6c..324104d9185 100644 --- a/engine/src/flutter/shell/common/engine_unittests.cc +++ b/engine/src/flutter/shell/common/engine_unittests.cc @@ -64,6 +64,7 @@ class MockDelegate : public Engine::Delegate { OnEngineUpdateSemantics, (int64_t, SemanticsNodeUpdates, CustomAccessibilityActionUpdates), (override)); + MOCK_METHOD(void, OnEngineSetSemanticsTreeEnabled, (bool), (override)); MOCK_METHOD(void, OnEngineHandlePlatformMessage, (std::unique_ptr), @@ -115,6 +116,7 @@ class MockRuntimeDelegate : public RuntimeDelegate { UpdateSemantics, (int64_t, SemanticsNodeUpdates, CustomAccessibilityActionUpdates), (override)); + MOCK_METHOD(void, SetSemanticsTreeEnabled, (bool), (override)); MOCK_METHOD(void, HandlePlatformMessage, (std::unique_ptr), diff --git a/engine/src/flutter/shell/common/platform_view.cc b/engine/src/flutter/shell/common/platform_view.cc index 9ca28042011..496c75f85d2 100644 --- a/engine/src/flutter/shell/common/platform_view.cc +++ b/engine/src/flutter/shell/common/platform_view.cc @@ -130,6 +130,10 @@ void PlatformView::UpdateSemantics( // NOLINTNEXTLINE(performance-unnecessary-value-param) CustomAccessibilityActionUpdates actions) {} +void PlatformView::SetSemanticsTreeEnabled( + bool enabled // NOLINT(performance-unnecessary-value-param) +) {} + void PlatformView::SendChannelUpdate(const std::string& name, bool listening) {} void PlatformView::HandlePlatformMessage( diff --git a/engine/src/flutter/shell/common/platform_view.h b/engine/src/flutter/shell/common/platform_view.h index 7be2b1ca097..2eb9e1d9899 100644 --- a/engine/src/flutter/shell/common/platform_view.h +++ b/engine/src/flutter/shell/common/platform_view.h @@ -514,6 +514,15 @@ class PlatformView { SemanticsNodeUpdates updates, CustomAccessibilityActionUpdates actions); + //---------------------------------------------------------------------------- + /// @brief Used by the framework to tell the embedder to prepare or clear + /// resoruce for accepting semantics tree. + /// + /// @param[in] enabled whether framework starts or stops sending semantics + /// updates + /// + virtual void SetSemanticsTreeEnabled(bool enabled); + //---------------------------------------------------------------------------- /// @brief Used by the framework to tell the embedder that it has /// registered a listener on a given channel. diff --git a/engine/src/flutter/shell/common/shell.cc b/engine/src/flutter/shell/common/shell.cc index 31aa6e73e77..fbce24f8ef6 100644 --- a/engine/src/flutter/shell/common/shell.cc +++ b/engine/src/flutter/shell/common/shell.cc @@ -1334,6 +1334,20 @@ void Shell::OnEngineUpdateSemantics(int64_t view_id, }); } +// |Engine::Delegate| +void Shell::OnEngineSetSemanticsTreeEnabled(bool enabled) { + FML_DCHECK(is_set_up_); + FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); + + task_runners_.GetPlatformTaskRunner()->RunNowOrPostTask( + task_runners_.GetPlatformTaskRunner(), + [view = platform_view_->GetWeakPtr(), enabled] { + if (view) { + view->SetSemanticsTreeEnabled(enabled); + } + }); +} + // |Engine::Delegate| void Shell::OnEngineHandlePlatformMessage( std::unique_ptr message) { diff --git a/engine/src/flutter/shell/common/shell.h b/engine/src/flutter/shell/common/shell.h index a5305e7abfd..2a3f0fccb4e 100644 --- a/engine/src/flutter/shell/common/shell.h +++ b/engine/src/flutter/shell/common/shell.h @@ -671,6 +671,9 @@ class Shell final : public PlatformView::Delegate, SemanticsNodeUpdates update, CustomAccessibilityActionUpdates actions) override; + // |Engine::Delegate| + void OnEngineSetSemanticsTreeEnabled(bool enabled) override; + // |Engine::Delegate| void OnEngineHandlePlatformMessage( std::unique_ptr message) override; diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index edcdbb71d00..fda68241aa2 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -242,6 +242,7 @@ shared_library("ios_test_flutter") { "ios_context_noop_unittests.mm", "ios_surface_noop_unittests.mm", "platform_message_handler_ios_test.mm", + "platform_view_ios_test.mm", ] deps = [ ":flutter_framework", diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h index 274dff306ce..7f03b7b8abb 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.h @@ -87,6 +87,9 @@ class PlatformViewIOS final : public PlatformView { // |PlatformView| void SetSemanticsEnabled(bool enabled) override; + // |PlatformView| + void SetSemanticsTreeEnabled(bool enabled) override; + // |PlatformView| void HandlePlatformMessage(std::unique_ptr message) override; @@ -128,6 +131,11 @@ class PlatformViewIOS final : public PlatformView { return platform_message_handler_; } + /** + * Gets the accessibility bridge created in this platform view. + */ + AccessibilityBridge* GetAccessibilityBridge() { return accessibility_bridge_.get(); } + private: /// Smart pointer for use with objective-c observers. /// This guarantees we remove the observer. @@ -143,24 +151,6 @@ class PlatformViewIOS final : public PlatformView { id observer_ = nil; }; - /// Wrapper that guarantees we communicate clearing Accessibility - /// information to Dart. - class AccessibilityBridgeManager { - public: - explicit AccessibilityBridgeManager(const std::function& set_semantics_enabled); - AccessibilityBridgeManager(const std::function& set_semantics_enabled, - AccessibilityBridge* bridge); - explicit operator bool() const noexcept { return static_cast(accessibility_bridge_); } - AccessibilityBridge* get() const noexcept { return accessibility_bridge_.get(); } - void Set(std::unique_ptr bridge); - void Clear(); - - private: - FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridgeManager); - std::unique_ptr accessibility_bridge_; - std::function set_semantics_enabled_; - }; - __weak FlutterViewController* owner_controller_; // Since the `ios_surface_` is created on the platform thread but // used on the raster thread we need to protect it with a mutex. @@ -168,7 +158,7 @@ class PlatformViewIOS final : public PlatformView { std::unique_ptr ios_surface_; std::shared_ptr ios_context_; __weak FlutterPlatformViewsController* platform_views_controller_; - AccessibilityBridgeManager accessibility_bridge_; + std::unique_ptr accessibility_bridge_; ScopedObserver dealloc_view_controller_observer_; std::vector platform_resolved_locale_; std::shared_ptr platform_message_handler_; diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm index 15f6223d700..a7195ecd955 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios.mm @@ -18,29 +18,6 @@ FLUTTER_ASSERT_ARC namespace flutter { -PlatformViewIOS::AccessibilityBridgeManager::AccessibilityBridgeManager( - const std::function& set_semantics_enabled) - : AccessibilityBridgeManager(set_semantics_enabled, nullptr) {} - -PlatformViewIOS::AccessibilityBridgeManager::AccessibilityBridgeManager( - const std::function& set_semantics_enabled, - AccessibilityBridge* bridge) - : accessibility_bridge_(bridge), set_semantics_enabled_(set_semantics_enabled) { - if (bridge) { - set_semantics_enabled_(true); - } -} - -void PlatformViewIOS::AccessibilityBridgeManager::Set(std::unique_ptr bridge) { - accessibility_bridge_ = std::move(bridge); - set_semantics_enabled_(true); -} - -void PlatformViewIOS::AccessibilityBridgeManager::Clear() { - set_semantics_enabled_(false); - accessibility_bridge_.reset(); -} - PlatformViewIOS::PlatformViewIOS(PlatformView::Delegate& delegate, const std::shared_ptr& context, __weak FlutterPlatformViewsController* platform_views_controller, @@ -48,7 +25,6 @@ PlatformViewIOS::PlatformViewIOS(PlatformView::Delegate& delegate, : PlatformView(delegate, task_runners), ios_context_(context), platform_views_controller_(platform_views_controller), - accessibility_bridge_([this](bool enabled) { PlatformView::SetSemanticsEnabled(enabled); }), platform_message_handler_( new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} @@ -86,7 +62,7 @@ void PlatformViewIOS::SetOwnerViewController(__weak FlutterViewController* owner if (ios_surface_ || !owner_controller) { NotifyDestroyed(); ios_surface_.reset(); - accessibility_bridge_.Clear(); + accessibility_bridge_.reset(); } owner_controller_ = owner_controller; @@ -98,7 +74,7 @@ void PlatformViewIOS::SetOwnerViewController(__weak FlutterViewController* owner queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* note) { // Implicit copy of 'this' is fine. - accessibility_bridge_.Clear(); + accessibility_bridge_.reset(); owner_controller_ = nil; }]); @@ -121,8 +97,8 @@ void PlatformViewIOS::attachView() { FML_DCHECK(ios_surface_ != nullptr); if (accessibility_bridge_) { - accessibility_bridge_.Set(std::make_unique( - owner_controller_, this, owner_controller_.platformViewsController)); + accessibility_bridge_ = std::make_unique( + owner_controller_, this, owner_controller_.platformViewsController); } } @@ -161,22 +137,10 @@ std::shared_ptr PlatformViewIOS::GetImpellerContext() const { // |PlatformView| void PlatformViewIOS::SetSemanticsEnabled(bool enabled) { - if (!owner_controller_) { - FML_LOG(WARNING) << "Could not set semantics to enabled, this " - "PlatformViewIOS has no ViewController."; - return; - } - if (enabled && !accessibility_bridge_) { - accessibility_bridge_.Set(std::make_unique( - owner_controller_, this, owner_controller_.platformViewsController)); - } else if (!enabled && accessibility_bridge_) { - accessibility_bridge_.Clear(); - } else { - PlatformView::SetSemanticsEnabled(enabled); - } + PlatformView::SetSemanticsEnabled(enabled); } -// |shell:PlatformView| +// |PlatformView| void PlatformViewIOS::SetAccessibilityFeatures(int32_t flags) { PlatformView::SetAccessibilityFeatures(flags); } @@ -186,6 +150,7 @@ void PlatformViewIOS::UpdateSemantics(int64_t view_id, flutter::SemanticsNodeUpdates update, flutter::CustomAccessibilityActionUpdates actions) { FML_DCHECK(owner_controller_); + FML_DCHECK(accessibility_bridge_); if (accessibility_bridge_) { accessibility_bridge_.get()->UpdateSemantics(std::move(update), actions); [[NSNotificationCenter defaultCenter] postNotificationName:FlutterSemanticsUpdateNotification @@ -193,6 +158,20 @@ void PlatformViewIOS::UpdateSemantics(int64_t view_id, } } +// |PlatformView| +void PlatformViewIOS::SetSemanticsTreeEnabled(bool enabled) { + FML_DCHECK(owner_controller_); + if (enabled) { + if (accessibility_bridge_) { + return; + } + accessibility_bridge_ = + std::make_unique(owner_controller_, this, platform_views_controller_); + } else { + accessibility_bridge_.reset(); + } +} + // |PlatformView| std::unique_ptr PlatformViewIOS::CreateVSyncWaiter() { return std::make_unique(task_runners_); diff --git a/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm new file mode 100644 index 00000000000..335763bd6d3 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm @@ -0,0 +1,105 @@ +// 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. + +#import +#import + +#import "flutter/fml/thread.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" + +FLUTTER_ASSERT_ARC + +namespace flutter { + +namespace { + +class MockDelegate : public PlatformView::Delegate { + public: + void OnPlatformViewCreated(std::unique_ptr surface) override {} + void OnPlatformViewDestroyed() override {} + void OnPlatformViewScheduleFrame() override {} + void OnPlatformViewAddView(int64_t view_id, + const ViewportMetrics& viewport_metrics, + AddViewCallback callback) override {} + void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) override {} + void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {} + void OnPlatformViewSetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) override {} + const flutter::Settings& OnPlatformViewGetSettings() const override { return settings_; } + void OnPlatformViewDispatchPlatformMessage(std::unique_ptr message) override {} + void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { + } + void OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) override {} + void OnPlatformViewDispatchSemanticsAction(int64_t view_id, + int32_t node_id, + SemanticsAction action, + fml::MallocMapping args) override {} + void OnPlatformViewSetSemanticsEnabled(bool enabled) override {} + void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} + void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} + void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} + void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} + + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + std::unique_ptr snapshot_data, + std::unique_ptr snapshot_instructions) override { + } + void LoadDartDeferredLibraryError(intptr_t loading_unit_id, + const std::string error_message, + bool transient) override {} + void UpdateAssetResolverByType(std::unique_ptr updated_asset_resolver, + flutter::AssetResolver::AssetResolverType type) override {} + + flutter::Settings settings_; +}; + +} // namespace +} // namespace flutter + +@interface PlatformViewIOSTest : XCTestCase +@end + +@implementation PlatformViewIOSTest + +- (void)testSetSemanticsTreeEnabled { + flutter::MockDelegate mock_delegate; + auto thread = std::make_unique("PlatformViewIOSTest"); + auto thread_task_runner = thread->GetTaskRunner(); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + id engine = OCMClassMock([FlutterEngine class]); + + id flutterViewController = OCMClassMock([FlutterViewController class]); + + OCMStub([flutterViewController isViewLoaded]).andReturn(NO); + OCMStub([flutterViewController engine]).andReturn(engine); + OCMStub([engine binaryMessenger]).andReturn(messenger); + + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, + /*platform_views_controller=*/nil, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_sync_switch=*/std::make_shared()); + fml::AutoResetWaitableEvent latch; + thread_task_runner->PostTask([&] { + platform_view->SetOwnerViewController(flutterViewController); + XCTAssertFalse(platform_view->GetAccessibilityBridge()); + platform_view->SetSemanticsTreeEnabled(true); + XCTAssertTrue(platform_view->GetAccessibilityBridge()); + platform_view->SetSemanticsTreeEnabled(false); + XCTAssertFalse(platform_view->GetAccessibilityBridge()); + latch.Signal(); + }); + latch.Wait(); + + [engine stopMocking]; +} + +@end diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart index 34eb8ff7b83..ab712c4454f 100644 --- a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart @@ -310,6 +310,7 @@ Future a11y_main() async { ) ..updateCustomAction(id: 21, label: 'Archive', hint: 'archive message'); + PlatformDispatcher.instance.setSemanticsTreeEnabled(true); PlatformDispatcher.instance.views.first.updateSemantics(builder.build()); signalNativeTest(); @@ -397,6 +398,7 @@ Future a11y_string_attributes() async { controlsNodes: null, ); + PlatformDispatcher.instance.setSemanticsTreeEnabled(true); PlatformDispatcher.instance.views.first.updateSemantics(builder.build()); signalNativeTest(); } @@ -1689,6 +1691,7 @@ Future a11y_main_multi_view() async { ); } + PlatformDispatcher.instance.setSemanticsTreeEnabled(true); for (final view in PlatformDispatcher.instance.views) { view.updateSemantics(createForView(view).build()); } diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 8174aa8afb2..f3aec5f677c 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -471,6 +471,7 @@ Future sendSemanticsTreeInfo() async { return builder.build(); } + ui.PlatformDispatcher.instance.setSemanticsTreeEnabled(true); view1.updateSemantics(createSemanticsUpdate(view1.viewId + 1)); view2.updateSemantics(createSemanticsUpdate(view2.viewId + 1)); signal(); diff --git a/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart b/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart index e129f8a771c..c0c11989cb4 100644 --- a/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart +++ b/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart @@ -79,7 +79,7 @@ class LocaleInitialization extends Scenario { ); final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build(); - + view.platformDispatcher.setSemanticsTreeEnabled(true); view.updateSemantics(semanticsUpdate); } diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart index e09ab653843..2a1749a2df6 100644 --- a/packages/flutter/lib/src/semantics/binding.dart +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -28,6 +28,7 @@ mixin SemanticsBinding on BindingBase { ..onSemanticsActionEvent = _handleSemanticsActionEvent ..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; _handleSemanticsEnabledChanged(); + addSemanticsEnabledListener(_handleFrameworkSemanticsEnabledChanged); } /// The current [SemanticsBinding], if one has been created. @@ -154,6 +155,10 @@ mixin SemanticsBinding on BindingBase { performSemanticsAction(decodedAction); } + void _handleFrameworkSemanticsEnabledChanged() { + platformDispatcher.setSemanticsTreeEnabled(semanticsEnabled); + } + /// Called whenever the platform requests an action to be performed on a /// [SemanticsNode]. /// diff --git a/packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart b/packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart new file mode 100644 index 00000000000..c5578c78c89 --- /dev/null +++ b/packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart @@ -0,0 +1,35 @@ +// Copyright 2014 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. + +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('SemanticsHandle ensureSemantics calls setSemanticsTreeEnabled', () async { + final SemanticsTestBinding binding = SemanticsTestBinding(); + expect(binding.platformDispatcher.semanticsTreeEnabled, isFalse); + final SemanticsHandle handle = binding.ensureSemantics(); + expect(binding.platformDispatcher.semanticsTreeEnabled, isTrue); + handle.dispose(); + expect(binding.platformDispatcher.semanticsTreeEnabled, isFalse); + }); +} + +class SemanticsTestBinding extends AutomatedTestWidgetsFlutterBinding { + @override + TestPlatformDispatcherSpy get platformDispatcher => _platformDispatcherSpy; + static final TestPlatformDispatcherSpy _platformDispatcherSpy = TestPlatformDispatcherSpy( + platformDispatcher: PlatformDispatcher.instance, + ); +} + +class TestPlatformDispatcherSpy extends TestPlatformDispatcher { + TestPlatformDispatcherSpy({required super.platformDispatcher}); + bool semanticsTreeEnabled = false; + @override + void setSemanticsTreeEnabled(bool enabled) { + semanticsTreeEnabled = enabled; + } +}