910e62b5创建于 1月15日历史提交
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/pointer/touch_ui_controller.h"

#include <memory>
#include <string>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/no_destructor.h"
#include "base/task/current_thread.h"
#include "base/trace_event/trace_event.h"
#include "ui/base/ui_base_switches.h"

#if BUILDFLAG(IS_WIN)
#include <windows.h>

#include "base/callback_list.h"
#include "base/win/win_util.h"
#include "ui/gfx/win/singleton_hwnd.h"
#endif

namespace ui {

namespace {

#if BUILDFLAG(USE_BLINK)
void RecordPointerDigitizerTypeOnStartup(const PointerDevice& device) {
  base::UmaHistogramEnumeration("Input.Digitizer.OnStartup", device.digitizer);
}

void RecordPointerDigitizerTypeOnConnected(const PointerDevice& device) {
  base::UmaHistogramEnumeration("Input.Digitizer.OnConnected",
                                device.digitizer);
}

void RecordPointerDigitizerTypeOnDisconnected(const PointerDevice& device) {
  base::UmaHistogramEnumeration("Input.Digitizer.OnDisconnected",
                                device.digitizer);
}

void RecordMaxTouchPointsHistogram(const char* histogram_name, int32_t sample) {
  // Record max touch points. Maximum value is 30, after which the values will
  // be recorded in an overflow bucket. The maximum of 30 is chosen based on 3
  // people interacting with both hands on a screen simultaneously.
  base::UmaHistogramExactLinear(histogram_name, sample, 30);
}

constexpr const char* GetMaxTouchPointsHistogramName(
    PointerDigitizerType type) {
  switch (type) {
    case PointerDigitizerType::kUnknown:
      return "Input.Digitizer.MaxTouchPoints.Unknown";
    case PointerDigitizerType::kDirectPen:
      return "Input.Digitizer.MaxTouchPoints.DirectPen";
    case PointerDigitizerType::kIndirectPen:
      return "Input.Digitizer.MaxTouchPoints.IndirectPen";
    case PointerDigitizerType::kTouch:
      return "Input.Digitizer.MaxTouchPoints.Touch";
    case PointerDigitizerType::kTouchPad:
      return "Input.Digitizer.MaxTouchPoints.TouchPad";
  }
  NOTREACHED();
}

void RecordPointerDigitizerTypeMaxTouchPoints(const PointerDevice& device) {
  RecordMaxTouchPointsHistogram(
      GetMaxTouchPointsHistogramName(device.digitizer),
      device.max_active_contacts);
}

void RecordMaxTouchPointsSupportedBySystem(int max_touch_points) {
  RecordMaxTouchPointsHistogram(
      "Input.Digitizer.MaxTouchPointsSupportedBySystemAtStartup",
      max_touch_points);
}
#endif  // BUILDFLAG(USE_BLINK)

#if BUILDFLAG(IS_WIN)

void RecordPostureModeOnStartup(bool tablet_mode) {
  base::UmaHistogramEnumeration("Touch.DevicePosture.Startup",
                                tablet_mode
                                    ? TouchUiController::PostureMode::kTablet
                                    : TouchUiController::PostureMode::kDesktop);
}

void RecordPostureModeOnSwitch(bool tablet_mode) {
  base::UmaHistogramEnumeration("Touch.DevicePosture.Switch",
                                tablet_mode
                                    ? TouchUiController::PostureMode::kTablet
                                    : TouchUiController::PostureMode::kDesktop);
}

bool IsWndProcMessageObserved(UINT message) {
#if BUILDFLAG(USE_BLINK)
  return message == WM_SETTINGCHANGE || message == WM_POINTERDEVICECHANGE;
#elif   // BUILDFLAG(USE_BLINK)
  return message == WM_SETTINGCHANGE;
#endif  // BUILDFLAG(USE_BLINK)
}

void SequencedWndProcHandler(UINT message, WPARAM wparam, LPARAM lparam) {
  switch (message) {
    case WM_SETTINGCHANGE:
      TouchUiController::Get()->RefreshTabletMode();
      break;
#if BUILDFLAG(USE_BLINK)
    case WM_POINTERDEVICECHANGE:
      if (wparam == PDC_ARRIVAL) {
        TouchUiController::Get()->OnPointerDeviceConnected(
            reinterpret_cast<HANDLE>(lparam));
      } else if (wparam == PDC_REMOVAL) {
        TouchUiController::Get()->OnPointerDeviceDisconnected(
            reinterpret_cast<HANDLE>(lparam));
      }
      break;
#endif  // BUILDFLAG(USE_BLINK)
    default:
      NOTREACHED();
  }
}

void OnWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
  if (!IsWndProcMessageObserved(message)) {
    return;
  }
  // Pass the work to a separate task to avoid possible jank when handling
  // winapi events. This also makes sure events are processed after
  // OnInitializePointerDevices which needs to run before handling
  // WM_POINTERDEVICECHANGE.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&SequencedWndProcHandler, message, wparam, lparam));
}

#endif  // BUILDFLAG(IS_WIN)

void RecordEnteredTouchMode() {
  base::RecordAction(base::UserMetricsAction("TouchMode.EnteredTouchMode"));
}

void RecordEnteredNonTouchMode() {
  base::RecordAction(base::UserMetricsAction("TouchMode.EnteredNonTouchMode"));
}

}  // namespace

