Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewire ios accessibility bridge receive semantics enabled signal #56691

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

chunhtai
Copy link
Contributor

@chunhtai chunhtai commented Nov 18, 2024

fixes flutter/flutter#158399

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

  1. add an empty dart:ui API setSemanticsTreeEnabled
  2. makes framework calls the empty API.
  3. merge this pr with actual implementation of setSemanticsTreeEnabled

I also need to do the Android part

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 and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

/**
* Gets the accessibility bridge created in this platform view.
*/
AccessibilityBridge* GetAccessibilityBridge() { return accessibility_bridge_.get(); }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are public for testing only, i am not sure how to best test the change without exposing these

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now; refactoring this class is on my to do list for a rainy day, but please add a note pointing out this is only for testing so we can clean it up in the refactor.

@@ -113,35 +128,18 @@ class PlatformViewIOS final : public PlatformView {
id<NSObject> observer_ = nil;
};

/// Wrapper that guarantees we communicate clearing Accessibility
/// information to Dart.
class AccessibilityBridgeManager {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sole purpose for this class is to call setSemanticsEnabled when a11y bridge is created. This is not needed even before the change because the a11y bridge will only be created when setSemanticsEnabled is called before the change.

I think this class is obsolete

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, yeah this is ICantBelieveItsNotUniquePtr with one additional feature.

if (generating) {
if (!accessibility_bridge_) {
accessibility_bridge_ = std::make_unique<AccessibilityBridge>(owner_controller_, this,
platform_views_controller_);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I assume platform_views_controller_ contains the same owner_controller_.platformViewsController. From the code I think yes, but not too sure.

@@ -36,7 +36,7 @@ namespace flutter {
* lifecycle. It's a long lived bridge owned by the `FlutterEngine` and can be attached and
* detached sequentially to multiple `FlutterViewController`s and `FlutterView`s.
*/
class PlatformViewIOS final : public PlatformView {
class PlatformViewIOS : public PlatformView {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was it necessary to remove the final?

@@ -171,14 +190,7 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {}
"PlatformViewIOS has no ViewController.";
return;
}
if (enabled && !accessibility_bridge_) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub won't let me comment on the line above, but I think you're safe to delete the if (!owner_controller_) { ... } block now, since it's no longer used here.

/**
* Handles accessibility message from accessibility channel.
*/
void handleAccessibilityMessage(__weak id message, FlutterReply reply);
Copy link
Member

@cbracken cbracken Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a C++ class, so C++ naming applies:

Suggested change
void handleAccessibilityMessage(__weak id message, FlutterReply reply);
void HandleAccessibilityMessage(__weak id message, FlutterReply reply);

Need to fix up the call sites though.

/**
* Send accessibility message to accessibility channel.
*/
void sendAccessibilityMessage(__weak id message);
Copy link
Member

@cbracken cbracken Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a C++ class, so C++ naming applies:

Suggested change
void sendAccessibilityMessage(__weak id message);
void SendAccessibilityMessage(__weak id message);

Need to fix up the call sites.

binaryMessenger:owner_controller.engine.binaryMessenger
codec:[FlutterStandardMessageCodec sharedInstance]];
[accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
handleAccessibilityMessage(message, reply);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
handleAccessibilityMessage(message, reply);
HandleAccessibilityMessage(message, reply);

}
}

void PlatformViewIOS::sendAccessibilityMessage(__weak id message) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void PlatformViewIOS::sendAccessibilityMessage(__weak id message) {
void PlatformViewIOS::SendAccessibilityMessage(__weak id message) {

@@ -74,7 +65,7 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,

void AccessibilityBridge::AccessibilityObjectDidBecomeFocused(int32_t id) {
last_focused_semantics_object_id_ = id;
[accessibility_channel_ sendMessage:@{@"type" : @"didGainFocus", @"nodeId" : @(id)}];
platform_view_->sendAccessibilityMessage(@{@"type" : @"didGainFocus", @"nodeId" : @(id)});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
platform_view_->sendAccessibilityMessage(@{@"type" : @"didGainFocus", @"nodeId" : @(id)});
platform_view_->SendAccessibilityMessage(@{@"type" : @"didGainFocus", @"nodeId" : @(id)});

if ([type isEqualToString:@"announce"]) {
NSString* message = annotatedEvent[@"data"][@"message"];
ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
NSString* messageToAnnounce = message[@"data"][@"message"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be message_to_announce since we're in a C++ method. I'm not in love with it either.

https://google.github.io/styleguide/objcguide.html#objective-c


namespace flutter {

namespace testing {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since everything in here is in flutter::testing, do:

namespace flutter::testing {

void SetNeedsReportTimings(bool value) override {};

std::unique_ptr<std::vector<std::string>> ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) override {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to do in your patch, but I'm going to take a look at the base class. Really curious why we're handing back a std::unique_ptr<std::vector<std::string>> here rather than just a std::vector<std::string>. I doubt these arrays are ever more than 5-10 entries even in extreme cases.

return {};
}

void SendChannelUpdate(std::string name, bool listening) override {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, nothing to do here, but we should almost certainly make this a const std::string& or a std::string_view in the base class.


MockRuntimeDelegate delegate_;
UIDartState::Context& context_;
RuntimeController runtime_controller_;
Copy link
Member

@cbracken cbracken Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these members be private:? If so, move CanUpdateSemantics up, since that almost certainly should be public :)

};

TEST_F(RuntimeControllerTest, CanUpdateSemantics) {
auto message_latch = std::make_shared<fml::AutoResetWaitableEvent>();
Copy link
Member

@cbracken cbracken Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be:

fml::AutoResetWaitableEvent message_latch;


Settings settings = CreateSettingsForFixture();
AddNativeCallback("SemanticsUpdate",
CREATE_NATIVE_ENTRY(nativeSemanticsUpdate));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CREATE_NATIVE_ENTRY(nativeSemanticsUpdate));
CREATE_NATIVE_ENTRY(native_semantics_update));

UIDartState::Context context(task_runners);
RuntimeControllerTester tester(context);

auto nativeSemanticsUpdate = [&tester,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
auto nativeSemanticsUpdate = [&tester,
auto native_semantics_update = [&tester,

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm modulo nits. Looks like a locale test failing though?

@chunhtai
Copy link
Contributor Author

all comment addressed

@chinmaygarde
Copy link
Member

The presub failures look real. Are you also waiting on another review from @cbracken ?

@chunhtai

This comment was marked as outdated.

github-merge-queue bot pushed a commit to flutter/flutter that referenced this pull request Nov 25, 2024
fixes #158399

engine ios flutter/engine#56691

## 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].

<!-- Links -->
[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
///
/// This function lets platform to prepare or dispose the resource for accepting
/// semantics update sent through [updateSemantics].
void setSemanticsTreeEnabled(bool enabled) => _setSemanticsTreeEnabled(enabled);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the framework only allowed to call updateSemantics after setting this to true? We should document that here and on updateSemantics.

lib/ui/window.dart Outdated Show resolved Hide resolved
@goderbauer
Copy link
Member

I am waiting for framework side change to be merged first.

What's the associated framework change?

@chunhtai
Copy link
Contributor Author

chunhtai commented Dec 9, 2024

I am waiting for framework side change to be merged first.

What's the associated framework change?

This was an outdated comment, I will need to do a three phase transition once i get LGTM on this pr
1, add an empty dart:ui API setSemanticsTreeEnabled
2, makes framework calls the empty API.
3, merge this pr with actual implementation of setSemanticsTreeEnabled

@chunhtai chunhtai changed the title Rewire ios accessibility bridge message channel to receive semantics generating event Rewire ios accessibility bridge receive semantics enabled signal Dec 9, 2024
@chunhtai chunhtai marked this pull request as ready for review December 9, 2024 23:03
@chunhtai chunhtai requested a review from goderbauer December 10, 2024 17:51
github-merge-queue bot pushed a commit to flutter/flutter that referenced this pull request Dec 10, 2024
…" (#160039)

This reverts commit 1a2d6a3.

This is a straight revert as we are pivot away from using message
channel, instead we will be adding api to dart:ui.
See flutter/engine#56691

## 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].

<!-- Links -->
[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
/// Informs the engine whether the framework is generating a semantics tree.
///
/// After this has been set to true, platforms are expected to prepare for accepting
/// semantics update sent via [updateSemantics]. When this is set to false, platforms
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it is possible that either the platform OR the framework turns on semantics. Who is in charge of keeping track of this? If the platform turned on semantics and then the framework turns off semantics by calling this method - do we just leave the platform hanging and not provide it with a semantics tree anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

framework is keep track of this since it is the one who knows how many semantics handles are generated.

platform can send signal through SetSemanticsEnabled to framework, but it shouldnt initialize resource for accessibility until framework sends setSemanticsTreeEnabled back.

when framework turn off semantics(can happen when last handle is disposed), it will send setSemanticsTreeEnabled(false), platform will then know it can dispose its accessibility resource

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document that contract here? And on where the signal from the platform is send to the framework?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it tricky to write document in this class, this api is facing framework so i find it weird to document framework behavior in this doc, so I mainly document what this function does and when the consume should call this function. let me try to see what I can come up with

/// semantics update sent via [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 [updateSemantics].
void setSemanticsTreeEnabled(bool enabled) => _setSemanticsTreeEnabled(enabled);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we just call this setSemanticsEnabled? I don't think we officially call this a tree anywhere else in the API.

Copy link
Contributor Author

@chunhtai chunhtai Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setSemanticsEnabled is used exclusively by platform to let framework know it "wants" semantics to turn on. SetSemanticsTreeEnabled is framework signal to tell platform that "yes i will start compiling semantics"

don't think we officially call this a tree anywhere else in the API.

Do you have other name suggestion? how about setSemanticsCompilingEnabled ?

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for the dart:ui stuff when the additional documentation is added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
platform-ios platform-web Code specifically for the web engine
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SemanticsBinding.instance.ensureSemantics causes inconsistent state for mobile
4 participants