| // 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 |