Skip to content

Commit b1d0ffb

Browse files
knoppmattkaehbatageloloic-sharma
authored
[Windows] Make lifecycle manager updates atomic (#164872)
Required for multi-window. On windows the `LifecycleManager` currently sends the lifecycle event as soon as windows message is processed. This however causes problems when changing focus between application windows. When switching focus from HWND1 to HWND2, HWND1 first gets unfocused, followed by HWND2 getting focused. After HWND1 gets unfocused, `LifecycleManager` immediately notifies the framework that the application is inactive, which is wrong as the application never went into inactive state, followed by subsequent call to put the application in resumed state when HWND2 is focused. Because this happens very quickly, sometimes focus manager gets into inconsistent state. To resolve this `LifecycleManager` should gather the all the changes while window sends the messages and then notify the framework atomically in next run loop turn. This PR also simplifies the logic in `LifecycleManager` through which the application state is derived from window states. This PR removes engine forcing `resumed` lifecycle state at startup. I'm not entirely sure what the point of this was - the state can and should be determined solely from window states, this just seems to muddy the state logic. Also it happens before the framework is even listening to state changes. The mutex in `WindowsLifecycleManager` is removed. Not sure why it was there. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] 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 --------- Co-authored-by: Matthew Kosarek <matt.kosarek@canonical.com> Co-authored-by: Harlen Batagelo <hbatagelo@gmail.com> Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
1 parent 91d195f commit b1d0ffb

7 files changed

+120
-74
lines changed

engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,6 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
489489
displays.data(), displays.size());
490490

491491
SendSystemLocales();
492-
SetLifecycleState(flutter::AppLifecycleState::kResumed);
493492

494493
settings_plugin_->StartWatching();
495494
settings_plugin_->SendSettings();
@@ -802,12 +801,6 @@ void FlutterWindowsEngine::SetNextFrameCallback(fml::closure callback) {
802801
this);
803802
}
804803

