Reintroduce Windows lifecycle with guard for posthumous OnWindowStateEvent (flutter/engine#44344)

Previously, destruction of `Window` called `DestroyWindow`, which may
send `WM_KILLFOCUS` to the to-be-destroyed window. Because `Window`'s
destructor is called after `FlutterWindow`'s, the `FlutterWindow` vtable
was already destroyed at this point, and the subsequent call to the
virtual method `OnWindowStateEvent` would cause a crash. This PR
reintroduces the reverted changes for Windows lifecycle with a check
before calling the virtual method that the `FlutterWindow` object has
not yet been destructed.

https://github.com/flutter/flutter/issues/131872

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## 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] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or Hixie said the PR is test-exempt. See [testing the engine]
for instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [ ] I signed the [CLA].
- [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/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat

---------

Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
This commit is contained in:
yaakovschectman 2023-08-10 15:37:04 -04:00 committed by GitHub
parent fb1851fe34
commit c2cc8508b6
25 changed files with 673 additions and 18 deletions

View File

@ -354,6 +354,7 @@
../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc
../../../flutter/shell/platform/windows/window_proc_delegate_manager_unittests.cc
../../../flutter/shell/platform/windows/window_unittests.cc
../../../flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc
../../../flutter/shell/profiling/sampling_profiler_unittest.cc
../../../flutter/shell/testing
../../../flutter/shell/vmservice/.dart_tool

View File

@ -221,6 +221,7 @@ executable("flutter_windows_unittests") {
"text_input_plugin_unittest.cc",
"window_proc_delegate_manager_unittests.cc",
"window_unittests.cc",
"windows_lifecycle_manager_unittests.cc",
]
configs +=

View File

@ -100,6 +100,19 @@ void FlutterEngine::SetNextFrameCallback(std::function<void()> callback) {
this);
}
std::optional<LRESULT> FlutterEngine::ProcessExternalWindowMessage(
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam) {
LRESULT result;
if (FlutterDesktopEngineProcessExternalWindowMessage(
engine_, hwnd, message, wparam, lparam, &result)) {
return result;
}
return std::nullopt;
}
FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() {
owns_engine_ = false;
return engine_;

View File

@ -56,6 +56,17 @@ class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi {
// |flutter::testing::StubFlutterWindowsApi|
void EngineReloadSystemFonts() override { reload_fonts_called_ = true; }
// |flutter::testing::StubFlutterWindowsApi|
bool EngineProcessExternalWindowMessage(FlutterDesktopEngineRef engine,
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result) override {
last_external_message_ = message;
return false;
}
bool create_called() { return create_called_; }
bool run_called() { return run_called_; }
@ -74,6 +85,8 @@ class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi {
next_frame_callback_ = nullptr;
}
UINT last_external_message() { return last_external_message_; }
private:
bool create_called_ = false;
bool run_called_ = false;
@ -82,6 +95,7 @@ class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi {
std::vector<std::string> dart_entrypoint_arguments_;
VoidCallback next_frame_callback_ = nullptr;
void* next_frame_user_data_ = nullptr;
UINT last_external_message_ = 0;
};
} // namespace
@ -201,4 +215,16 @@ TEST(FlutterEngineTest, SetNextFrameCallback) {
EXPECT_TRUE(success);
}
TEST(FlutterEngineTest, ProcessExternalWindowMessage) {
testing::ScopedStubFlutterWindowsApi scoped_api_stub(
std::make_unique<TestFlutterWindowsApi>());
auto test_api = static_cast<TestFlutterWindowsApi*>(scoped_api_stub.stub());
FlutterEngine engine(DartProject(L"fake/project/path"));
engine.ProcessExternalWindowMessage(reinterpret_cast<HWND>(1), 1234, 0, 0);
EXPECT_EQ(test_api->last_external_message(), 1234);
}
} // namespace flutter

View File

@ -9,6 +9,7 @@
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include "binary_messenger.h"
@ -84,6 +85,15 @@ class FlutterEngine : public PluginRegistry {
// once on the platform thread.
void SetNextFrameCallback(std::function<void()> callback);
// Called to pass an external window message to the engine for lifecycle
// state updates. Non-Flutter windows must call this method in their WndProc
// in order to be included in the logic for application lifecycle state
// updates. Returns a result if the message should be consumed.
std::optional<LRESULT> ProcessExternalWindowMessage(HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam);
private:
// For access to RelinquishEngine.
friend class FlutterViewController;

View File

@ -162,6 +162,20 @@ IDXGIAdapter* FlutterDesktopViewGetGraphicsAdapter(FlutterDesktopViewRef view) {
return nullptr;
}
bool FlutterDesktopEngineProcessExternalWindowMessage(
FlutterDesktopEngineRef engine,
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result) {
if (s_stub_implementation) {
return s_stub_implementation->EngineProcessExternalWindowMessage(
engine, hwnd, message, wparam, lparam, result);
}
return false;
}
FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView(
FlutterDesktopPluginRegistrarRef controller) {
// The stub ignores this, so just return an arbitrary non-zero value.

View File

@ -89,6 +89,17 @@ class StubFlutterWindowsApi {
// FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate.
virtual void PluginRegistrarUnregisterTopLevelWindowProcDelegate(
FlutterDesktopWindowProcCallback delegate) {}
// Called for FlutterDesktopEngineProcessExternalWindowMessage.
virtual bool EngineProcessExternalWindowMessage(
FlutterDesktopEngineRef engine,
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result) {
return false;
}
};
// A test helper that owns a stub implementation, making it the test stub for

View File

@ -74,11 +74,20 @@ FlutterWindow::FlutterWindow(int width, int height)
current_cursor_ = ::LoadCursor(nullptr, IDC_ARROW);
}
FlutterWindow::~FlutterWindow() {}
FlutterWindow::~FlutterWindow() {
OnWindowStateEvent(WindowStateEvent::kHide);
Destroy();
}
void FlutterWindow::SetView(WindowBindingHandlerDelegate* window) {
binding_handler_delegate_ = window;
direct_manipulation_owner_->SetBindingHandlerDelegate(window);
if (restored_ && window) {
OnWindowStateEvent(WindowStateEvent::kShow);
}
if (focused_ && window) {
OnWindowStateEvent(WindowStateEvent::kFocus);
}
}
WindowsRenderTarget FlutterWindow::GetRenderTarget() {
@ -328,4 +337,26 @@ bool FlutterWindow::NeedsVSync() {
return true;
}
void FlutterWindow::OnWindowStateEvent(WindowStateEvent event) {
switch (event) {
case WindowStateEvent::kShow:
restored_ = true;
break;
case WindowStateEvent::kHide:
restored_ = false;
focused_ = false;
break;
case WindowStateEvent::kFocus:
focused_ = true;
break;
case WindowStateEvent::kUnfocus:
focused_ = false;
break;
}
HWND hwnd = GetPlatformWindow();
if (hwnd && binding_handler_delegate_) {
binding_handler_delegate_->OnWindowStateEvent(hwnd, event);
}
}
} // namespace flutter

View File

@ -162,6 +162,9 @@ class FlutterWindow : public Window, public WindowBindingHandler {
// |Window|
ui::AXFragmentRootDelegateWin* GetAxFragmentRootDelegate() override;
// |Window|
virtual void OnWindowStateEvent(WindowStateEvent event) override;
private:
// A pointer to a FlutterWindowsView that can be used to update engine
// windowing and input state.
@ -173,6 +176,12 @@ class FlutterWindow : public Window, public WindowBindingHandler {
// The cursor rect set by Flutter.
RECT cursor_rect_;
// The window receives resize and focus messages before its view is set, so
// these values cache the state of the window in the meantime so that the
// proper application lifecycle state can be updated once the view is set.
bool restored_ = false;
bool focused_ = false;
FML_DISALLOW_COPY_AND_ASSIGN(FlutterWindow);
};

View File

@ -23,11 +23,16 @@ static constexpr int32_t kDefaultPointerDeviceId = 0;
class MockFlutterWindow : public FlutterWindow {
public:
MockFlutterWindow() : FlutterWindow(800, 600) {
MockFlutterWindow(bool reset_view_on_exit = true)
: FlutterWindow(800, 600), reset_view_on_exit_(reset_view_on_exit) {
ON_CALL(*this, GetDpiScale())
.WillByDefault(Return(this->FlutterWindow::GetDpiScale()));
}
virtual ~MockFlutterWindow() {}
virtual ~MockFlutterWindow() {
if (reset_view_on_exit_) {
SetView(nullptr);
}
}
// Wrapper for GetCurrentDPI() which is a protected method.
UINT GetDpi() { return GetCurrentDPI(); }
@ -61,6 +66,7 @@ class MockFlutterWindow : public FlutterWindow {
MOCK_METHOD1(Win32MapVkToChar, uint32_t(uint32_t));
MOCK_METHOD0(GetPlatformWindow, HWND());
MOCK_METHOD0(GetAxFragmentRootDelegate, ui::AXFragmentRootDelegateWin*());
MOCK_METHOD1(OnWindowStateEvent, void(WindowStateEvent));
protected:
// |KeyboardManager::WindowDelegate|
@ -72,6 +78,7 @@ class MockFlutterWindow : public FlutterWindow {
}
private:
bool reset_view_on_exit_;
FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindow);
};
@ -229,6 +236,10 @@ TEST(FlutterWindowTest, OnPointerStarSendsDeviceType) {
kDefaultPointerDeviceId, WM_LBUTTONDOWN);
win32window.OnPointerLeave(10.0, 10.0, kFlutterPointerDeviceKindStylus,
kDefaultPointerDeviceId);
// Destruction of win32window sends a HIDE update. In situ, the window is
// owned by the delegate, and so is destructed first. Not so here.
win32window.SetView(nullptr);
}
// Tests that calls to OnScroll in turn calls GetScrollOffsetMultiplier
@ -324,5 +335,100 @@ TEST(FlutterWindowTest, AlertNode) {
EXPECT_EQ(role.lVal, ROLE_SYSTEM_ALERT);
}
TEST(FlutterWindowTest, LifecycleFocusMessages) {
MockFlutterWindow win32window;
ON_CALL(win32window, GetPlatformWindow).WillByDefault([]() {
return reinterpret_cast<HWND>(1);
});
MockWindowBindingHandlerDelegate delegate;
win32window.SetView(&delegate);
WindowStateEvent last_event;
ON_CALL(delegate, OnWindowStateEvent)
.WillByDefault([&last_event](HWND hwnd, WindowStateEvent event) {
last_event = event;
});
ON_CALL(win32window, OnWindowStateEvent)
.WillByDefault([&](WindowStateEvent event) {
win32window.FlutterWindow::OnWindowStateEvent(event);
});
win32window.InjectWindowMessage(WM_SIZE, 0, 0);
EXPECT_EQ(last_event, WindowStateEvent::kHide);
win32window.InjectWindowMessage(WM_SIZE, 0, MAKEWORD(1, 1));
EXPECT_EQ(last_event, WindowStateEvent::kShow);
win32window.InjectWindowMessage(WM_SETFOCUS, 0, 0);
EXPECT_EQ(last_event, WindowStateEvent::kFocus);
win32window.InjectWindowMessage(WM_KILLFOCUS, 0, 0);
EXPECT_EQ(last_event, WindowStateEvent::kUnfocus);
}
TEST(FlutterWindowTest, CachedLifecycleMessage) {
MockFlutterWindow win32window;
ON_CALL(win32window, GetPlatformWindow).WillByDefault([]() {
return reinterpret_cast<HWND>(1);
});
ON_CALL(win32window, OnWindowStateEvent)
.WillByDefault([&](WindowStateEvent event) {
win32window.FlutterWindow::OnWindowStateEvent(event);
});
// Restore
win32window.InjectWindowMessage(WM_SIZE, 0, MAKEWORD(1, 1));
// Focus
win32window.InjectWindowMessage(WM_SETFOCUS, 0, 0);
MockWindowBindingHandlerDelegate delegate;
bool focused = false;
bool restored = false;
ON_CALL(delegate, OnWindowStateEvent)
.WillByDefault([&](HWND hwnd, WindowStateEvent event) {
if (event == WindowStateEvent::kFocus) {
focused = true;
} else if (event == WindowStateEvent::kShow) {
restored = true;
}
});
win32window.SetView(&delegate);
EXPECT_TRUE(focused);
EXPECT_TRUE(restored);
}
TEST(FlutterWindowTest, PosthumousWindowMessage) {
MockWindowBindingHandlerDelegate delegate;
int msg_count = 0;
HWND hwnd;
ON_CALL(delegate, OnWindowStateEvent)
.WillByDefault([&](HWND hwnd, WindowStateEvent event) { msg_count++; });
{
MockFlutterWindow win32window(false);
ON_CALL(win32window, GetPlatformWindow).WillByDefault([&]() {
return win32window.FlutterWindow::GetPlatformWindow();
});
ON_CALL(win32window, OnWindowStateEvent)
.WillByDefault([&](WindowStateEvent event) {
win32window.FlutterWindow::OnWindowStateEvent(event);
});
win32window.SetView(&delegate);
win32window.InitializeChild("Title", 1, 1);
hwnd = win32window.GetPlatformWindow();
SendMessage(hwnd, WM_SIZE, 0, MAKEWORD(1, 1));
SendMessage(hwnd, WM_SETFOCUS, 0, 0);
// By setting this to zero before exiting the scope that contains
// win32window, and then checking its value afterwards, enforce that the
// window receive and process messages from its destructor without
// accessing out-of-bounds memory.
msg_count = 0;
}
EXPECT_GE(msg_count, 1);
}
} // namespace testing
} // namespace flutter

View File

@ -207,6 +207,22 @@ IDXGIAdapter* FlutterDesktopViewGetGraphicsAdapter(FlutterDesktopViewRef view) {
return nullptr;
}
bool FlutterDesktopEngineProcessExternalWindowMessage(
FlutterDesktopEngineRef engine,
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result) {
std::optional<LRESULT> lresult =
EngineFromHandle(engine)->ProcessExternalWindowMessage(hwnd, message,
wparam, lparam);
if (result && lresult.has_value()) {
*result = lresult.value();
}
return lresult.has_value();
}
FlutterDesktopViewRef FlutterDesktopPluginRegistrarGetView(
FlutterDesktopPluginRegistrarRef registrar) {
return HandleForView(registrar->engine->view());

View File

@ -584,10 +584,9 @@ void FlutterWindowsEngine::SetNextFrameCallback(fml::closure callback) {
}
void FlutterWindowsEngine::SetLifecycleState(flutter::AppLifecycleState state) {
const char* state_name = flutter::AppLifecycleStateToString(state);
SendPlatformMessage("flutter/lifecycle",
reinterpret_cast<const uint8_t*>(state_name),
strlen(state_name), nullptr, nullptr);
if (lifecycle_manager_) {
lifecycle_manager_->SetLifecycleState(state);
}
}
void FlutterWindowsEngine::SendSystemLocales() {
@ -797,4 +796,21 @@ void FlutterWindowsEngine::OnApplicationLifecycleEnabled() {
lifecycle_manager_->BeginProcessingClose();
}
void FlutterWindowsEngine::OnWindowStateEvent(HWND hwnd,
WindowStateEvent event) {
lifecycle_manager_->OnWindowStateEvent(hwnd, event);
}
std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam) {
if (lifecycle_manager_) {
return lifecycle_manager_->ExternalWindowMessage(hwnd, message, wparam,
lparam);
}
return std::nullopt;
}
} // namespace flutter

View File

@ -262,6 +262,22 @@ class FlutterWindowsEngine {
// Registers the top level handler for the WM_CLOSE window message.
void OnApplicationLifecycleEnabled();
// Called when a Window receives an event that may alter the application
// lifecycle state.
void OnWindowStateEvent(HWND hwnd, WindowStateEvent event);
// Handle a message from a non-Flutter window in the same application.
// Returns a result when the message is consumed and should not be processed
// further.
std::optional<LRESULT> ProcessExternalWindowMessage(HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam);
WindowsLifecycleManager* lifecycle_manager() {
return lifecycle_manager_.get();
}
protected:
// Creates the keyboard key handler.
//

View File

@ -608,6 +608,7 @@ class MockFlutterWindowsView : public FlutterWindowsView {
MOCK_METHOD2(NotifyWinEventWrapper,
void(ui::AXPlatformNodeWin*, ax::mojom::Event));
MOCK_METHOD0(GetPlatformWindow, HWND());
private:
FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView);
@ -668,6 +669,7 @@ class MockWindowsLifecycleManager : public WindowsLifecycleManager {
UINT));
MOCK_METHOD4(DispatchMessage, void(HWND, UINT, WPARAM, LPARAM));
MOCK_METHOD0(IsLastWindowOfProcess, bool(void));
MOCK_METHOD1(SetLifecycleState, void(AppLifecycleState));
};
TEST_F(FlutterWindowsEngineTest, TestExit) {
@ -895,5 +897,110 @@ TEST_F(FlutterWindowsEngineTest, EnableApplicationLifecycle) {
0);
}
TEST_F(FlutterWindowsEngineTest, AppStartsInResumedState) {
FlutterWindowsEngineBuilder builder{GetContext()};
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
MockFlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(builder.Build());
FlutterWindowsEngine* engine = view.GetEngine();
EngineModifier modifier(engine);
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
auto handler = std::make_unique<MockWindowsLifecycleManager>(engine);
EXPECT_CALL(*handler, SetLifecycleState(AppLifecycleState::kResumed))
.Times(1);
modifier.SetLifecycleManager(std::move(handler));
engine->Run();
}
TEST_F(FlutterWindowsEngineTest, LifecycleStateTransition) {
FlutterWindowsEngineBuilder builder{GetContext()};
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
MockFlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(builder.Build());
FlutterWindowsEngine* engine = view.GetEngine();
EngineModifier modifier(engine);
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
engine->Run();
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
(HWND)1, WM_SIZE, SIZE_RESTORED, 0);
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
AppLifecycleState::kResumed);
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
(HWND)1, WM_SIZE, SIZE_MINIMIZED, 0);
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
AppLifecycleState::kHidden);
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
(HWND)1, WM_SIZE, SIZE_RESTORED, 0);
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
AppLifecycleState::kInactive);
}
TEST_F(FlutterWindowsEngineTest, ExternalWindowMessage) {
FlutterWindowsEngineBuilder builder{GetContext()};
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
MockFlutterWindowsView view(std::move(window_binding_handler));
view.SetEngine(builder.Build());
FlutterWindowsEngine* engine = view.GetEngine();
EngineModifier modifier(engine);
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
// Sets lifecycle state to resumed.
engine->Run();
// Ensure HWND(1) is in the set of visible windows before hiding it.
engine->ProcessExternalWindowMessage(reinterpret_cast<HWND>(1), WM_SHOWWINDOW,
TRUE, NULL);
engine->ProcessExternalWindowMessage(reinterpret_cast<HWND>(1), WM_SHOWWINDOW,
FALSE, NULL);
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
AppLifecycleState::kHidden);
}
TEST_F(FlutterWindowsEngineTest, InnerWindowHidden) {
FlutterWindowsEngineBuilder builder{GetContext()};
HWND outer = reinterpret_cast<HWND>(1);
HWND inner = reinterpret_cast<HWND>(2);
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
MockFlutterWindowsView view(std::move(window_binding_handler));
ON_CALL(view, GetPlatformWindow).WillByDefault([=]() { return inner; });
view.SetEngine(builder.Build());
FlutterWindowsEngine* engine = view.GetEngine();
EngineModifier modifier(engine);
modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
// Sets lifecycle state to resumed.
engine->Run();
// Show both top-level and Flutter window.
engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
outer, WM_SHOWWINDOW, TRUE, NULL);
view.OnWindowStateEvent(inner, WindowStateEvent::kShow);
view.OnWindowStateEvent(inner, WindowStateEvent::kFocus);
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
AppLifecycleState::kResumed);
// Hide Flutter window, but not top level window.
view.OnWindowStateEvent(inner, WindowStateEvent::kHide);
// The top-level window is still visible, so we ought not enter hidden state.
EXPECT_EQ(engine->lifecycle_manager()->GetLifecycleState(),
AppLifecycleState::kInactive);
}
} // namespace testing
} // namespace flutter

