Check child windows in CroppingWindowCapturerWin

On Windows a window may be covered by its own child window. So this change also
detects child windows by using EnumChildWindow().
The tooltip or context menu of the child window still cannot be detected after
this change. See bug for details.

Bug: webrtc:8062
Change-Id: I8455a9206d6a1d9da61013ac9debba4d3edae7d8
Reviewed-on: https://chromium-review.googlesource.com/619728
Commit-Queue: Zijie He <zijiehe@chromium.org>
Reviewed-by: Jamie Walch <jamiewalch@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#19457}
Cr-Mirrored-From: https://chromium.googlesource.com/external/webrtc
Cr-Mirrored-Commit: f9d7eca9b0e759d8289cd37598e817528cc2602b
diff --git a/modules/desktop_capture/cropping_window_capturer_win.cc b/modules/desktop_capture/cropping_window_capturer_win.cc
index 2585014..27be392 100644
--- a/modules/desktop_capture/cropping_window_capturer_win.cc
+++ b/modules/desktop_capture/cropping_window_capturer_win.cc
@@ -60,15 +60,23 @@
     return TRUE;
   }
 
-  // Ignore descendant/owned windows since we want to capture them.
+  // Ignore descendant windows since we want to capture them.
   // This check does not work for tooltips and context menus. Drop down menus
   // and popup windows are fine.
-  if (GetAncestor(hwnd, GA_ROOTOWNER) == context->selected_window) {
+  //
+  // GA_ROOT returns the root window instead of the owner. I.e. for a dialog
+  // window, GA_ROOT returns the dialog window itself. GA_ROOTOWNER returns the
+  // application main window which opens the dialog window. Since we are sharing
+  // the application main window, GA_ROOT should be used here.
+  if (GetAncestor(hwnd, GA_ROOT) == context->selected_window) {
     return TRUE;
   }
 
   // If |hwnd| has no title and belongs to the same process, assume it's a
   // tooltip or context menu from the selected window and ignore it.
+  // TODO(zijiehe): This check cannot cover the case where tooltip or context
+  // menu of the child-window is covering the main window. See
+  // https://bugs.chromium.org/p/webrtc/issues/detail?id=8062 for details.
   const size_t kTitleLength = 32;
   WCHAR window_title[kTitleLength];
   GetWindowText(hwnd, window_title, kTitleLength);
@@ -83,24 +91,20 @@
     }
   }
 
