chromium/remoting/host/disconnect_window_win.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <windows.h>

#include <stddef.h>

#include <cstdlib>
#include <memory>

#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/win/current_module.h"
#include "base/win/scoped_gdi_object.h"
#include "base/win/scoped_hdc.h"
#include "base/win/scoped_select_object.h"
#include "remoting/host/client_session_control.h"
#include "remoting/host/host_window.h"
#include "remoting/host/input_monitor/local_input_monitor.h"
#include "remoting/host/win/core_resource.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
#include "ui/events/event.h"

namespace remoting {

namespace {

constexpr int DISCONNECT_HOTKEY_ID = 1000;

// Maximum length of "Your desktop is shared with ..." message in UTF-16
// characters.
constexpr size_t kMaxSharingWithTextLength = 100;

constexpr wchar_t kShellTrayWindowName[] = L"Shell_TrayWnd";
constexpr int kWindowBorderRadius = 14;

// Margin between dialog controls (in dialog units).
constexpr int kWindowTextMargin = 8;

// The amount of time to wait before hiding the disconnect window.
constexpr base::TimeDelta kAutoHideTimeout = base::Seconds(10);

// The length of the hide and show animations.
constexpr DWORD kAnimationDurationMs = 200;

class DisconnectWindowWin : public HostWindow {
 public:
  DisconnectWindowWin();

  DisconnectWindowWin(const DisconnectWindowWin&) = delete;
  DisconnectWindowWin& operator=(const DisconnectWindowWin&) = delete;

  ~DisconnectWindowWin() override;

  // Allow dialog to auto-hide after a period of time.  The dialog will be
  // reshown when local user input is detected.
  void EnableAutoHide(std::unique_ptr<LocalInputMonitor> local_input_monitor);

  // HostWindow overrides.
  void Start(const base::WeakPtr<ClientSessionControl>& client_session_control)
      override;

 private:
  static INT_PTR CALLBACK DialogProc(HWND hwnd,
                                     UINT message,
                                     WPARAM wparam,
                                     LPARAM lparam);

  BOOL OnDialogMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

  // Creates the dialog window and registers the disconnect hot key.
  bool BeginDialog();

  // Closes the dialog, unregisters the hot key and invokes the disconnect
  // callback, if set.
  void EndDialog();

  // Returns |control| rectangle in the dialog coordinates.
  bool GetControlRect(HWND control, RECT* rect);

  // Positions the dialog window based on the current auto-hide state.
  // If auto-hide is enabled, the window is displayed near the center of the
  // display, otherwise it is displayed just above the taskbar.
  void SetDialogPosition();

  // Applies localization string and resizes the dialog.
  bool SetStrings();

  // Draws the border around the dialog window.  Can be used to draw the initial
  // border or to redraw if when the dialog is reshown.  |hwnd| is the window to
  // have the border applied.  |hdc| is the device context to draw to.
  void DrawBorder(HWND hwnd, HDC hdc);

  // Shows a previously hidden dialog using an animation.
  void ShowDialog();

  // Hides the dialog using an animation.
  void HideDialog();

  // Prevent the dialog from being hidden if local input monitoring fails.
  void StopAutoHideBehavior();

  // Called when local mouse event is seen and shows the dialog (if hidden).
  void OnLocalMouseEvent(const webrtc::DesktopVector& mouse_position,
                         ui::EventType type);

  // Called when local keyboard event is seen and shows the dialog (if hidden).
  void OnLocalKeyPressed(uint32_t usb_keycode);

  // Used to disconnect the client session.
  base::WeakPtr<ClientSessionControl> client_session_control_;

  // Used to watch for local input which will trigger the dialog to be reshown.
  std::unique_ptr<LocalInputMonitor> local_input_monitor_;

  // Specifies the remote user name.
  std::string username_;

  bool was_auto_hidden_ = false;
  bool local_input_seen_ = false;
  base::OneShotTimer auto_hide_timer_;