View File

@ -673,4 +673,10 @@ void FlutterWindowsView::OnDwmCompositionChanged() {
}
}
void FlutterWindowsView::OnWindowStateEvent(HWND hwnd, WindowStateEvent event) {
if (engine_) {
engine_->OnWindowStateEvent(hwnd, event);
}
}
} // namespace flutter

View File

@ -211,6 +211,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
return accessibility_bridge_;
}
// |WindowBindingHandlerDelegate|
void OnWindowStateEvent(HWND hwnd, WindowStateEvent event) override;
protected:
virtual void NotifyWinEventWrapper(ui::AXPlatformNodeWin* node,
ax::mojom::Event event);

View File

@ -212,6 +212,18 @@ FLUTTER_EXPORT HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view);
FLUTTER_EXPORT IDXGIAdapter* FlutterDesktopViewGetGraphicsAdapter(
FlutterDesktopViewRef view);
// Called to pass an external window message to the engine for lifecycle
// state updates. Non-Flutter windows must call this method in their WndProc
// in order to be included in the logic for application lifecycle state
// updates. Returns a result if the message should be consumed.
FLUTTER_EXPORT bool FlutterDesktopEngineProcessExternalWindowMessage(
FlutterDesktopEngineRef engine,
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result);
// ========== Plugin Registrar (extensions) ==========
// These are Windows-specific extensions to flutter_plugin_registrar.h