-  // Check if the enumerated window intersects with the selected window.
-  RECT enumerated_rect;
-  if (!GetWindowRect(hwnd, &enumerated_rect)) {
+  DesktopRect window_rect;
+  // TODO(zijiehe): Window content rectangle should be preferred to avoid
+  // falling back to window capturer when the border or shadow of another window
+  // covering the target window.
+  if (!GetWindowRect(hwnd, &window_rect)) {
     // Bail out if failed to get the window area.
     context->is_top_window = false;
     return FALSE;
   }
 
-  DesktopRect intersect_rect = context->selected_window_rect;
-  DesktopRect enumerated_desktop_rect =
-      DesktopRect::MakeLTRB(enumerated_rect.left,
-                            enumerated_rect.top,
-                            enumerated_rect.right,
-                            enumerated_rect.bottom);
-  intersect_rect.IntersectWith(enumerated_desktop_rect);
+  window_rect.IntersectWith(context->selected_window_rect);
 
   // If intersection is not empty, the selected window is not on top.
-  if (!intersect_rect.is_empty()) {
+  if (!window_rect.is_empty()) {
     context->is_top_window = false;
     return FALSE;
   }
@@ -126,8 +130,9 @@
 };
 
 bool CroppingWindowCapturerWin::ShouldUseScreenCapturer() {
-  if (!rtc::IsWindows8OrLater() && aero_checker_.IsAeroEnabled())
+  if (!rtc::IsWindows8OrLater() && aero_checker_.IsAeroEnabled()) {
     return false;
+  }
 
   HWND selected = reinterpret_cast<HWND>(selected_window());
   // Check if the window is hidden or minimized.
@@ -151,17 +156,14 @@
     // UpdateLayeredWindow is the only way to set per-pixel alpha and will cause
     // the previous GetLayeredWindowAttributes to fail. So we only need to check
     // the window wide color key or alpha.
-    if ((flags & LWA_COLORKEY) || ((flags & LWA_ALPHA) && (alpha < 255)))
+    if ((flags & LWA_COLORKEY) || ((flags & LWA_ALPHA) && (alpha < 255))) {
       return false;
+    }
   }
 
-  RECT selected_window_rect;
-  if (!GetWindowRect(selected, &selected_window_rect)) {
+  if (!GetWindowRect(selected, &window_region_rect_)) {
     return false;
   }
-  window_region_rect_ = DesktopRect::MakeLTRB(
-      selected_window_rect.left, selected_window_rect.top,
-      selected_window_rect.right, selected_window_rect.bottom);
 
   // Get the window region and check if it is rectangular.
   win::ScopedGDIObject<HRGN, win::DeleteObjectTraits<HRGN> >
@@ -169,8 +171,9 @@
   int region_type = GetWindowRgn(selected, scoped_hrgn.Get());
 
   // Do not use the screen capturer if the region is empty or not rectangular.
-  if (region_type == COMPLEXREGION || region_type == NULLREGION)
+  if (region_type == COMPLEXREGION || region_type == NULLREGION) {
     return false;
+  }
 
   if (region_type == SIMPLEREGION) {
     RECT region_rect;
@@ -180,26 +183,39 @@
                               region_rect.top,
                               region_rect.right,
                               region_rect.bottom);
-    rgn_rect.Translate(window_region_rect_.left(), window_region_rect_.top());
-    window_region_rect_.IntersectWith(rgn_rect);
+    DesktopRect translated_rect = rgn_rect;
+    translated_rect.Translate(window_region_rect_.left(),
+                              window_region_rect_.top());
+    window_region_rect_.IntersectWith(translated_rect);
   }
 
+  // TODO(zijiehe): Check whether the client area is out of the screen area.
+
   // Check if the window is occluded by any other window, excluding the child
   // windows, context menus, and |excluded_window_|.
-  // TODO(zijiehe): EnumWindows enumerates root window only, so the window may
-  // be covered by its own child window. See bug
-  // https://bugs.chromium.org/p/webrtc/issues/detail?id=8062
+  // TODO(zijiehe): Content rectangle should be preferred to avoid falling back
+  // to window capturer when border or shadow of another window covering the
+  // target window.
   TopWindowVerifierContext context(
       selected, reinterpret_cast<HWND>(excluded_window()), window_region_rect_);
-  EnumWindows(&TopWindowVerifier, reinterpret_cast<LPARAM>(&context));
+  const LPARAM enum_param = reinterpret_cast<LPARAM>(&context);
+  EnumWindows(&TopWindowVerifier, enum_param);
+  if (!context.is_top_window) {
+    return false;
+  }
+
+  // If |selected| is not covered by other windows, check whether it is
+  // covered by its own child windows. Note: EnumChildWindows() enumerates child
+  // windows in all generations, but does not include any controls like buttons
+  // or textboxes.
+  EnumChildWindows(selected, &TopWindowVerifier, enum_param);
   return context.is_top_window;
 }
 
 DesktopRect CroppingWindowCapturerWin::GetWindowRectInVirtualScreen() {
-  DesktopRect original_rect;
   DesktopRect window_rect;
   HWND hwnd = reinterpret_cast<HWND>(selected_window());
-  if (!GetCroppedWindowRect(hwnd, &window_rect, &original_rect)) {
+  if (!GetCroppedWindowRect(hwnd, &window_rect, /* original_rect */ nullptr)) {
     LOG(LS_WARNING) << "Failed to get window info: " << GetLastError();
     return window_rect;
   }
diff --git a/modules/desktop_capture/win/window_capture_utils.cc b/modules/desktop_capture/win/window_capture_utils.cc
index 41c4822..abcb6a6 100644
--- a/modules/desktop_capture/win/window_capture_utils.cc
+++ b/modules/desktop_capture/win/window_capture_utils.cc
@@ -10,42 +10,91 @@
 
 #include "webrtc/modules/desktop_capture/win/window_capture_utils.h"
 
+#include "webrtc/rtc_base/checks.h"
 #include "webrtc/rtc_base/win32.h"
 
 namespace webrtc {
 
-bool
-GetCroppedWindowRect(HWND window,
-                     DesktopRect* cropped_rect,
-                     DesktopRect* original_rect) {
+bool GetWindowRect(HWND window, DesktopRect* result) {
   RECT rect;
-  if (!GetWindowRect(window, &rect)) {
+  if (!::GetWindowRect(window, &rect)) {
     return false;
   }
-  WINDOWPLACEMENT window_placement;
-  window_placement.length = sizeof(window_placement);
-  if (!GetWindowPlacement(window, &window_placement)) {
+  *result = DesktopRect::MakeLTRB(
+      rect.left, rect.top, rect.right, rect.bottom);
+  return true;
+}
+
+bool GetCroppedWindowRect(HWND window,
+                          DesktopRect* cropped_rect,
+                          DesktopRect* original_rect) {
+  DesktopRect window_rect;
+  if (!GetWindowRect(window, &window_rect)) {
     return false;
   }
 
-  *original_rect = DesktopRect::MakeLTRB(
-      rect.left, rect.top, rect.right, rect.bottom);
+  if (original_rect) {
+    *original_rect = window_rect;
+  }
+  *cropped_rect = window_rect;
+
+  WINDOWPLACEMENT window_placement;
+  window_placement.length = sizeof(window_placement);
+  if (!::GetWindowPlacement(window, &window_placement)) {
+    return false;
+  }
 
   // After Windows8, transparent borders will be added by OS at
   // left/bottom/right sides of a window. If the cropped window
   // doesn't remove these borders, the background will be exposed a bit.
+  //
+  // On Windows 8.1. or upper, rtc::IsWindows8OrLater(), which uses
+  // GetVersionEx() may not correctly return the windows version. See
+  // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx
+  // So we always prefer to check |window_placement|.showCmd.
   if (rtc::IsWindows8OrLater() ||
       window_placement.showCmd == SW_SHOWMAXIMIZED) {
-    DesktopSize border = DesktopSize(GetSystemMetrics(SM_CXSIZEFRAME),
-                                     GetSystemMetrics(SM_CYSIZEFRAME));
-    *cropped_rect = DesktopRect::MakeLTRB(
-        rect.left + border.width(),
-        rect.top,
-        rect.right - border.width(),
-        rect.bottom - border.height());
-  } else {
-    *cropped_rect = *original_rect;
+    const int width = GetSystemMetrics(SM_CXSIZEFRAME);
+    const int height = GetSystemMetrics(SM_CYSIZEFRAME);
+    cropped_rect->Extend(-width, 0, -width, -height);
   }
+
+  return true;
+}
+
+bool GetWindowContentRect(HWND window, DesktopRect* result) {
+  if (!GetWindowRect(window, result)) {
+    return false;
+  }
+
+  RECT rect;
+  if (!::GetClientRect(window, &rect)) {
+    return false;
+  }
+
+  const int width = rect.right - rect.left;
+  // The GetClientRect() is not expected to return a larger area than
+  // GetWindowRect().
+  if (width > 0 && width < result->width()) {
+    // - GetClientRect() always set the left / top of RECT to 0. So we need to
+    //   estimate the border width from GetClientRect() and GetWindowRect().
+    // - Border width of a window varies according to the window type.
+    // - GetClientRect() excludes the title bar, which should be considered as
+    //   part of the content and included in the captured frame. So we always
+    //   estimate the border width according to the window width.
+    // - We assume a window has same border width in each side.
+    // So we shrink half of the width difference from all four sides.
+    const int shrink = ((width - result->width()) / 2);
+    // When |shrink| is negative, DesktopRect::Extend() shrinks itself.
+    result->Extend(shrink, 0, shrink, 0);
+    // Usually this should not happen, just in case we have received a strange
+    // window, which has only left and right borders.
+    if (result->height() > shrink * 2) {
+      result->Extend(0, shrink, 0, shrink);
+    }
+    RTC_DCHECK(!result->is_empty());
+  }
+
   return true;
 }
 
diff --git a/modules/desktop_capture/win/window_capture_utils.h b/modules/desktop_capture/win/window_capture_utils.h
index 8f59c12..255963e 100644
--- a/modules/desktop_capture/win/window_capture_utils.h
+++ b/modules/desktop_capture/win/window_capture_utils.h
@@ -15,16 +15,28 @@
 
 namespace webrtc {
 
-// Output the window rect, with the left/right/bottom frame border cropped if
+// Outputs the window rect. The returned DesktopRect is in system coordinates,
+// i.e. the primary monitor on the system always starts from (0, 0). This
+// function returns false if native APIs fail.
+bool GetWindowRect(HWND window, DesktopRect* result);
+
+// Outputs the window rect, with the left/right/bottom frame border cropped if
 // the window is maximized. |cropped_rect| is the cropped rect relative to the
 // desktop. |original_rect| is the original rect returned from GetWindowRect.
 // Returns true if all API calls succeeded. The returned DesktopRect is in
 // system coordinates, i.e. the primary monitor on the system always starts from
-// (0, 0).
+// (0, 0). |original_rect| can be nullptr.
 bool GetCroppedWindowRect(HWND window,
                           DesktopRect* cropped_rect,
                           DesktopRect* original_rect);
 
+// Retrieves the rectangle of the content area of |window|. Usually it contains
+// title bar and window client area, but borders or shadow are excluded. The
+// returned DesktopRect is in system coordinates, i.e. the primary monitor on
+// the system always starts from (0, 0). This function returns false if native
+// APIs fail.
+bool GetWindowContentRect(HWND window, DesktopRect* result);
+
 typedef HRESULT (WINAPI *DwmIsCompositionEnabledFunc)(BOOL* enabled);
 class AeroChecker {
  public: