1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "gtest/gtest.h"
8 
9 #include "MockWinWidget.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/widget/WinEventObserver.h"
12 #include "mozilla/widget/WinWindowOcclusionTracker.h"
13 #include "nsThreadUtils.h"
14 #include "Units.h"
15 #include "WinUtils.h"
16 
17 using namespace mozilla;
18 using namespace mozilla::widget;
19 
20 #define PREF_DISPLAY_STATE \
21   "widget.windows.window_occlusion_tracking_display_state.enabled"
22 #define PREF_SESSION_LOCK \
23   "widget.windows.window_occlusion_tracking_session_lock.enabled"
24 
25 class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test {
26  protected:
SetUp()27   void SetUp() override {
28     // Shut down WinWindowOcclusionTracker if it exists.
29     // This could happen when WinWindowOcclusionTracker was initialized by other
30     // gtest
31     if (WinWindowOcclusionTracker::Get()) {
32       WinWindowOcclusionTracker::ShutDown();
33     }
34     EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
35 
36     WinWindowOcclusionTracker::Ensure();
37     EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get());
38 
39     WinWindowOcclusionTracker::Get()->EnsureDisplayStatusObserver();
40     WinWindowOcclusionTracker::Get()->EnsureSessionChangeObserver();
41   }
42 
TearDown()43   void TearDown() override {
44     WinWindowOcclusionTracker::ShutDown();
45     EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
46   }
47 
SetNativeWindowBounds(HWND aHWnd,const LayoutDeviceIntRect aBounds)48   void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) {
49     RECT wr;
50     wr.left = aBounds.X();
51     wr.top = aBounds.Y();
52     wr.right = aBounds.XMost();
53     wr.bottom = aBounds.YMost();
54 
55     ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE,
56                          ::GetWindowLong(aHWnd, GWL_EXSTYLE));
57 
58     // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have
59     // moved part of it offscreen. But, if the original requested bounds are
60     // offscreen, don't adjust the position.
61     LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left,
62                                      wr.bottom - wr.top);
63 
64     if (aBounds.X() >= 0) {
65       windowBounds.x = std::max(0, windowBounds.X());
66     }
67     if (aBounds.Y() >= 0) {
68       windowBounds.y = std::max(0, windowBounds.Y());
69     }
70     ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(),
71                    windowBounds.Width(), windowBounds.Height(),
72                    SWP_NOREPOSITION);
73     EXPECT_TRUE(::UpdateWindow(aHWnd));
74   }
75 
CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds)76   void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) {
77     mMockWinWidget = MockWinWidget::Create(
78         WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds);
79     EXPECT_NE(nullptr, mMockWinWidget.get());
80     HWND hwnd = mMockWinWidget->GetWnd();
81     SetNativeWindowBounds(hwnd, aBounds);
82     HRGN region = ::CreateRectRgn(0, 0, 0, 0);
83     EXPECT_NE(nullptr, region);
84     if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) {
85       // On Windows 7, the newly created window has a complex region, which
86       // means it will be ignored during the occlusion calculation. So, force
87       // it to have a simple region so that we get test coverage on win 7.
88       RECT boundingRect;
89       EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect));
90       HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect);
91       EXPECT_NE(nullptr, rectangularRegion);
92       ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE);
93       ::DeleteObject(rectangularRegion);
94     }
95     ::DeleteObject(region);
96 
97     ::ShowWindow(hwnd, SW_SHOWNORMAL);
98     EXPECT_TRUE(UpdateWindow(hwnd));
99   }
100 
CreateTrackedWindowWithBounds(LayoutDeviceIntRect aBounds)101   RefPtr<MockWinWidget> CreateTrackedWindowWithBounds(
102       LayoutDeviceIntRect aBounds) {
103     RefPtr<MockWinWidget> window = MockWinWidget::Create(
104         WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds);
105     EXPECT_NE(nullptr, window.get());
106     HWND hwnd = window->GetWnd();
107     ::ShowWindow(hwnd, SW_SHOWNORMAL);
108     WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd());
109     return window;
110   }
111 
GetNumVisibleRootWindows()112   int GetNumVisibleRootWindows() {
113     return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows;
114   }
115 
OnDisplayStateChanged(bool aDisplayOn)116   void OnDisplayStateChanged(bool aDisplayOn) {
117     WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn);
118   }
119 
120   RefPtr<MockWinWidget> mMockWinWidget;
121 };
122 
123 // Simple test completely covering a tracked window with a native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,SimpleOcclusion)124 TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) {
125   RefPtr<MockWinWidget> window =
126       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
127   window->SetExpectation(widget::OcclusionState::OCCLUDED);
128   CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
129   while (window->IsExpectingCall()) {
130     WinWindowOcclusionTracker::Get()->TriggerCalculation();
131     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
132   }
133   EXPECT_FALSE(window->IsExpectingCall());
134 }
135 
136 // Simple test partially covering a tracked window with a native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,PartialOcclusion)137 TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) {
138   RefPtr<MockWinWidget> window =
139       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
140   window->SetExpectation(widget::OcclusionState::VISIBLE);
141   CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
142   while (window->IsExpectingCall()) {
143     WinWindowOcclusionTracker::Get()->TriggerCalculation();
144     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
145   }
146   EXPECT_FALSE(window->IsExpectingCall());
147 }
148 
149 // Simple test that a partly off screen tracked window, with the on screen part
150 // occluded by a native window, is considered occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,OffscreenOcclusion)151 TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) {
152   RefPtr<MockWinWidget> window =
153       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
154   // Move the tracked window 50 pixels offscreen to the left.
155   int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
156   ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100,
157                  SWP_NOZORDER | SWP_NOSIZE);
158 
159   // Create a native window that covers the onscreen part of the tracked window.
160   CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100));
161   window->SetExpectation(widget::OcclusionState::OCCLUDED);
162   while (window->IsExpectingCall()) {
163     WinWindowOcclusionTracker::Get()->TriggerCalculation();
164     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
165   }
166   EXPECT_FALSE(window->IsExpectingCall());
167 }
168 
169 // Simple test with a tracked window and native window that do not overlap.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,SimpleVisible)170 TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) {
171   RefPtr<MockWinWidget> window =
172       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
173   window->SetExpectation(widget::OcclusionState::VISIBLE);
174   CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
175   while (window->IsExpectingCall()) {
176     WinWindowOcclusionTracker::Get()->TriggerCalculation();
177     WinWindowOcclusionTracker::Get()->TriggerCalculation();
178     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
179   }
180   EXPECT_FALSE(window->IsExpectingCall());
181 }
182 
183 // Simple test with a minimized tracked window and native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,SimpleHidden)184 TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) {
185   RefPtr<MockWinWidget> window =
186       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
187   CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
188   // Iconify the tracked window and check that its occlusion state is HIDDEN.
189   ::CloseWindow(window->GetWnd());
190   window->SetExpectation(widget::OcclusionState::HIDDEN);
191   while (window->IsExpectingCall()) {
192     WinWindowOcclusionTracker::Get()->TriggerCalculation();
193     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
194   }
195   EXPECT_FALSE(window->IsExpectingCall());
196 }
197 
198 // Test that minimizing and restoring a tracked window results in the occlusion
199 // tracker re-registering for win events and detecting that a native window
200 // occludes the tracked window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,OcclusionAfterVisibilityToggle)201 TEST_F(WinWindowOcclusionTrackerInteractiveTest,
202        OcclusionAfterVisibilityToggle) {
203   RefPtr<MockWinWidget> window =
204       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
205   window->SetExpectation(widget::OcclusionState::VISIBLE);
206   while (window->IsExpectingCall()) {
207     WinWindowOcclusionTracker::Get()->TriggerCalculation();
208     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
209   }
210 
211   window->SetExpectation(widget::OcclusionState::HIDDEN);
212   WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
213       window, /* aVisible = */ false);
214 
215   // This makes the window iconic.
216   ::CloseWindow(window->GetWnd());
217 
218   while (window->IsExpectingCall()) {
219     WinWindowOcclusionTracker::Get()->TriggerCalculation();
220     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
221   }
222 
223   // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification,
224   // before occlusion is calculated, so the above expectation will be met w/o an
225   // occlusion calculation.
226   // Loop until an occlusion calculation has run with no non-hidden windows.
227   while (GetNumVisibleRootWindows() != 0) {
228     // Need to pump events in order for UpdateOcclusionState to get called, and
229     // update the number of non hidden windows. When that number is 0,
230     // occlusion has been calculated with no visible tracked windows.
231     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
232   }
233 
234   window->SetExpectation(widget::OcclusionState::VISIBLE);
235   WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
236       window, /* aVisible = */ true);
237   // This opens the window made iconic above.
238   ::OpenIcon(window->GetWnd());
239 
240   while (window->IsExpectingCall()) {
241     WinWindowOcclusionTracker::Get()->TriggerCalculation();
242     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
243   }
244 
245   // Open a native window that occludes the visible tracked window.
246   window->SetExpectation(widget::OcclusionState::OCCLUDED);
247   CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
248   while (window->IsExpectingCall()) {
249     WinWindowOcclusionTracker::Get()->TriggerCalculation();
250     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
251   }
252   EXPECT_FALSE(window->IsExpectingCall());
253 }
254 
255 // Test that locking the screen causes visible windows to become occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,LockScreenVisibleOcclusion)256 TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) {
257   RefPtr<MockWinWidget> window =
258       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
259   window->SetExpectation(widget::OcclusionState::VISIBLE);
260   while (window->IsExpectingCall()) {
261     WinWindowOcclusionTracker::Get()->TriggerCalculation();
262     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
263   }
264 
265   window->SetExpectation(widget::OcclusionState::OCCLUDED);
266   // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
267   // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but
268   // actually locking the screen isn't feasible.
269   DWORD currentSessionId = 0;
270   ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
271   ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
272                 WTS_SESSION_LOCK, currentSessionId);
273 
274   while (window->IsExpectingCall()) {
275     WinWindowOcclusionTracker::Get()->TriggerCalculation();
276 
277     MSG msg;
278     bool gotMessage =
279         ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
280     if (gotMessage) {
281       ::TranslateMessage(&msg);
282       ::DispatchMessageW(&msg);
283     }
284     NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
285     PR_Sleep(PR_INTERVAL_NO_WAIT);
286   }
287   EXPECT_FALSE(window->IsExpectingCall());
288 }
289 
290 // Test that locking the screen leaves hidden windows as hidden.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,LockScreenHiddenOcclusion)291 TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) {
292   RefPtr<MockWinWidget> window =
293       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
294   // Iconify the tracked window and check that its occlusion state is HIDDEN.
295   ::CloseWindow(window->GetWnd());
296   window->SetExpectation(widget::OcclusionState::HIDDEN);
297   while (window->IsExpectingCall()) {
298     WinWindowOcclusionTracker::Get()->TriggerCalculation();
299     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
300   }
301 
302   // Force the state to VISIBLE.
303   window->NotifyOcclusionState(widget::OcclusionState::VISIBLE);
304 
305   window->SetExpectation(widget::OcclusionState::HIDDEN);
306 
307   // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
308   // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but
309   // actually locking the screen isn't feasible.
310   DWORD currentSessionId = 0;
311   ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
312   PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
313               WTS_SESSION_LOCK, currentSessionId);
314 
315   while (window->IsExpectingCall()) {
316     WinWindowOcclusionTracker::Get()->TriggerCalculation();
317 
318     MSG msg;
319     bool gotMessage =
320         ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
321     if (gotMessage) {
322       ::TranslateMessage(&msg);
323       ::DispatchMessageW(&msg);
324     }
325     NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
326     PR_Sleep(PR_INTERVAL_NO_WAIT);
327   }
328   EXPECT_FALSE(window->IsExpectingCall());
329 }
330 
331 // Test that locking the screen from a different session doesn't mark window
332 // as occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,LockScreenDifferentSession)333 TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) {
334   RefPtr<MockWinWidget> window =
335       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
336   window->SetExpectation(widget::OcclusionState::VISIBLE);
337   while (window->IsExpectingCall()) {
338     WinWindowOcclusionTracker::Get()->TriggerCalculation();
339 
340     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
341   }
342 
343   // Force the state to OCCLUDED.
344   window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED);
345 
346   // Generate a session change lock screen with a session id that's not
347   // currentSessionId.
348   DWORD currentSessionId = 0;
349   ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
350   ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
351                 WTS_SESSION_LOCK, currentSessionId + 1);
352 
353   window->SetExpectation(widget::OcclusionState::VISIBLE);
354   // Create a native window to trigger occlusion calculation.
355   CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
356   while (window->IsExpectingCall()) {
357     WinWindowOcclusionTracker::Get()->TriggerCalculation();
358 
359     MSG msg;
360     bool gotMessage =
361         ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
362     if (gotMessage) {
363       ::TranslateMessage(&msg);
364       ::DispatchMessageW(&msg);
365     }
366     NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
367     PR_Sleep(PR_INTERVAL_NO_WAIT);
368   }
369   EXPECT_FALSE(window->IsExpectingCall());
370 }
371 
372 // Test that display off & on power state notification causes visible windows to
373 // become occluded, then visible.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,DisplayOnOffHandling)374 TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) {
375   RefPtr<MockWinWidget> window =
376       CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
377   window->SetExpectation(widget::OcclusionState::VISIBLE);
378   while (window->IsExpectingCall()) {
379     WinWindowOcclusionTracker::Get()->TriggerCalculation();
380 
381     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
382   }
383 
384   window->SetExpectation(widget::OcclusionState::OCCLUDED);
385 
386   // Turning display off and on isn't feasible, so send a notification.
387   OnDisplayStateChanged(/* aDisplayOn */ false);
388   while (window->IsExpectingCall()) {
389     WinWindowOcclusionTracker::Get()->TriggerCalculation();
390 
391     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
392   }
393 
394   window->SetExpectation(widget::OcclusionState::VISIBLE);
395   OnDisplayStateChanged(/* aDisplayOn */ true);
396   while (window->IsExpectingCall()) {
397     WinWindowOcclusionTracker::Get()->TriggerCalculation();
398 
399     NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
400   }
401   EXPECT_FALSE(window->IsExpectingCall());
402 }
403