View File

@ -66,6 +66,8 @@ class MockWindow : public Window {
MOCK_METHOD3(OnGetObject, LRESULT(UINT, WPARAM, LPARAM));
MOCK_METHOD1(OnWindowStateEvent, void(WindowStateEvent));
void CallOnImeComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam);

View File

@ -61,6 +61,8 @@ class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate {
MOCK_METHOD0(GetAxFragmentRootDelegate, ui::AXFragmentRootDelegateWin*());
MOCK_METHOD2(OnWindowStateEvent, void(HWND, WindowStateEvent));
private:
FML_DISALLOW_COPY_AND_ASSIGN(MockWindowBindingHandlerDelegate);
};

View File

@ -81,9 +81,7 @@ Window::Window(std::unique_ptr<WindowsProcTable> windows_proc_table,
keyboard_manager_ = std::make_unique<KeyboardManager>(this);
}
Window::~Window() {
Destroy();
}
Window::~Window() {}
void Window::InitializeChild(const char* title,
unsigned int width,
@ -352,6 +350,9 @@ Window::HandleMessage(UINT const message,
current_width_ = width;
current_height_ = height;
HandleResize(width, height);
OnWindowStateEvent(width == 0 && height == 0 ? WindowStateEvent::kHide
: WindowStateEvent::kShow);
break;
case WM_PAINT:
OnPaint();
@ -430,9 +431,11 @@ Window::HandleMessage(UINT const message,
break;
}
case WM_SETFOCUS:
OnWindowStateEvent(WindowStateEvent::kFocus);
::CreateCaret(window_handle_, nullptr, 1, 1);
break;
case WM_KILLFOCUS:
OnWindowStateEvent(WindowStateEvent::kUnfocus);
::DestroyCaret();
break;
case WM_LBUTTONDOWN:

View File

@ -18,6 +18,7 @@
#include "flutter/shell/platform/windows/keyboard_manager.h"
#include "flutter/shell/platform/windows/sequential_id_generator.h"
#include "flutter/shell/platform/windows/text_input_manager.h"
#include "flutter/shell/platform/windows/windows_lifecycle_manager.h"
#include "flutter/shell/platform/windows/windows_proc_table.h"
#include "flutter/shell/platform/windows/windowsx_shim.h"
#include "flutter/third_party/accessibility/ax/platform/ax_fragment_root_delegate_win.h"
@ -223,6 +224,9 @@ class Window : public KeyboardManager::WindowDelegate {
// Called to obtain a pointer to the fragment root delegate.
virtual ui::AXFragmentRootDelegateWin* GetAxFragmentRootDelegate() = 0;
// Called on a resize or focus event.
virtual void OnWindowStateEvent(WindowStateEvent event) = 0;
protected:
// Win32's DefWindowProc.
//
@ -236,6 +240,9 @@ class Window : public KeyboardManager::WindowDelegate {
// Returns the root view accessibility node, or nullptr if none.
virtual gfx::NativeViewAccessible GetNativeViewAccessible() = 0;
// Release OS resources associated with the window.
void Destroy();
// Handles running DirectManipulation on the window to receive trackpad
// gestures.
std::unique_ptr<DirectManipulationOwner> direct_manipulation_owner_;
@ -250,9 +257,6 @@ class Window : public KeyboardManager::WindowDelegate {
std::unique_ptr<ui::AXPlatformNodeWin> alert_node_;
private:
// Release OS resources associated with window.
void Destroy();
// Activates tracking for a "mouse leave" event.
void TrackMouseLeaveEvent(HWND hwnd);

View File

@ -9,6 +9,7 @@
#include "flutter/shell/platform/common/geometry.h"
#include "flutter/shell/platform/embedder/embedder.h"
#include "flutter/shell/platform/windows/windows_lifecycle_manager.h"
#include "flutter/third_party/accessibility/ax/platform/ax_fragment_root_delegate_win.h"
#include "flutter/third_party/accessibility/gfx/native_widget_types.h"
@ -144,6 +145,10 @@ class WindowBindingHandlerDelegate {
// MSAA, UIA elements do not explicitly store or enumerate their
// children and parents, so a method such as this is required.
virtual ui::AXFragmentRootDelegateWin* GetAxFragmentRootDelegate() = 0;
// Called when a window receives an event that may alter application lifecycle
// state.
virtual void OnWindowStateEvent(HWND hwnd, WindowStateEvent event) = 0;
};
} // namespace flutter

View File

@ -75,6 +75,26 @@ bool WindowsLifecycleManager::WindowProc(HWND hwnd,
case WM_DWMCOMPOSITIONCHANGED:
engine_->OnDwmCompositionChanged();
break;
case WM_SIZE:
if (wpar == SIZE_MAXIMIZED || wpar == SIZE_RESTORED) {
OnWindowStateEvent(hwnd, WindowStateEvent::kShow);
} else if (wpar == SIZE_MINIMIZED) {
OnWindowStateEvent(hwnd, WindowStateEvent::kHide);
}
break;
case WM_SHOWWINDOW:
if (!wpar) {
OnWindowStateEvent(hwnd, WindowStateEvent::kHide);
} else {
OnWindowStateEvent(hwnd, WindowStateEvent::kShow);
}
break;
case WM_DESTROY:
OnWindowStateEvent(hwnd, WindowStateEvent::kHide);
break;
}
return false;
}
@ -163,4 +183,111 @@ void WindowsLifecycleManager::BeginProcessingClose() {
process_close_ = true;
}
// TODO(schectman): Wait until the platform channel is registered to send
// the platform message.
// https://github.com/flutter/flutter/issues/131616
void WindowsLifecycleManager::SetLifecycleState(AppLifecycleState state) {
if (state_ == state) {
return;
}
state_ = state;
if (engine_) {
const char* state_name = AppLifecycleStateToString(state);
engine_->SendPlatformMessage("flutter/lifecycle",
reinterpret_cast<const uint8_t*>(state_name),
strlen(state_name), nullptr, nullptr);
}
}
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);
}
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);
}
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);
}
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);
}
break;
}
case WindowStateEvent::kUnfocus: {
if (focused_windows_.erase(hwnd) && focused_windows_.empty() &&
state_ == AppLifecycleState::kResumed) {
SetLifecycleState(AppLifecycleState::kInactive);
}
break;
}
}
}
std::optional<LRESULT> WindowsLifecycleManager::ExternalWindowMessage(
HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam) {
std::optional<flutter::WindowStateEvent> event = std::nullopt;
// TODO (schectman): Handle WM_CLOSE messages.
// https://github.com/flutter/flutter/issues/131497
switch (message) {
case WM_SHOWWINDOW:
event = wparam ? flutter::WindowStateEvent::kShow
: flutter::WindowStateEvent::kHide;
break;
case WM_SIZE:
switch (wparam) {
case SIZE_MINIMIZED:
event = flutter::WindowStateEvent::kHide;
break;
case SIZE_RESTORED:
case SIZE_MAXIMIZED:
event = flutter::WindowStateEvent::kShow;
break;
}
break;
case WM_SETFOCUS:
event = flutter::WindowStateEvent::kFocus;
break;
case WM_KILLFOCUS:
event = flutter::WindowStateEvent::kUnfocus;
break;
case WM_DESTROY:
event = flutter::WindowStateEvent::kHide;
break;
}
if (event.has_value()) {
OnWindowStateEvent(hwnd, *event);
}
return std::nullopt;
}
} // namespace flutter