  HWND hwnd_ = nullptr;
  bool has_hotkey_ = false;
  base::win::ScopedGDIObject<HPEN> border_pen_;

  webrtc::DesktopVector mouse_position_;

  base::WeakPtrFactory<DisconnectWindowWin> weak_factory_{this};
};

// Returns the text for the given dialog control window.
bool GetControlText(HWND control, std::wstring* text) {
  // GetWindowText truncates the text if it is longer than can fit into
  // the buffer.
  WCHAR buffer[256];
  int result = GetWindowText(control, buffer, std::size(buffer));
  if (!result) {
    return false;
  }

  text->assign(buffer);
  return true;
}

// Returns width |text| rendered in |control| window.
bool GetControlTextWidth(HWND control, const std::wstring& text, LONG* width) {
  RECT rect = {0, 0, 0, 0};
  base::win::ScopedGetDC dc(control);
  base::win::ScopedSelectObject font(
      dc, (HFONT)SendMessage(control, WM_GETFONT, 0, 0));
  if (!DrawText(dc, text.c_str(), -1, &rect, DT_CALCRECT | DT_SINGLELINE)) {
    return false;
  }

  *width = rect.right;
  return true;
}

DisconnectWindowWin::DisconnectWindowWin()
    : border_pen_(
          CreatePen(PS_SOLID, 5, RGB(0.13 * 255, 0.69 * 255, 0.11 * 255))) {}

DisconnectWindowWin::~DisconnectWindowWin() {
  EndDialog();
}

void DisconnectWindowWin::EnableAutoHide(
    std::unique_ptr<LocalInputMonitor> local_input_monitor) {
  local_input_monitor_ = std::move(local_input_monitor);
}

void DisconnectWindowWin::Start(
    const base::WeakPtr<ClientSessionControl>& client_session_control) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!client_session_control_);
  DCHECK(client_session_control);

  client_session_control_ = client_session_control;

  std::string client_jid = client_session_control_->client_jid();
  username_ = client_jid.substr(0, client_jid.find('/'));
  if (!BeginDialog()) {
    EndDialog();
    return;
  }

  if (local_input_monitor_) {
    local_input_monitor_->StartMonitoring(
        base::BindRepeating(&DisconnectWindowWin::OnLocalMouseEvent,
                            weak_factory_.GetWeakPtr()),
        base::BindRepeating(&DisconnectWindowWin::OnLocalKeyPressed,
                            weak_factory_.GetWeakPtr()),
        base::BindRepeating(&DisconnectWindowWin::StopAutoHideBehavior,
                            weak_factory_.GetWeakPtr()));

    auto_hide_timer_.Start(FROM_HERE, kAutoHideTimeout,
                           base::BindOnce(&DisconnectWindowWin::HideDialog,
                                          base::Unretained(this)));
  }
}

INT_PTR CALLBACK DisconnectWindowWin::DialogProc(HWND hwnd,
                                                 UINT message,
                                                 WPARAM wparam,
                                                 LPARAM lparam) {
  LONG_PTR self = 0;
  if (message == WM_INITDIALOG) {
    self = lparam;

    // Store |this| to the window's user data.
    SetLastError(ERROR_SUCCESS);
    LONG_PTR result = SetWindowLongPtr(hwnd, DWLP_USER, self);
    if (result == 0 && GetLastError() != ERROR_SUCCESS) {
      reinterpret_cast<DisconnectWindowWin*>(self)->EndDialog();
    }
  } else {
    self = GetWindowLongPtr(hwnd, DWLP_USER);
  }

  if (self) {
    return reinterpret_cast<DisconnectWindowWin*>(self)->OnDialogMessage(
        hwnd, message, wparam, lparam);
  }
  return FALSE;
}

