blob: 5fd2c62a2a90057581866d3ddaf94c55a8bb97f1 [file] [log] [blame]
Michelle58ac84702023-08-16 23:41:461// Copyright 2023 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ui/touch_selection/touch_selection_metrics.h"
6
7#include "base/metrics/histogram_functions.h"
Michelle615d1d12023-08-23 00:26:208#include "base/time/time.h"
Michelle615d1d12023-08-23 00:26:209#include "ui/events/event.h"
Joseph Parkf1147f742024-10-18 17:39:1710#include "ui/touch_selection/touch_editing_controller.h"
Michelle58ac84702023-08-16 23:41:4611
12namespace ui {
13
14namespace {
15
Michelle615d1d12023-08-23 00:26:2016constexpr int kSessionTouchDownCountMin = 1;
17constexpr int kSessionTouchDownCountMax = 20;
18constexpr int kSessionTouchDownCountBuckets = 20;
19
20// Duration of inactivity after which we consider a touch selection session to
21// have timed out for the purpose of determining session action count metrics.
22constexpr base::TimeDelta kSessionTimeoutDuration = base::Seconds(10);
23
Michelle58ac84702023-08-16 23:41:4624TouchSelectionMenuAction MapCommandIdToMenuAction(int command_id) {
25 switch (command_id) {
Michelle615d1d12023-08-23 00:26:2026 case TouchEditable::kCut:
Michelle58ac84702023-08-16 23:41:4627 return TouchSelectionMenuAction::kCut;
Michelle615d1d12023-08-23 00:26:2028 case TouchEditable::kCopy:
Michelle58ac84702023-08-16 23:41:4629 return TouchSelectionMenuAction::kCopy;
Michelle615d1d12023-08-23 00:26:2030 case TouchEditable::kPaste:
Michelle58ac84702023-08-16 23:41:4631 return TouchSelectionMenuAction::kPaste;
Michelle615d1d12023-08-23 00:26:2032 case TouchEditable::kSelectAll:
Michelle58ac84702023-08-16 23:41:4633 return TouchSelectionMenuAction::kSelectAll;
Michelle615d1d12023-08-23 00:26:2034 case TouchEditable::kSelectWord:
Michelle58ac84702023-08-16 23:41:4635 return TouchSelectionMenuAction::kSelectWord;
36 default:
Peter Boström01ab59a2024-08-15 02:39:4937 NOTREACHED() << "Invalid command id: " << command_id;
Michelle58ac84702023-08-16 23:41:4638 }
39}
40
Michelle615d1d12023-08-23 00:26:2041// We want to record the touch down count required to get to a successful cursor
42// placement or selection, but it's hard to know if this has happened. We'll
43// just consider a session to be successful if it ends in a character key event
44// or an IME fabricated key event (e.g. from the ChromeOS virtual keyboard).
45bool IsSuccessfulSessionEndEvent(const Event& session_end_event) {
46 if (!session_end_event.IsKeyEvent()) {
47 return false;
48 }
49
50 return session_end_event.AsKeyEvent()->GetDomKey().IsCharacter() ||
51 session_end_event.flags() & EF_IME_FABRICATED_KEY;
52}
53
Michelle58ac84702023-08-16 23:41:4654} // namespace
55
56void RecordTouchSelectionDrag(TouchSelectionDragType drag_type) {
57 base::UmaHistogramEnumeration(kTouchSelectionDragTypeHistogramName,
58 drag_type);
59}
60
61void RecordTouchSelectionMenuCommandAction(int command_id) {
62 base::UmaHistogramEnumeration(kTouchSelectionMenuActionHistogramName,
63 MapCommandIdToMenuAction(command_id));
64}
65
66void RecordTouchSelectionMenuEllipsisAction() {
67 base::UmaHistogramEnumeration(kTouchSelectionMenuActionHistogramName,
68 TouchSelectionMenuAction::kEllipsis);
69}
70
71void RecordTouchSelectionMenuSmartAction() {
72 base::UmaHistogramEnumeration(kTouchSelectionMenuActionHistogramName,
73 TouchSelectionMenuAction::kSmartAction);
74}
75
Michelle615d1d12023-08-23 00:26:2076TouchSelectionSessionMetricsRecorder::TouchSelectionSessionMetricsRecorder() =
77 default;
78
79TouchSelectionSessionMetricsRecorder::~TouchSelectionSessionMetricsRecorder() =
80 default;
81
82void TouchSelectionSessionMetricsRecorder::OnCursorActivationEvent() {
83 if (!IsSessionActive()) {
84 // We assume that an initial activation event occurs after a single touch
85 // down movement (from a tap or long press). This is not always correct,
86 // e.g. if the user double taps quickly enough then the cursor event from
87 // the first tap might occur after the second tap was already detected. But
88 // it should be ok to assume that this won't be a problem most of the time.
89 session_touch_down_count_ = 1;
90 }
91
92 active_status_ = ActiveStatus::kActiveCursor;
93}
94
95void TouchSelectionSessionMetricsRecorder::OnSelectionActivationEvent() {
96 if (!IsSessionActive()) {
97 // We assume that an initial activation event occurs after a single touch
98 // down movement (from a long press), since a selection event from a
99 // repeated tap would usually only occur after a cursor event from the
100 // first tap has already started the session.
101 session_touch_down_count_ = 1;
102 }
103
104 active_status_ = ActiveStatus::kActiveSelection;
105}
106
107void TouchSelectionSessionMetricsRecorder::OnTouchEvent(bool is_down_event) {
108 RefreshSessionStatus();
109 if (!IsSessionActive()) {
110 return;
111 }
112
113 session_touch_down_count_ += is_down_event;
114}
115
116void TouchSelectionSessionMetricsRecorder::OnMenuCommand(
117 bool should_end_session) {
118 RefreshSessionStatus();
119 if (!IsSessionActive()) {
120 return;
121 }
122
123 // We assume that a menu button was tapped, but only include this in the touch
124 // down count if the session continues (since we want to know the touch down
125 // count required to get to a successful cursor placement or selection, which
126 // would have occurred before the menu button was tapped).
127 if (should_end_session) {
128 RecordSessionMetrics();
129 ResetMetrics();
130 } else {
131 session_touch_down_count_++;
132 }
133}
134
135void TouchSelectionSessionMetricsRecorder::OnSessionEndEvent(
136 const Event& session_end_event) {
137 RefreshSessionStatus();
138 if (!IsSessionActive()) {
139 return;
140 }
141
142 if (IsSuccessfulSessionEndEvent(session_end_event)) {
143 RecordSessionMetrics();
144 }
145 ResetMetrics();
146}
147
148void TouchSelectionSessionMetricsRecorder::ResetMetrics() {
149 active_status_ = ActiveStatus::kInactive;
150 last_activity_time_ = base::TimeTicks();
151 session_touch_down_count_ = 0;
152}
153
154void TouchSelectionSessionMetricsRecorder::RefreshSessionStatus() {
155 // After a period of inactivity, we consider a session to have timed out since
156 // the user intent has probably changed.
157 if (last_activity_time_ + kSessionTimeoutDuration < base::TimeTicks::Now()) {
158 ResetMetrics();
159 }
160
161 last_activity_time_ = base::TimeTicks::Now();
162}
163
164bool TouchSelectionSessionMetricsRecorder::IsSessionActive() const {
165 return active_status_ != ActiveStatus::kInactive;
166}
167
168void TouchSelectionSessionMetricsRecorder::RecordSessionMetrics() const {
169 if (!IsSessionActive()) {
170 return;
171 }
172
173 base::UmaHistogramCustomCounts(
174 active_status_ == ActiveStatus::kActiveCursor
175 ? kTouchCursorSessionTouchDownCountHistogramName
176 : kTouchSelectionSessionTouchDownCountHistogramName,
177 session_touch_down_count_, kSessionTouchDownCountMin,
178 kSessionTouchDownCountMax, kSessionTouchDownCountBuckets);
179}
180
Michelle58ac84702023-08-16 23:41:46181} // namespace ui