mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
fb1851fe34
commit
c2cc8508b6
@ -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
|
||||
|
||||
@ -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 +=
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
//
|
||||
|
||||
@ -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
|
||||
|
||||
@ -673,4 +673,10 @@ void FlutterWindowsView::OnDwmCompositionChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
void FlutterWindowsView::OnWindowStateEvent(HWND hwnd, WindowStateEvent event) {
|
||||
if (engine_) {
|
||||
engine_->OnWindowStateEvent(hwnd, event);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace flutter
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user