BOOL DisconnectWindowWin::OnDialogMessage(HWND hwnd,
                                          UINT message,
                                          WPARAM wparam,
                                          LPARAM lparam) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  switch (message) {
    // Ignore close messages.
    case WM_CLOSE:
      return TRUE;

    // Handle the Disconnect button.
    case WM_COMMAND:
      switch (LOWORD(wparam)) {
        case IDC_DISCONNECT:
          EndDialog();
          return TRUE;
      }
      return FALSE;

    // Ensure we don't try to use the HWND anymore.
    case WM_DESTROY:
      hwnd_ = nullptr;

      // Ensure that the disconnect callback is invoked even if somehow our
      // window gets destroyed.
      EndDialog();

      return TRUE;

    // Ensure the dialog stays visible if the work area dimensions change.
    case WM_SETTINGCHANGE:
      if (wparam == SPI_SETWORKAREA) {
        SetDialogPosition();
      }
      return TRUE;

    // Ensure the dialog stays visible if the display dimensions change.
    case WM_DISPLAYCHANGE:
      SetDialogPosition();
      return TRUE;

    // Handle the disconnect hot-key.
    case WM_HOTKEY:
      EndDialog();
      return TRUE;

    // Let the window be draggable by its client area by responding
    // that the entire window is the title bar.
    case WM_NCHITTEST:
      SetWindowLongPtr(hwnd, DWLP_MSGRESULT, HTCAPTION);
      return TRUE;

    case WM_PAINT: {
      // Draw the client area after ShowWindow is used to make |hwnd_| visible.
      PAINTSTRUCT ps;
      HDC hdc = BeginPaint(hwnd_, &ps);
      DrawBorder(hwnd_, hdc);
      EndPaint(hwnd_, &ps);
      return TRUE;
    }

    case WM_PRINTCLIENT: {
      // Refresh the dialog client area.  Called after AnimateWindow is used to
      // reshow the dialog.
      HDC hdc = reinterpret_cast<HDC>(wparam);
      DrawBorder(hwnd_, hdc);
      return TRUE;
    }
  }
  return FALSE;
}

bool DisconnectWindowWin::BeginDialog() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!hwnd_);

  hwnd_ =
      CreateDialogParam(CURRENT_MODULE(), MAKEINTRESOURCE(IDD_DISCONNECT),
                        nullptr, DialogProc, reinterpret_cast<LPARAM>(this));
  if (!hwnd_) {
    return false;
  }

  // Set up handler for Ctrl-Alt-Esc shortcut.
  if (!has_hotkey_ && RegisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID,
                                     MOD_ALT | MOD_CONTROL, VK_ESCAPE)) {
    has_hotkey_ = true;
  }

  if (!SetStrings()) {
    return false;
  }

  SetDialogPosition();
  ShowWindow(hwnd_, SW_SHOW);
  return IsWindowVisible(hwnd_) != FALSE;
}

void DisconnectWindowWin::EndDialog() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (has_hotkey_) {
    UnregisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID);
    has_hotkey_ = false;
  }

  if (hwnd_) {
    DestroyWindow(hwnd_);
    hwnd_ = nullptr;
  }

  // Disable auto-hide events since the window has been destroyed.
  auto_hide_timer_.Stop();

  if (client_session_control_) {
    client_session_control_->DisconnectSession(ErrorCode::OK);
  }
}

void DisconnectWindowWin::ShowDialog() {
  // Always reset the hide timer when this method is called.
  if (local_input_monitor_) {
    auto_hide_timer_.Start(FROM_HERE, kAutoHideTimeout,
                           base::BindOnce(&DisconnectWindowWin::HideDialog,
                                          base::Unretained(this)));
  }

  if (!was_auto_hidden_) {
    return;
  }

  // Make sure the dialog is fully visible when it is reshown.
  if (!local_input_seen_) {
    SetDialogPosition();
  }

  if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND)) {
    PLOG(ERROR) << "AnimateWindow() failed to show dialog: ";
    ShowWindow(hwnd_, SW_SHOW);

    // If the window still isn't visible, then disconnect the session.
    if (!IsWindowVisible(hwnd_)) {
      client_session_control_->DisconnectSession(ErrorCode::OK);
    }
  }
  was_auto_hidden_ = false;
}