View File

@ -9,17 +9,31 @@
#include <cstdint>
#include <map>
#include <mutex>
#include <optional>
#include <set>
#include "flutter/shell/platform/common/app_lifecycle_state.h"
namespace flutter {
class FlutterWindowsEngine;
/// A manager for lifecycle events of the top-level window.
/// An event representing a change in window state that may update the
// application lifecycle state.
enum class WindowStateEvent {
kShow,
kHide,
kFocus,
kUnfocus,
};
/// A manager for lifecycle events of the top-level windows.
///
/// Currently handles the following events:
/// 1. WM_CLOSE
/// 2. WM_DWMCOMPOSITIONCHANGED
/// WndProc is called for window messages of the top-level Flutter window.
/// ExternalWindowMessage is called for non-flutter top-level window messages.
/// OnWindowStateEvent is called when the visibility or focus state of a window
/// is changed, including the FlutterView window.
class WindowsLifecycleManager {
public:
WindowsLifecycleManager(FlutterWindowsEngine* engine);
@ -34,12 +48,43 @@ class WindowsLifecycleManager {
std::optional<LPARAM> lparam,
UINT exit_code);
// Intercept top level window messages, only paying attention to WM_CLOSE.
// Intercept top level window WM_CLOSE message and listen to events that may
// update the application lifecycle.
bool WindowProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l, LRESULT* result);
// Signal to start consuming WM_CLOSE messages.
void BeginProcessingClose();
// Update the app lifecycle state in response to a change in window state.
// When the app lifecycle state actually changes, this sends a platform
// 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.
virtual void OnWindowStateEvent(HWND hwnd, WindowStateEvent event);
AppLifecycleState GetLifecycleState() { return state_; }
// 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:
// - FlutterWindow does not receive WM_SHOW messages,
// - When FlutterWindow receives WM_SIZE messages, wparam stores no meaningful
// information, whereas it usually indicates the action which changed the
// window size.
// When this returns a result, the message has been consumed and should not be
// processed further. Currently, it will always return nullopt.
std::optional<LRESULT> ExternalWindowMessage(HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam);
protected:
// Check the number of top-level windows associated with this process, and
// return true only if there are 1 or fewer.
@ -56,6 +101,14 @@ class WindowsLifecycleManager {
std::map<std::tuple<HWND, WPARAM, LPARAM>, int> sent_close_messages_;
bool process_close_;
std::set<HWND> visible_windows_;
std::set<HWND> focused_windows_;
std::mutex state_update_lock_;
flutter::AppLifecycleState state_;
};
} // namespace flutter