805-
void FlutterWindowsEngine::SetLifecycleState(flutter::AppLifecycleState state) {
806-
if (lifecycle_manager_) {
807-
lifecycle_manager_->SetLifecycleState(state);
808-
}
809-
}
810-
811804
void FlutterWindowsEngine::SendSystemLocales() {
812805
std::vector<LanguageInfo> languages =
813806
GetPreferredLanguageInfo(*windows_proc_table_);

engine/src/flutter/shell/platform/windows/flutter_windows_engine.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,6 @@ class FlutterWindowsEngine {
347347
// system changes.
348348
void SendSystemLocales();
349349

350-
// Sends the current lifecycle state to the framework.
351-
void SetLifecycleState(flutter::AppLifecycleState state);
352-
353350
// Create the keyboard & text input sub-systems.
354351
//
355352
// This requires that a view is attached to the engine.

engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,6 @@ TEST_F(FlutterWindowsEngineTest, TestExit) {
800800
modifier.SetImplicitView(&view);
801801
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
802802
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
803-
EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed));
804803
EXPECT_CALL(*handler, Quit)
805804
.WillOnce([&finished](std::optional<HWND> hwnd,
806805
std::optional<WPARAM> wparam,
@@ -837,7 +836,6 @@ TEST_F(FlutterWindowsEngineTest, TestExitCancel) {
837836
modifier.SetImplicitView(&view);
838837
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
839838
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
840-
EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed));
841839
EXPECT_CALL(*handler, IsLastWindowOfProcess).WillRepeatedly(Return(true));
842840
EXPECT_CALL(*handler, Quit).Times(0);
843841
modifier.SetLifecycleManager(std::move(handler));
@@ -885,7 +883,6 @@ TEST_F(FlutterWindowsEngineTest, TestExitSecondCloseMessage) {
885883
modifier.SetImplicitView(&view);
886884
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
887885
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
888-
EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed));
889886
EXPECT_CALL(*handler, IsLastWindowOfProcess).WillOnce(Return(true));
890887
EXPECT_CALL(*handler, Quit)
891888
.WillOnce([handler_ptr = handler.get()](
@@ -945,7 +942,6 @@ TEST_F(FlutterWindowsEngineTest, TestExitCloseMultiWindow) {
945942
modifier.SetImplicitView(&view);
946943
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
947944
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
948-
EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed));
949945
EXPECT_CALL(*handler, IsLastWindowOfProcess).WillOnce([&finished]() {
950946
finished = true;
951947
return false;
@@ -1023,24 +1019,6 @@ TEST_F(FlutterWindowsEngineTest, ApplicationLifecycleExternalWindow) {
10231019
engine->lifecycle_manager()->ExternalWindowMessage(0, WM_CLOSE, 0, 0);
10241020
}
10251021

1026-
TEST_F(FlutterWindowsEngineTest, AppStartsInResumedState) {
1027-
FlutterWindowsEngineBuilder builder{GetContext()};
1028-
1029-
auto engine = builder.Build();
1030-
auto window_binding_handler =
1031-
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
1032-
MockFlutterWindowsView view(engine.get(), std::move(window_binding_handler));
1033-
1034-
EngineModifier modifier(engine.get());
1035-
modifier.SetImplicitView(&view);
1036-
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
1037-
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
1038-
EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed))
1039-
.Times(1);
1040-
modifier.SetLifecycleManager(std::move(handler));
1041-
engine->Run();
1042-
}
1043-
10441022
TEST_F(FlutterWindowsEngineTest, LifecycleStateTransition) {
10451023
FlutterWindowsEngineBuilder builder{GetContext()};
10461024

@@ -1056,16 +1034,41 @@ TEST_F(FlutterWindowsEngineTest, LifecycleStateTransition) {
10561034

10571035
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
10581036
(HWND)1, WM_SIZE, SIZE_RESTORED, 0);
1037+
1038+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1039+
PumpMessage();
1040+
}
1041+
1042+
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
1043+
AppLifecycleState::kInactive);
1044+
1045+
engine->lifecycle_manager()->OnWindowStateEvent((HWND)1,
1046+
WindowStateEvent::kFocus);
1047+
1048+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1049+
PumpMessage();
1050+
}
1051+
10591052
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
10601053
AppLifecycleState::kResumed);
10611054

10621055
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
10631056
(HWND)1, WM_SIZE, SIZE_MINIMIZED, 0);
1057+
1058+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1059+
PumpMessage();
1060+
}
1061+
10641062
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
10651063
AppLifecycleState::kHidden);
10661064

10671065
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
10681066
(HWND)1, WM_SIZE, SIZE_RESTORED, 0);
1067+
1068+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1069+
PumpMessage();
1070+
}
1071+
10691072
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
10701073
AppLifecycleState::kInactive);
10711074
}
@@ -1090,6 +1093,10 @@ TEST_F(FlutterWindowsEngineTest, ExternalWindowMessage) {
10901093
engine->ProcessExternalWindowMessage(reinterpret_cast<HWND>(1), WM_SHOWWINDOW,
10911094
FALSE, NULL);
10921095

1096+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1097+
PumpMessage();
1098+
}
1099+
10931100
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
10941101
AppLifecycleState::kHidden);
10951102
}
@@ -1117,12 +1124,20 @@ TEST_F(FlutterWindowsEngineTest, InnerWindowHidden) {
11171124
view.OnWindowStateEvent(inner, WindowStateEvent::kShow);
11181125
view.OnWindowStateEvent(inner, WindowStateEvent::kFocus);
11191126

1127+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1128+
PumpMessage();
1129+
}
1130+
11201131
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
11211132
AppLifecycleState::kResumed);
11221133

11231134
// Hide Flutter window, but not top level window.
11241135
view.OnWindowStateEvent(inner, WindowStateEvent::kHide);
11251136