void DisconnectWindowWin::HideDialog() {
  if (was_auto_hidden_ || !local_input_monitor_ || !hwnd_) {
    return;
  }

  if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND | AW_HIDE)) {
    PLOG(ERROR) << "AnimateWindow() failed to hide dialog: ";
  } else {
    was_auto_hidden_ = true;
  }
}

void DisconnectWindowWin::StopAutoHideBehavior() {
  auto_hide_timer_.Stop();
  local_input_monitor_.reset();

  ShowDialog();
}

void DisconnectWindowWin::OnLocalMouseEvent(
    const webrtc::DesktopVector& position,
    ui::EventType type) {
  // Don't show the dialog if the position changes by ~1px in any direction.
  // This will prevent the dialog from being reshown due to small movements
  // caused by hardware/software issues which cause cursor drift or small
  // vibrations in the environment around the remote host.
  if (std::abs(position.x() - mouse_position_.x()) > 1 ||
      std::abs(position.y() - mouse_position_.y()) > 1) {
    // Show the dialog before setting |local_input_seen_|.  That way the dialog
    // will be shown in the center position and subsequent reshows will honor
    // the new position (if any) the dialog is moved to.
    ShowDialog();
    local_input_seen_ = true;
  }

  mouse_position_ = position;
}

void DisconnectWindowWin::OnLocalKeyPressed(uint32_t usb_keycode) {
  // Show the dialog before setting |local_input_seen_|.  That way the dialog
  // will be shown in the center position and subsequent reshows will honor
  // the new position (if any) the dialog is moved to.
  ShowDialog();
  local_input_seen_ = true;
}

void DisconnectWindowWin::DrawBorder(HWND hwnd, HDC hdc) {
  RECT rect;
  GetClientRect(hwnd, &rect);
  base::win::ScopedSelectObject border(hdc, border_pen_.get());
  base::win::ScopedSelectObject brush(hdc, GetStockObject(NULL_BRUSH));
  RoundRect(hdc, rect.left, rect.top, rect.right - 1, rect.bottom - 1,
            kWindowBorderRadius, kWindowBorderRadius);
}

// Returns |control| rectangle in the dialog coordinates.
bool DisconnectWindowWin::GetControlRect(HWND control, RECT* rect) {
  if (!GetWindowRect(control, rect)) {
    return false;
  }
  SetLastError(ERROR_SUCCESS);
  int result =
      MapWindowPoints(HWND_DESKTOP, hwnd_, reinterpret_cast<LPPOINT>(rect), 2);
  if (!result && GetLastError() != ERROR_SUCCESS) {
    return false;
  }

  return true;
}

void DisconnectWindowWin::SetDialogPosition() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Try to center the window above the task-bar. If that fails, use the
  // primary monitor. If that fails (very unlikely), use the default position.
  HWND taskbar = FindWindow(kShellTrayWindowName, nullptr);
  HMONITOR monitor = MonitorFromWindow(taskbar, MONITOR_DEFAULTTOPRIMARY);
  MONITORINFO monitor_info = {sizeof(monitor_info)};
  RECT window_rect;
  if (!GetMonitorInfo(monitor, &monitor_info) ||
      !GetWindowRect(hwnd_, &window_rect)) {
    return;
  }

  int window_width = window_rect.right - window_rect.left;
  int window_height = window_rect.bottom - window_rect.top;

  // Default settings will display the window above the taskbar and centered
  // along the x axis.
  int top = monitor_info.rcWork.bottom - window_height;
  int left =
      (monitor_info.rcWork.right + monitor_info.rcWork.left - window_width) / 2;

  // Adjust the top value if the window is in auto-hide mode and we have not
  // seen local input yet.  We adjust the position to make the dialog a bit more
  // obtrusive so that a local user will notice it before it auto-hides.
  if (local_input_monitor_ && !local_input_seen_) {
    top = top * 0.7;
  }

  SetWindowPos(hwnd_, nullptr, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
}

