Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions shell/platform/windows/fixtures/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,22 @@ void hiPlatformChannels() {
});
}

/// Returns a future that completes when
/// `PlatformDispatcher.instance.onSemanticsEnabledChanged` fires.
Future<void> get semanticsChanged {
final Completer<void> semanticsChanged = Completer<void>();
ui.PlatformDispatcher.instance.onSemanticsEnabledChanged =
semanticsChanged.complete;
return semanticsChanged.future;
}

@pragma('vm:entry-point')
void alertPlatformChannel() async {
void sendAccessiblityAnnouncement() async {
// Wait until semantics are enabled.
if (!ui.PlatformDispatcher.instance.semanticsEnabled) {
await semanticsChanged;
}

// Serializers for data types are in the framework, so this will be hardcoded.
const int valueMap = 13, valueString = 7;
// Corresponds to:
Expand Down Expand Up @@ -78,13 +92,9 @@ void alertPlatformChannel() async {
]);
final ByteData byteData = data.buffer.asByteData();

final Completer<ByteData?> enabled = Completer<ByteData?>();
ui.PlatformDispatcher.instance.sendPlatformMessage('semantics', ByteData(0), (ByteData? reply){
enabled.complete(reply);
ui.PlatformDispatcher.instance.sendPlatformMessage('flutter/accessibility', byteData, (ByteData? _){
signal();
});
await enabled.future;

ui.PlatformDispatcher.instance.sendPlatformMessage('flutter/accessibility', byteData, (ByteData? _){});
}

@pragma('vm:entry-point')
Expand Down
41 changes: 30 additions & 11 deletions shell/platform/windows/flutter_windows_engine.cc
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ std::unique_ptr<FlutterWindowsView> FlutterWindowsEngine::CreateView(
auto view = std::make_unique<FlutterWindowsView>(
kImplicitViewId, this, std::move(window), windows_proc_table_);

view_ = view.get();
views_[kImplicitViewId] = view.get();
InitializeKeyboard();

return std::move(view);
Expand Down Expand Up @@ -528,9 +528,12 @@ std::chrono::nanoseconds FlutterWindowsEngine::FrameInterval() {
}

FlutterWindowsView* FlutterWindowsEngine::view(FlutterViewId view_id) const {
FML_DCHECK(view_id == kImplicitViewId);
auto iterator = views_.find(view_id);
if (iterator == views_.end()) {
return nullptr;
}

return view_;
return iterator->second;
}

// Returns the currently configured Plugin Registrar.
Expand Down Expand Up @@ -669,7 +672,7 @@ void FlutterWindowsEngine::SendSystemLocales() {
}

void FlutterWindowsEngine::InitializeKeyboard() {
if (view_ == nullptr) {
if (views_.empty()) {
FML_LOG(ERROR) << "Cannot initialize keyboard on Windows headless mode.";
}

Expand Down Expand Up @@ -756,16 +759,24 @@ bool FlutterWindowsEngine::DispatchSemanticsAction(
}

void FlutterWindowsEngine::UpdateSemanticsEnabled(bool enabled) {
if (engine_ && semantics_enabled_ != enabled) {
semantics_enabled_ = enabled;
embedder_api_.UpdateSemanticsEnabled(engine_, enabled);
view_->UpdateSemanticsEnabled(enabled);
if (!engine_ || semantics_enabled_ == enabled) {
return;
}

semantics_enabled_ = enabled;
embedder_api_.UpdateSemanticsEnabled(engine_, enabled);

for (auto iterator = views_.begin(); iterator != views_.end(); iterator++) {
iterator->second->UpdateSemanticsEnabled(enabled);
}
}

void FlutterWindowsEngine::OnPreEngineRestart() {
// Reset the keyboard's state on hot restart.
if (view_) {
// TODO(loicsharma): Always reset the keyboard once
// it no longer depends on the implicit view.
// https://github.com/flutter/flutter/issues/115611
if (!views_.empty()) {
InitializeKeyboard();
}
}
Expand Down Expand Up @@ -821,7 +832,13 @@ void FlutterWindowsEngine::HandleAccessibilityMessage(
std::string text =
std::get<std::string>(data_map.at(EncodableValue("message")));
std::wstring wide_text = fml::Utf8ToWideString(text);
view_->AnnounceAlert(wide_text);

// TODO(loicsharma): Remove implicit view assumption.
// https://github.com/flutter/flutter/issues/142845
auto view = this->view(kImplicitViewId);
if (view) {
view->AnnounceAlert(wide_text);
}
}
}
SendPlatformMessageResponse(message->response_handle,
Expand All @@ -843,7 +860,9 @@ void FlutterWindowsEngine::OnQuit(std::optional<HWND> hwnd,
}

void FlutterWindowsEngine::OnDwmCompositionChanged() {
view_->OnDwmCompositionChanged();
for (auto iterator = views_.begin(); iterator != views_.end(); iterator++) {
iterator->second->OnDwmCompositionChanged();
}
}

void FlutterWindowsEngine::OnWindowStateEvent(HWND hwnd,
Expand Down
12 changes: 7 additions & 5 deletions shell/platform/windows/flutter_windows_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>

#include "flutter/fml/closure.h"
Expand Down Expand Up @@ -117,12 +118,13 @@ class FlutterWindowsEngine {
// Returns false if stopping the engine fails, or if it was not running.
virtual bool Stop();

// Create the view that is displaying this engine's content.
// Create a view that can display this engine's content.
std::unique_ptr<FlutterWindowsView> CreateView(
std::unique_ptr<WindowBindingHandler> window);

// The view displaying this engine's content, if any. This will be null for
// headless engines.
// Get a view that displays this engine's content.
//
// Returns null if the view does not exist.
FlutterWindowsView* view(FlutterViewId view_id) const;

// Returns the currently configured Plugin Registrar.
Expand Down Expand Up @@ -348,8 +350,8 @@ class FlutterWindowsEngine {
// AOT data, if any.
UniqueAotDataPtr aot_data_;

// The view displaying the content running in this engine, if any.
FlutterWindowsView* view_ = nullptr;
// The views displaying the content running in this engine, if any.
std::unordered_map<FlutterViewId, FlutterWindowsView*> views_;

// Task runner for tasks posted from the engine.
std::unique_ptr<TaskRunner> task_runner_;
Expand Down
90 changes: 62 additions & 28 deletions shell/platform/windows/flutter_windows_engine_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "flutter/shell/platform/windows/testing/mock_windows_proc_table.h"
#include "flutter/shell/platform/windows/testing/test_keyboard.h"
#include "flutter/shell/platform/windows/testing/windows_test.h"
#include "flutter/shell/platform/windows/testing/windows_test_config_builder.h"
#include "flutter/third_party/accessibility/ax/platform/ax_platform_node_win.h"
#include "fml/synchronization/waitable_event.h"
#include "gmock/gmock.h"
Expand All @@ -27,10 +28,23 @@
namespace flutter {
namespace testing {

using ::testing::NiceMock;
using ::testing::Return;

class FlutterWindowsEngineTest : public WindowsTest {};

TEST_F(FlutterWindowsEngineTest, RunHeadless) {
FlutterWindowsEngineBuilder builder{GetContext()};
std::unique_ptr<FlutterWindowsEngine> engine = builder.Build();

EngineModifier modifier(engine.get());
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };

ASSERT_TRUE(engine->Run());
ASSERT_EQ(engine->view(kImplicitViewId), nullptr);
ASSERT_EQ(engine->view(123), nullptr);
}

TEST_F(FlutterWindowsEngineTest, RunDoesExpectedInitialization) {
FlutterWindowsEngineBuilder builder{GetContext()};
builder.AddDartEntrypointArgument("arg1");
Expand Down Expand Up @@ -636,44 +650,64 @@ class MockFlutterWindowsView : public FlutterWindowsView {
FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView);
};

TEST_F(FlutterWindowsEngineTest, AlertPlatformMessage) {
FlutterWindowsEngineBuilder builder{GetContext()};
builder.SetDartEntrypoint("alertPlatformChannel");
// Verify the view is notified of accessibility announcements.
TEST_F(FlutterWindowsEngineTest, AccessibilityAnnouncement) {
auto& context = GetContext();
WindowsConfigBuilder builder{context};
builder.SetDartEntrypoint("sendAccessiblityAnnouncement");

bool done = false;
auto native_entry =
CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; });
context.AddNativeFunction("Signal", native_entry);

EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);

auto engine = builder.Build();
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
ui::AXPlatformNodeDelegateBase parent_delegate;
AlertPlatformNodeDelegate delegate(parent_delegate);
AlertPlatformNodeDelegate delegate{parent_delegate};

auto window_binding_handler =
std::make_unique<NiceMock<MockWindowBindingHandler>>();
EXPECT_CALL(*window_binding_handler, GetAlertDelegate)
.WillRepeatedly(Return(&delegate));
MockFlutterWindowsView view(engine.get(), std::move(window_binding_handler));
.WillOnce(Return(&delegate));

EngineModifier modifier(engine.get());
auto windows_engine = reinterpret_cast<FlutterWindowsEngine*>(engine.get());
MockFlutterWindowsView view{windows_engine,
std::move(window_binding_handler)};
EngineModifier modifier{windows_engine};
modifier.SetImplicitView(&view);
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };

auto binary_messenger =
std::make_unique<BinaryMessengerImpl>(engine->messenger());
binary_messenger->SetMessageHandler(
"semantics", [&engine](const uint8_t* message, size_t message_size,
BinaryReply reply) {
engine->UpdateSemanticsEnabled(true);
char response[] = "";
reply(reinterpret_cast<uint8_t*>(response), 0);
});
windows_engine->UpdateSemanticsEnabled(true);

bool did_call = false;
EXPECT_CALL(view, NotifyWinEventWrapper)
.WillOnce([&did_call](ui::AXPlatformNodeWin* node,
ax::mojom::Event event) { did_call = true; });
EXPECT_CALL(view, NotifyWinEventWrapper).Times(1);

engine->UpdateSemanticsEnabled(true);
engine->Run();
// Rely on timeout mechanism in CI.
while (!done) {
windows_engine->task_runner()->ProcessTasks();
}
}

// Verify the app can send accessibility announcements while in headless mode.
TEST_F(FlutterWindowsEngineTest, AccessibilityAnnouncementHeadless) {
auto& context = GetContext();
WindowsConfigBuilder builder{context};
builder.SetDartEntrypoint("sendAccessiblityAnnouncement");

bool done = false;
auto native_entry =
CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; });
context.AddNativeFunction("Signal", native_entry);

EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);

auto windows_engine = reinterpret_cast<FlutterWindowsEngine*>(engine.get());
windows_engine->UpdateSemanticsEnabled(true);

// Rely on timeout mechanism in CI.
while (!did_call) {
engine->task_runner()->ProcessTasks();
while (!done) {
windows_engine->task_runner()->ProcessTasks();
}
}

Expand Down
4 changes: 1 addition & 3 deletions shell/platform/windows/flutter_windows_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,8 @@ TEST_F(WindowsTest, LaunchCustomEntrypointInEngineRunInvocation) {
TEST_F(WindowsTest, LaunchHeadlessEngine) {
auto& context = GetContext();
WindowsConfigBuilder builder(context);
EnginePtr engine{builder.InitializeEngine()};
EnginePtr engine{builder.RunHeadless()};
ASSERT_NE(engine, nullptr);

ASSERT_TRUE(FlutterDesktopEngineRun(engine.get(), nullptr));
}

// Verify that accessibility features are initialized when a view is created.
Expand Down
4 changes: 3 additions & 1 deletion shell/platform/windows/testing/engine_modifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class EngineModifier {

// Override the engine's implicit view. This is the "default" view
// that Flutter apps render to.
void SetImplicitView(FlutterWindowsView* view) { engine_->view_ = view; }
void SetImplicitView(FlutterWindowsView* view) {
engine_->views_[kImplicitViewId] = view;
}

/// Reset the start_time field that is used to align vsync events.
void SetStartTime(uint64_t start_time_nanos) {
Expand Down
25 changes: 23 additions & 2 deletions shell/platform/windows/testing/windows_test_config_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ EnginePtr WindowsConfigBuilder::InitializeEngine() const {
return EnginePtr(FlutterDesktopEngineCreate(&engine_properties));
}

EnginePtr WindowsConfigBuilder::RunHeadless() const {
InitializeCOM();

EnginePtr engine = InitializeEngine();
if (!engine) {
return {};
}

// Register native functions.
FlutterWindowsEngine* windows_engine =
reinterpret_cast<FlutterWindowsEngine*>(engine.get());
windows_engine->SetRootIsolateCreateCallback(
context_.GetRootIsolateCallback());

if (!FlutterDesktopEngineRun(engine.get(), /* entry_point */ nullptr)) {
return {};
}

return engine;
}

ViewControllerPtr WindowsConfigBuilder::Run() const {
InitializeCOM();

Expand All @@ -87,8 +108,8 @@ ViewControllerPtr WindowsConfigBuilder::Run() const {

int width = 600;
int height = 400;
ViewControllerPtr controller(
FlutterDesktopViewControllerCreate(width, height, engine.release()));
ViewControllerPtr controller{
FlutterDesktopViewControllerCreate(width, height, engine.release())};
if (!controller) {
return {};
}
Expand Down
6 changes: 5 additions & 1 deletion shell/platform/windows/testing/windows_test_config_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ class WindowsConfigBuilder {
// Returns a configured and initialized engine.
EnginePtr InitializeEngine() const;

// Returns a configured and initialized view controller running the default
// Returns a configured and initialized engine running the
// Dart entrypoint.
EnginePtr RunHeadless() const;

// Returns a configured and initialized view controller running the
// Dart entrypoint.
ViewControllerPtr Run() const;

Expand Down