1137+
while (engine->lifecycle_manager()->IsUpdateStateScheduled()) {
1138+
PumpMessage();
1139+
}
1140+
11261141
// The top-level window is still visible, so we ought not enter hidden state.
11271142
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
11281143
AppLifecycleState::kInactive);
@@ -1244,7 +1259,6 @@ TEST_F(FlutterWindowsEngineTest, ChannelListenedTo) {
12441259

12451260
bool lifecycle_began = false;
12461261
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
1247-
EXPECT_CALL(*handler, SetLifecycleState).Times(1);
12481262
handler->begin_processing_callback = [&]() { lifecycle_began = true; };
12491263
modifier.SetLifecycleManager(std::move(handler));
12501264

engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ TEST_F(WindowsTest, Lifecycle) {
535535
modifier.SetLifecycleManager(std::move(lifecycle_manager));
536536

537537
EXPECT_CALL(*lifecycle_manager_ptr,
538-
SetLifecycleState(AppLifecycleState::kResumed))
538+
SetLifecycleState(AppLifecycleState::kInactive))
539539
.WillOnce([lifecycle_manager_ptr](AppLifecycleState state) {
540540
lifecycle_manager_ptr->WindowsLifecycleManager::SetLifecycleState(
541541
state);
@@ -548,10 +548,12 @@ TEST_F(WindowsTest, Lifecycle) {
548548
state);
549549
});
550550

551+
FlutterDesktopViewControllerProperties properties = {0, 0};
552+
551553
// Create a controller. This launches the engine and sets the app lifecycle
552554
// to the "resumed" state.
553555
ViewControllerPtr controller{
554-
FlutterDesktopViewControllerCreate(0, 0, engine.release())};
556+
FlutterDesktopEngineCreateViewController(engine.get(), &properties)};
555557

556558
FlutterDesktopViewRef view =
557559
FlutterDesktopViewControllerGetView(controller.get());
@@ -565,6 +567,17 @@ TEST_F(WindowsTest, Lifecycle) {
565567
// "hidden" app lifecycle event.
566568
::MoveWindow(hwnd, /* X */ 0, /* Y */ 0, /* nWidth*/ 100, /* nHeight*/ 100,
567569
/* bRepaint*/ false);
570+
571+
while (lifecycle_manager_ptr->IsUpdateStateScheduled()) {
572+
PumpMessage();
573+
}
574+
575+
// Resets the view, simulating the window being hidden.
576+
controller.reset();
577+
578+
while (lifecycle_manager_ptr->IsUpdateStateScheduled()) {
579+
PumpMessage();
580+
}
568581
}
569582

570583
TEST_F(WindowsTest, GetKeyboardStateHeadless) {

engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -205,49 +205,48 @@ void WindowsLifecycleManager::SetLifecycleState(AppLifecycleState state) {
205205
}
206206
}
207207

