blob: 6739d6805cc5f326e9665083b4df46658b913f2b [file] [log] [blame] [edit]
// 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