TouchUiController::TouchUiScoperForTesting::TouchUiScoperForTesting(
    bool touch_ui_enabled,
    bool tablet_mode_enabled,
    TouchUiController* controller)
    : controller_(controller),
      old_ui_state_(controller_->SetTouchUiState(
          touch_ui_enabled ? TouchUiState::kEnabled : TouchUiState::kDisabled)),
      old_tablet_mode_(controller_->SetTabletMode(tablet_mode_enabled)) {}

TouchUiController::TouchUiScoperForTesting::~TouchUiScoperForTesting() {
  controller_->SetTouchUiState(old_ui_state_);
  controller_->SetTabletMode(old_tablet_mode_);
}

void TouchUiController::TouchUiScoperForTesting::UpdateState(bool enabled) {
  controller_->SetTouchUiState(enabled ? TouchUiState::kEnabled
                                       : TouchUiState::kDisabled);
}

void TouchUiController::TouchUiScoperForTesting::UpdateTabletMode(
    bool enabled) {
  controller_->SetTabletMode(enabled);
}

// static
TouchUiController* TouchUiController::Get() {
  static base::NoDestructor<TouchUiController> instance([] {
    const std::string switch_value =
        base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
            switches::kTopChromeTouchUi);
    if (switch_value == switches::kTopChromeTouchUiDisabled)
      return TouchUiState::kDisabled;
    const bool enabled = switch_value == switches::kTopChromeTouchUiEnabled;
    return enabled ? TouchUiState::kEnabled : TouchUiState::kAuto;
  }());
  return instance.get();
}

TouchUiController::TouchUiController(TouchUiState touch_ui_state)
    : touch_ui_state_(touch_ui_state) {
  if (base::CurrentUIThread::IsSet()) {
#if BUILDFLAG(USE_BLINK)
    // Pass the work to a separate task to avoid affecting browser startup time.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(&TouchUiController::OnInitializePointerDevices,
                       weak_factory_.GetWeakPtr()));
#if BUILDFLAG(IS_WIN)
    // Register to listen for WM_POINTERDEVICECHANGE.
    base::win::RegisterPointerDeviceNotifications(
        gfx::SingletonHwnd::GetInstance()->hwnd(),
        /*notify_proximity_changes=*/false);
#endif  // BUILDFLAG(IS_WIN)
#endif  // BUILDFLAG(USE_BLINK)

#if BUILDFLAG(IS_WIN)
    hwnd_subscription_ = gfx::SingletonHwnd::GetInstance()->RegisterCallback(
        base::BindRepeating(&OnWndProc));
    base::win::IsDeviceInTabletMode(
        gfx::SingletonHwnd::GetInstance()->hwnd(),
        base::BindOnce(&TouchUiController::SetInitialTabletMode,
                       weak_factory_.GetWeakPtr()));
#endif  // BUILDFLAG(IS_WIN)
  }
#if !BUILDFLAG(IS_WIN)
  if (touch_ui())
    RecordEnteredTouchMode();
  else
    RecordEnteredNonTouchMode();
#endif  // !BUILDFLAG(IS_WIN)
}

TouchUiController::~TouchUiController() = default;

void TouchUiController::OnTabletModeToggled(bool enabled) {
#if BUILDFLAG(IS_WIN)
  const bool was_tablet_mode = tablet_mode_;
#endif  // BUILDFLAG(IS_WIN)
  const bool was_touch_ui = touch_ui();
  tablet_mode_ = enabled;
  if (touch_ui() != was_touch_ui) {
    TouchUiChanged();
  }
#if BUILDFLAG(IS_WIN)
  if (tablet_mode_ != was_tablet_mode) {
    TabletModeChanged();
  }
#endif  // BUILDFLAG(IS_WIN)
}

#if BUILDFLAG(IS_WIN)
void TouchUiController::RefreshTabletMode() {
  base::win::IsDeviceInTabletMode(
      gfx::SingletonHwnd::GetInstance()->hwnd(),
      base::BindOnce(&TouchUiController::OnTabletModeToggled,
                     weak_factory_.GetWeakPtr()));
}

