diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index 2833fe0fc7b..e8033bd098c 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -489,7 +489,6 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { displays.data(), displays.size()); SendSystemLocales(); - SetLifecycleState(flutter::AppLifecycleState::kResumed); settings_plugin_->StartWatching(); settings_plugin_->SendSettings(); @@ -802,12 +801,6 @@ void FlutterWindowsEngine::SetNextFrameCallback(fml::closure callback) { this); } -void FlutterWindowsEngine::SetLifecycleState(flutter::AppLifecycleState state) { - if (lifecycle_manager_) { - lifecycle_manager_->SetLifecycleState(state); - } -} - void FlutterWindowsEngine::SendSystemLocales() { std::vector languages = GetPreferredLanguageInfo(*windows_proc_table_); diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h index 8b68ee15f64..739912d943c 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h @@ -347,9 +347,6 @@ class FlutterWindowsEngine { // system changes. void SendSystemLocales(); - // Sends the current lifecycle state to the framework. - void SetLifecycleState(flutter::AppLifecycleState state); - // Create the keyboard & text input sub-systems. // // This requires that a view is attached to the engine. diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc index bda12c2674c..03b2131ad18 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -800,7 +800,6 @@ TEST_F(FlutterWindowsEngineTest, TestExit) { modifier.SetImplicitView(&view); modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; auto handler = std::make_unique(engine.get()); - EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed)); EXPECT_CALL(*handler, Quit) .WillOnce([&finished](std::optional hwnd, std::optional wparam, @@ -837,7 +836,6 @@ TEST_F(FlutterWindowsEngineTest, TestExitCancel) { modifier.SetImplicitView(&view); modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; auto handler = std::make_unique(engine.get()); - EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed)); EXPECT_CALL(*handler, IsLastWindowOfProcess).WillRepeatedly(Return(true)); EXPECT_CALL(*handler, Quit).Times(0); modifier.SetLifecycleManager(std::move(handler)); @@ -885,7 +883,6 @@ TEST_F(FlutterWindowsEngineTest, TestExitSecondCloseMessage) { modifier.SetImplicitView(&view); modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; auto handler = std::make_unique(engine.get()); - EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed)); EXPECT_CALL(*handler, IsLastWindowOfProcess).WillOnce(Return(true)); EXPECT_CALL(*handler, Quit) .WillOnce([handler_ptr = handler.get()]( @@ -945,7 +942,6 @@ TEST_F(FlutterWindowsEngineTest, TestExitCloseMultiWindow) { modifier.SetImplicitView(&view); modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; auto handler = std::make_unique(engine.get()); - EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed)); EXPECT_CALL(*handler, IsLastWindowOfProcess).WillOnce([&finished]() { finished = true; return false; @@ -1023,24 +1019,6 @@ TEST_F(FlutterWindowsEngineTest, ApplicationLifecycleExternalWindow) { engine->lifecycle_manager()->ExternalWindowMessage(0, WM_CLOSE, 0, 0); } -TEST_F(FlutterWindowsEngineTest, AppStartsInResumedState) { - FlutterWindowsEngineBuilder builder{GetContext()}; - - auto engine = builder.Build(); - auto window_binding_handler = - std::make_unique<::testing::NiceMock>(); - MockFlutterWindowsView view(engine.get(), std::move(window_binding_handler)); - - EngineModifier modifier(engine.get()); - modifier.SetImplicitView(&view); - modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; - auto handler = std::make_unique(engine.get()); - EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed)) - .Times(1); - modifier.SetLifecycleManager(std::move(handler)); - engine->Run(); -} - TEST_F(FlutterWindowsEngineTest, LifecycleStateTransition) { FlutterWindowsEngineBuilder builder{GetContext()}; @@ -1056,16 +1034,41 @@ TEST_F(FlutterWindowsEngineTest, LifecycleStateTransition) { engine->window_proc_delegate_manager()->OnTopLevelWindowProc( (HWND)1, WM_SIZE, SIZE_RESTORED, 0); + + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + + EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), + AppLifecycleState::kInactive); + + engine->lifecycle_manager()->OnWindowStateEvent((HWND)1, + WindowStateEvent::kFocus); + + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), AppLifecycleState::kResumed); engine->window_proc_delegate_manager()->OnTopLevelWindowProc( (HWND)1, WM_SIZE, SIZE_MINIMIZED, 0); + + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), AppLifecycleState::kHidden); engine->window_proc_delegate_manager()->OnTopLevelWindowProc( (HWND)1, WM_SIZE, SIZE_RESTORED, 0); + + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), AppLifecycleState::kInactive); } @@ -1090,6 +1093,10 @@ TEST_F(FlutterWindowsEngineTest, ExternalWindowMessage) { engine->ProcessExternalWindowMessage(reinterpret_cast(1), WM_SHOWWINDOW, FALSE, NULL); + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), AppLifecycleState::kHidden); } @@ -1117,12 +1124,20 @@ TEST_F(FlutterWindowsEngineTest, InnerWindowHidden) { view.OnWindowStateEvent(inner, WindowStateEvent::kShow); view.OnWindowStateEvent(inner, WindowStateEvent::kFocus); + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), AppLifecycleState::kResumed); // Hide Flutter window, but not top level window. view.OnWindowStateEvent(inner, WindowStateEvent::kHide); + while (engine->lifecycle_manager()->IsUpdateStateScheduled()) { + PumpMessage(); + } + // The top-level window is still visible, so we ought not enter hidden state. EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(), AppLifecycleState::kInactive); @@ -1244,7 +1259,6 @@ TEST_F(FlutterWindowsEngineTest, ChannelListenedTo) { bool lifecycle_began = false; auto handler = std::make_unique(engine.get()); - EXPECT_CALL(*handler, SetLifecycleState).Times(1); handler->begin_processing_callback = [&]() { lifecycle_began = true; }; modifier.SetLifecycleManager(std::move(handler)); diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc index 25a336c8c99..710a0836199 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc @@ -535,7 +535,7 @@ TEST_F(WindowsTest, Lifecycle) { modifier.SetLifecycleManager(std::move(lifecycle_manager)); EXPECT_CALL(*lifecycle_manager_ptr, - SetLifecycleState(AppLifecycleState::kResumed)) + SetLifecycleState(AppLifecycleState::kInactive)) .WillOnce([lifecycle_manager_ptr](AppLifecycleState state) { lifecycle_manager_ptr->WindowsLifecycleManager::SetLifecycleState( state); @@ -548,10 +548,12 @@ TEST_F(WindowsTest, Lifecycle) { state); }); + FlutterDesktopViewControllerProperties properties = {0, 0}; + // Create a controller. This launches the engine and sets the app lifecycle // to the "resumed" state. ViewControllerPtr controller{ - FlutterDesktopViewControllerCreate(0, 0, engine.release())}; + FlutterDesktopEngineCreateViewController(engine.get(), &properties)}; FlutterDesktopViewRef view = FlutterDesktopViewControllerGetView(controller.get()); @@ -565,6 +567,17 @@ TEST_F(WindowsTest, Lifecycle) { // "hidden" app lifecycle event. ::MoveWindow(hwnd, /* X */ 0, /* Y */ 0, /* nWidth*/ 100, /* nHeight*/ 100, /* bRepaint*/ false); + + while (lifecycle_manager_ptr->IsUpdateStateScheduled()) { + PumpMessage(); + } + + // Resets the view, simulating the window being hidden. + controller.reset(); + + while (lifecycle_manager_ptr->IsUpdateStateScheduled()) { + PumpMessage(); + } } TEST_F(WindowsTest, GetKeyboardStateHeadless) { diff --git a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc index afa571a507f..669bcbec283 100644 --- a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc +++ b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc @@ -205,49 +205,48 @@ void WindowsLifecycleManager::SetLifecycleState(AppLifecycleState state) { } } +void WindowsLifecycleManager::UpdateState() { + AppLifecycleState new_state = AppLifecycleState::kResumed; + if (visible_windows_.empty()) { + new_state = AppLifecycleState::kHidden; + } else if (focused_windows_.empty()) { + new_state = AppLifecycleState::kInactive; + } + SetLifecycleState(new_state); +} + void WindowsLifecycleManager::OnWindowStateEvent(HWND hwnd, WindowStateEvent event) { - // Synthesize an unfocus event when a focused window is hidden. - if (event == WindowStateEvent::kHide && - focused_windows_.find(hwnd) != focused_windows_.end()) { - OnWindowStateEvent(hwnd, WindowStateEvent::kUnfocus); + // Instead of updating the state immediately, remember all + // changes to individual window and then update the state in next run loop + // turn. Otherwise the application would be temporarily deactivated when + // switching focus between windows for example. + if (!update_state_scheduled_) { + update_state_scheduled_ = true; + // Task runner will be destroyed together with engine so it is safe + // to keep reference to it. + engine_->task_runner()->PostTask([this]() { + update_state_scheduled_ = false; + UpdateState(); + }); } - std::lock_guard guard(state_update_lock_); switch (event) { case WindowStateEvent::kShow: { - bool first_shown_window = visible_windows_.empty(); - auto pair = visible_windows_.insert(hwnd); - if (first_shown_window && pair.second && - state_ == AppLifecycleState::kHidden) { - SetLifecycleState(AppLifecycleState::kInactive); - } + visible_windows_.insert(hwnd); break; } case WindowStateEvent::kHide: { - bool present = visible_windows_.erase(hwnd); - bool empty = visible_windows_.empty(); - if (present && empty && - (state_ == AppLifecycleState::kResumed || - state_ == AppLifecycleState::kInactive)) { - SetLifecycleState(AppLifecycleState::kHidden); - } + visible_windows_.erase(hwnd); + focused_windows_.erase(hwnd); break; } case WindowStateEvent::kFocus: { - bool first_focused_window = focused_windows_.empty(); - auto pair = focused_windows_.insert(hwnd); - if (first_focused_window && pair.second && - state_ == AppLifecycleState::kInactive) { - SetLifecycleState(AppLifecycleState::kResumed); - } + focused_windows_.insert(hwnd); break; } case WindowStateEvent::kUnfocus: { - if (focused_windows_.erase(hwnd) && focused_windows_.empty() && - state_ == AppLifecycleState::kResumed) { - SetLifecycleState(AppLifecycleState::kInactive); - } + focused_windows_.erase(hwnd); break; } } diff --git a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h index fc20b382467..705020aea4f 100644 --- a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h +++ b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h @@ -63,17 +63,16 @@ class WindowsLifecycleManager { // message to the framework notifying it of the state change. virtual void SetLifecycleState(AppLifecycleState state); - // Respond to a change in window state. Transitions as follows: - // When the only visible window is hidden, transition from resumed or - // inactive to hidden. - // When the only focused window is unfocused, transition from resumed to - // inactive. - // When a window is focused, transition from inactive to resumed. - // When a window is shown, transition from hidden to inactive. + // Respond to a change in window state. + // Saves the state for the HWND and schedules UpdateState to be called + // if it is not already scheduled. virtual void OnWindowStateEvent(HWND hwnd, WindowStateEvent event); AppLifecycleState GetLifecycleState() { return state_; } + // Used in tests to wait until the state is updated. + bool IsUpdateStateScheduled() const { return update_state_scheduled_; } + // Called by the engine when a non-Flutter window receives an event that may // alter the lifecycle state. The logic for external windows must differ from // that used for FlutterWindow instances, because: @@ -114,12 +113,20 @@ class WindowsLifecycleManager { bool process_exit_ = false; std::set visible_windows_; - std::set focused_windows_; - std::mutex state_update_lock_; + // Transitions the application state. If any windows are focused, + // the application is considered resumed. If no windows are focused + // but there are visible windows, application is considered inactive. + // Otherwise, if there are no visible window, application is considered + // hidden. + void UpdateState(); - flutter::AppLifecycleState state_; + // Whether update state is scheduled to be called in next run loop turn. + // This is needed to provide atomic updates of the state. + bool update_state_scheduled_ = false; + + AppLifecycleState state_ = AppLifecycleState::kDetached; }; } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc index 6414d0ade78..f658a8999ce 100644 --- a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc @@ -4,6 +4,7 @@ #include "flutter/shell/platform/windows/windows_lifecycle_manager.h" +#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" #include "flutter/shell/platform/windows/testing/windows_test.h" #include "gtest/gtest.h" @@ -12,48 +13,70 @@ namespace testing { class WindowsLifecycleManagerTest : public WindowsTest {}; +static void WaitUntilUpdated(const WindowsLifecycleManager& manager) { + while (manager.IsUpdateStateScheduled()) { + ::MSG msg; + if (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } +} + TEST_F(WindowsLifecycleManagerTest, StateTransitions) { - WindowsLifecycleManager manager(nullptr); + FlutterWindowsEngineBuilder builder{GetContext()}; + std::unique_ptr engine = builder.Build(); + + WindowsLifecycleManager manager{engine.get()}; HWND win1 = reinterpret_cast(1); HWND win2 = reinterpret_cast(2); // Hidden to inactive upon window shown. manager.SetLifecycleState(AppLifecycleState::kHidden); manager.OnWindowStateEvent(win1, WindowStateEvent::kShow); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive); // Showing a second window does not change state. manager.OnWindowStateEvent(win2, WindowStateEvent::kShow); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive); // Inactive to resumed upon window focus. manager.OnWindowStateEvent(win2, WindowStateEvent::kFocus); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kResumed); // Showing a second window does not change state. manager.OnWindowStateEvent(win1, WindowStateEvent::kFocus); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kResumed); // Unfocusing one window does not change state while another is focused. manager.OnWindowStateEvent(win1, WindowStateEvent::kUnfocus); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kResumed); // Unfocusing final remaining focused window transitions to inactive. manager.OnWindowStateEvent(win2, WindowStateEvent::kUnfocus); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive); // Hiding one of two visible windows does not change state. manager.OnWindowStateEvent(win2, WindowStateEvent::kHide); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive); // Hiding only visible window transitions to hidden. manager.OnWindowStateEvent(win1, WindowStateEvent::kHide); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kHidden); // Transition directly from resumed to hidden when the window is hidden. manager.OnWindowStateEvent(win1, WindowStateEvent::kShow); manager.OnWindowStateEvent(win1, WindowStateEvent::kFocus); manager.OnWindowStateEvent(win1, WindowStateEvent::kHide); + WaitUntilUpdated(manager); EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kHidden); }