bool DisconnectWindowWin::SetStrings() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Localize the disconnect button text and measure length of the old and new
  // labels.
  HWND hwnd_button = GetDlgItem(hwnd_, IDC_DISCONNECT);
  HWND hwnd_message = GetDlgItem(hwnd_, IDC_DISCONNECT_SHARINGWITH);
  if (!hwnd_button || !hwnd_message) {
    return false;
  }

  std::wstring button_text;
  std::wstring message_text;
  if (!GetControlText(hwnd_button, &button_text) ||
      !GetControlText(hwnd_message, &message_text)) {
    return false;
  }

  // Format and truncate "Your desktop is shared with ..." message.
  message_text = base::AsWString(base::ReplaceStringPlaceholders(
      base::AsString16(message_text), base::UTF8ToUTF16(username_), nullptr));
  if (message_text.length() > kMaxSharingWithTextLength) {
    message_text.erase(kMaxSharingWithTextLength);
  }

  if (!SetWindowText(hwnd_message, message_text.c_str())) {
    return false;
  }

  // Calculate the margin between controls in pixels.
  RECT rect = {0};
  rect.right = kWindowTextMargin;
  if (!MapDialogRect(hwnd_, &rect)) {
    return false;
  }
  int margin = rect.right;

  // Resize |hwnd_message| so that the text is not clipped.
  RECT message_rect;
  if (!GetControlRect(hwnd_message, &message_rect)) {
    return false;
  }

  LONG control_width;
  if (!GetControlTextWidth(hwnd_message, message_text, &control_width)) {
    return false;
  }
  message_rect.right = message_rect.left + control_width + margin;

  if (!SetWindowPos(hwnd_message, nullptr, message_rect.left, message_rect.top,
                    message_rect.right - message_rect.left,
                    message_rect.bottom - message_rect.top, SWP_NOZORDER)) {
    return false;
  }

  // Reposition and resize |hwnd_button| as well.
  RECT button_rect;
  if (!GetControlRect(hwnd_button, &button_rect)) {
    return false;
  }

  if (!GetControlTextWidth(hwnd_button, button_text, &control_width)) {
    return false;
  }

  button_rect.left = message_rect.right;
  button_rect.right = button_rect.left + control_width + margin * 2;
  if (!SetWindowPos(hwnd_button, nullptr, button_rect.left, button_rect.top,
                    button_rect.right - button_rect.left,
                    button_rect.bottom - button_rect.top, SWP_NOZORDER)) {
    return false;
  }

  // Resize the whole window to fit the resized controls.
  RECT window_rect;
  if (!GetWindowRect(hwnd_, &window_rect)) {
    return false;
  }
  int width = button_rect.right + margin;
  int height = window_rect.bottom - window_rect.top;
  if (!SetWindowPos(hwnd_, nullptr, 0, 0, width, height,
                    SWP_NOMOVE | SWP_NOZORDER)) {
    return false;
  }

  // Make the corners of the disconnect window rounded.
  HRGN rgn = CreateRoundRectRgn(0, 0, width, height, kWindowBorderRadius,
                                kWindowBorderRadius);
  if (!rgn) {
    return false;
  }
  if (!SetWindowRgn(hwnd_, rgn, TRUE)) {
    return false;
  }

  return true;
}

}  // namespace

// static
std::unique_ptr<HostWindow> HostWindow::CreateDisconnectWindow() {
  return std::make_unique<DisconnectWindowWin>();
}

std::unique_ptr<HostWindow> HostWindow::CreateAutoHidingDisconnectWindow(
    std::unique_ptr<LocalInputMonitor> local_input_monitor) {
  auto disconnect_window = std::make_unique<DisconnectWindowWin>();
  disconnect_window->EnableAutoHide(std::move(local_input_monitor));

  return disconnect_window;
}

}  // namespace remoting