View File

@ -0,0 +1,61 @@
// 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/shell/platform/windows/windows_lifecycle_manager.h"
#include "flutter/shell/platform/windows/testing/windows_test.h"
#include "gtest/gtest.h"
namespace flutter {
namespace testing {
class WindowsLifecycleManagerTest : public WindowsTest {};
TEST_F(WindowsLifecycleManagerTest, StateTransitions) {
WindowsLifecycleManager manager(nullptr);
HWND win1 = reinterpret_cast<HWND>(1);
HWND win2 = reinterpret_cast<HWND>(2);
// Hidden to inactive upon window shown.
manager.SetLifecycleState(AppLifecycleState::kHidden);
manager.OnWindowStateEvent(win1, WindowStateEvent::kShow);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive);
// Showing a second window does not change state.
manager.OnWindowStateEvent(win2, WindowStateEvent::kShow);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive);
// Inactive to resumed upon window focus.
manager.OnWindowStateEvent(win2, WindowStateEvent::kFocus);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kResumed);
// Showing a second window does not change state.
manager.OnWindowStateEvent(win1, WindowStateEvent::kFocus);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kResumed);
// Unfocusing one window does not change state while another is focused.
manager.OnWindowStateEvent(win1, WindowStateEvent::kUnfocus);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kResumed);
// Unfocusing final remaining focused window transitions to inactive.
manager.OnWindowStateEvent(win2, WindowStateEvent::kUnfocus);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive);
// Hiding one of two visible windows does not change state.
manager.OnWindowStateEvent(win2, WindowStateEvent::kHide);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kInactive);
// Hiding only visible window transitions to hidden.
manager.OnWindowStateEvent(win1, WindowStateEvent::kHide);
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);
EXPECT_EQ(manager.GetLifecycleState(), AppLifecycleState::kHidden);
}
} // namespace testing
} // namespace flutter