void TouchUiController::SetInitialTabletMode(bool enabled) {
  const bool was_tablet_mode = tablet_mode_;
  const bool was_touch_ui = touch_ui();
  tablet_mode_ = enabled;
  // Unconditionally record the histogram following discovery of the initial
  // mode.
  if (touch_ui()) {
    RecordEnteredTouchMode();
  } else {
    RecordEnteredNonTouchMode();
  }
  // Notify observers only if the mode has changed.
  if (touch_ui() != was_touch_ui) {
    TRACE_EVENT0("ui", "TouchUiController.NotifyListeners");
    touch_mode_callback_list_.Notify();
  }

  const auto& convertibility_enabled =
      base::win::GetConvertibilityEnabledOverride();
  if (!convertibility_enabled || *convertibility_enabled) {
    RecordPostureModeOnStartup(enabled);
    // Notify observers only if the posture mode has changed.
    if (tablet_mode_ != was_tablet_mode) {
      tablet_mode_callback_list_.Notify();
    }
  }
}
#endif  // BUILDFLAG(IS_WIN)

base::CallbackListSubscription TouchUiController::RegisterCallback(
    const base::RepeatingClosure& closure) {
  return touch_mode_callback_list_.Add(closure);
}

#if BUILDFLAG(IS_WIN)
base::CallbackListSubscription TouchUiController::RegisterTabletModeCallback(
    const base::RepeatingClosure& closure) {
  return tablet_mode_callback_list_.Add(closure);
}
#endif  // BUILDFLAG(IS_WIN)

TouchUiController::TouchUiState TouchUiController::SetTouchUiState(
    TouchUiState touch_ui_state) {
  const bool was_touch_ui = touch_ui();
  const TouchUiState old_state = std::exchange(touch_ui_state_, touch_ui_state);
  if (touch_ui() != was_touch_ui)
    TouchUiChanged();
  return old_state;
}

bool TouchUiController::SetTabletMode(bool tablet_mode_enabled) {
  const bool was_tablet_mode = tablet_mode_;
  tablet_mode_ = tablet_mode_enabled;
#if BUILDFLAG(IS_WIN)
  if (tablet_mode_ != was_tablet_mode) {
    TabletModeChanged();
  }
#endif  // BUILDFLAG(IS_WIN)
  return was_tablet_mode;
}

void TouchUiController::TouchUiChanged() {
  if (touch_ui())
    RecordEnteredTouchMode();
  else
    RecordEnteredNonTouchMode();

  TRACE_EVENT0("ui", "TouchUiController.NotifyListeners");
  touch_mode_callback_list_.Notify();
}

#if BUILDFLAG(IS_WIN)
void TouchUiController::TabletModeChanged() {
  RecordPostureModeOnSwitch(tablet_mode());
  tablet_mode_callback_list_.Notify();
}
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(USE_BLINK)
void TouchUiController::OnPointerDeviceConnected(PointerDevice::Key key) {
  if (const std::optional<PointerDevice> device = GetPointerDevice(key)) {
    RecordPointerDigitizerTypeOnConnected(*device);
    RecordPointerDigitizerTypeMaxTouchPoints(*device);
    last_known_pointer_devices_.emplace_back(*device);
  }
}

void TouchUiController::OnPointerDeviceDisconnected(PointerDevice::Key key) {
  // Iterative search should be fine because `last_known_pointer_devices_` is
  // expected to be a very small set.
  const auto iter = std::find(last_known_pointer_devices_.begin(),
                              last_known_pointer_devices_.end(), key);
  if (iter != last_known_pointer_devices_.end()) {
    RecordPointerDigitizerTypeOnDisconnected(*iter);
    last_known_pointer_devices_.erase(iter);
  }
}

int TouchUiController::MaxTouchPoints() const {
  return ::ui::MaxTouchPoints();
}

std::optional<PointerDevice> TouchUiController::GetPointerDevice(
    PointerDevice::Key key) const {
  return ::ui::GetPointerDevice(key);
}

std::vector<PointerDevice> TouchUiController::GetPointerDevices() const {
  return ::ui::GetPointerDevices();
}

const std::vector<PointerDevice>&
TouchUiController::GetLastKnownPointerDevicesForTesting() const {
  return last_known_pointer_devices_;
}

void TouchUiController::OnInitializePointerDevices() {
  last_known_pointer_devices_ = GetPointerDevices();
  for (const PointerDevice& device : last_known_pointer_devices_) {
    RecordPointerDigitizerTypeOnStartup(device);
    RecordPointerDigitizerTypeMaxTouchPoints(device);
  }
  RecordMaxTouchPointsSupportedBySystem(MaxTouchPoints());
}
#endif  // BUILDFLAG(USE_BLINK)

}  // namespace ui