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; + } +}