208+
void WindowsLifecycleManager::UpdateState() {
209+
AppLifecycleState new_state = AppLifecycleState::kResumed;
210+
if (visible_windows_.empty()) {
211+
new_state = AppLifecycleState::kHidden;
212+
} else if (focused_windows_.empty()) {
213+
new_state = AppLifecycleState::kInactive;
214+
}
215+
SetLifecycleState(new_state);
216+
}
217+
208218
void WindowsLifecycleManager::OnWindowStateEvent(HWND hwnd,
209219
WindowStateEvent event) {
210-
// Synthesize an unfocus event when a focused window is hidden.
211-
if (event == WindowStateEvent::kHide &&
212-
focused_windows_.find(hwnd) != focused_windows_.end()) {
213-
OnWindowStateEvent(hwnd, WindowStateEvent::kUnfocus);
220+
// Instead of updating the state immediately, remember all
221+
// changes to individual window and then update the state in next run loop
222+
// turn. Otherwise the application would be temporarily deactivated when
223+
// switching focus between windows for example.
224+
if (!update_state_scheduled_) {
225+
update_state_scheduled_ = true;
226+
// Task runner will be destroyed together with engine so it is safe
227+
// to keep reference to it.
228+
engine_->task_runner()->PostTask([this]() {
229+
update_state_scheduled_ = false;
230+
UpdateState();
231+
});
214232
}
215233

216-
std::lock_guard guard(state_update_lock_);
217234
switch (event) {
218235
case WindowStateEvent::kShow: {
219-
bool first_shown_window = visible_windows_.empty();
220-
auto pair = visible_windows_.insert(hwnd);
221-
if (first_shown_window && pair.second &&
222-
state_ == AppLifecycleState::kHidden) {
223-
SetLifecycleState(AppLifecycleState::kInactive);
224-
}
236+
visible_windows_.insert(hwnd);
225237
break;
226238
}
227239
case WindowStateEvent::kHide: {
228-
bool present = visible_windows_.erase(hwnd);
229-
bool empty = visible_windows_.empty();
230-
if (present && empty &&
231-
(state_ == AppLifecycleState::kResumed ||
232-
state_ == AppLifecycleState::kInactive)) {
233-
SetLifecycleState(AppLifecycleState::kHidden);
234-
}
240+
visible_windows_.erase(hwnd);
241+
focused_windows_.erase(hwnd);
235242
break;
236243
}
237244
case WindowStateEvent::kFocus: {
238-
bool first_focused_window = focused_windows_.empty();
239-
auto pair = focused_windows_.insert(hwnd);
240-
if (first_focused_window && pair.second &&
241-
state_ == AppLifecycleState::kInactive) {
242-
SetLifecycleState(AppLifecycleState::kResumed);
243-
}
245+
focused_windows_.insert(hwnd);
244246
break;
245247
}
246248
case WindowStateEvent::kUnfocus: {
247-
if (focused_windows_.erase(hwnd) && focused_windows_.empty() &&
248-
state_ == AppLifecycleState::kResumed) {
249-
SetLifecycleState(AppLifecycleState::kInactive);
250-
}
249+
focused_windows_.erase(hwnd);
251250
break;
252251
}
253252
}

engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,16 @@ class WindowsLifecycleManager {
6363
// message to the framework notifying it of the state change.
6464
virtual void SetLifecycleState(AppLifecycleState state);
6565

66-
// Respond to a change in window state. Transitions as follows:
67-
// When the only visible window is hidden, transition from resumed or
68-
// inactive to hidden.
69-
// When the only focused window is unfocused, transition from resumed to
70-
// inactive.
71-
// When a window is focused, transition from inactive to resumed.
72-
// When a window is shown, transition from hidden to inactive.
66+
// Respond to a change in window state.
67+
// Saves the state for the HWND and schedules UpdateState to be called
68+
// if it is not already scheduled.
7369
virtual void OnWindowStateEvent(HWND hwnd, WindowStateEvent event);
7470

7571
AppLifecycleState GetLifecycleState() { return state_; }
7672

73+
// Used in tests to wait until the state is updated.
74+
bool IsUpdateStateScheduled() const { return update_state_scheduled_; }
75+
7776
// Called by the engine when a non-Flutter window receives an event that may
7877
// alter the lifecycle state. The logic for external windows must differ from
7978
// that used for FlutterWindow instances, because:
@@ -114,12 +113,20 @@ class WindowsLifecycleManager {
114113
bool process_exit_ = false;
115114

116115
std::set<HWND> visible_windows_;
117-
118116
std::set<HWND> focused_windows_;
119117

120-
std::mutex state_update_lock_;
118+
// Transitions the application state. If any windows are focused,
119+
// the application is considered resumed. If no windows are focused
120+
// but there are visible windows, application is considered inactive.
121+
// Otherwise, if there are no visible window, application is considered
122+
// hidden.
123+
void UpdateState();
124+
125+
// Whether update state is scheduled to be called in next run loop turn.
126+
// This is needed to provide atomic updates of the state.
127+
bool update_state_scheduled_ = false;
121128

122-
flutter::AppLifecycleState state_;
129+
AppLifecycleState state_ = AppLifecycleState::kDetached;
123130
};
124131

125132
} // namespace flutter

0 commit comments

Comments
 (0)