1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
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 "ash/touch/touch_uma.h"
6
7#include "ash/shell_delegate.h"
8#include "base/metrics/histogram.h"
9#include "base/strings/stringprintf.h"
10#include "ui/aura/env.h"
11#include "ui/aura/root_window.h"
12#include "ui/aura/window.h"
13#include "ui/aura/window_property.h"
14#include "ui/base/events/event.h"
15#include "ui/base/events/event_utils.h"
16#include "ui/gfx/point_conversions.h"
17
18#if defined(USE_XI2_MT)
19#include <X11/extensions/XInput2.h>
20#include <X11/Xlib.h>
21#endif
22
23namespace {
24
25enum UMAEventType {
26  UMA_ET_UNKNOWN,
27  UMA_ET_TOUCH_RELEASED,
28  UMA_ET_TOUCH_PRESSED,
29  UMA_ET_TOUCH_MOVED,
30  UMA_ET_TOUCH_STATIONARY,
31  UMA_ET_TOUCH_CANCELLED,
32  UMA_ET_GESTURE_SCROLL_BEGIN,
33  UMA_ET_GESTURE_SCROLL_END,
34  UMA_ET_GESTURE_SCROLL_UPDATE,
35  UMA_ET_GESTURE_TAP,
36  UMA_ET_GESTURE_TAP_DOWN,
37  UMA_ET_GESTURE_BEGIN,
38  UMA_ET_GESTURE_END,
39  UMA_ET_GESTURE_DOUBLE_TAP,
40  UMA_ET_GESTURE_TRIPLE_TAP,
41  UMA_ET_GESTURE_TWO_FINGER_TAP,
42  UMA_ET_GESTURE_PINCH_BEGIN,
43  UMA_ET_GESTURE_PINCH_END,
44  UMA_ET_GESTURE_PINCH_UPDATE,
45  UMA_ET_GESTURE_LONG_PRESS,
46  UMA_ET_GESTURE_MULTIFINGER_SWIPE,
47  UMA_ET_SCROLL,
48  UMA_ET_SCROLL_FLING_START,
49  UMA_ET_SCROLL_FLING_CANCEL,
50  UMA_ET_GESTURE_MULTIFINGER_SWIPE_3,
51  UMA_ET_GESTURE_MULTIFINGER_SWIPE_4P,  // 4+ fingers
52  UMA_ET_GESTURE_SCROLL_UPDATE_2,
53  UMA_ET_GESTURE_SCROLL_UPDATE_3,
54  UMA_ET_GESTURE_SCROLL_UPDATE_4P,
55  UMA_ET_GESTURE_PINCH_UPDATE_3,
56  UMA_ET_GESTURE_PINCH_UPDATE_4P,
57  UMA_ET_GESTURE_LONG_TAP,
58  // NOTE: Add new event types only immediately above this line. Make sure to
59  // update the enum list in tools/histogram/histograms.xml accordingly.
60  UMA_ET_COUNT
61};
62
63struct WindowTouchDetails {
64  // Move and start times of the touch points. The key is the touch-id.
65  std::map<int, base::TimeDelta> last_move_time_;
66  std::map<int, base::TimeDelta> last_start_time_;
67
68  // The first and last positions of the touch points.
69  std::map<int, gfx::Point> start_touch_position_;
70  std::map<int, gfx::Point> last_touch_position_;
71
72  // Last time-stamp of the last touch-end event.
73  base::TimeDelta last_release_time_;
74
75  // Stores the time of the last touch released on this window (if there was a
76  // multi-touch gesture on the window, then this is the release-time of the
77  // last touch on the window).
78  base::TimeDelta last_mt_time_;
79};
80
81DEFINE_OWNED_WINDOW_PROPERTY_KEY(WindowTouchDetails,
82                                 kWindowTouchDetails,
83                                 NULL);
84
85
86UMAEventType UMAEventTypeFromEvent(const ui::Event& event) {
87  switch (event.type()) {
88    case ui::ET_TOUCH_RELEASED:
89      return UMA_ET_TOUCH_RELEASED;
90    case ui::ET_TOUCH_PRESSED:
91      return UMA_ET_TOUCH_PRESSED;
92    case ui::ET_TOUCH_MOVED:
93      return UMA_ET_TOUCH_MOVED;
94    case ui::ET_TOUCH_STATIONARY:
95      return UMA_ET_TOUCH_STATIONARY;
96    case ui::ET_TOUCH_CANCELLED:
97      return UMA_ET_TOUCH_CANCELLED;
98    case ui::ET_GESTURE_SCROLL_BEGIN:
99      return UMA_ET_GESTURE_SCROLL_BEGIN;
100    case ui::ET_GESTURE_SCROLL_END:
101      return UMA_ET_GESTURE_SCROLL_END;
102    case ui::ET_GESTURE_SCROLL_UPDATE: {
103      const ui::GestureEvent& gesture =
104          static_cast<const ui::GestureEvent&>(event);
105      if (gesture.details().touch_points() >= 4)
106        return UMA_ET_GESTURE_SCROLL_UPDATE_4P;
107      else if (gesture.details().touch_points() == 3)
108        return UMA_ET_GESTURE_SCROLL_UPDATE_3;
109      else if (gesture.details().touch_points() == 2)
110        return UMA_ET_GESTURE_SCROLL_UPDATE_2;
111      return UMA_ET_GESTURE_SCROLL_UPDATE;
112    }
113    case ui::ET_GESTURE_TAP: {
114      const ui::GestureEvent& gesture =
115          static_cast<const ui::GestureEvent&>(event);
116      int tap_count = gesture.details().tap_count();
117      if (tap_count == 1)
118        return UMA_ET_GESTURE_TAP;
119      if (tap_count == 2)
120        return UMA_ET_GESTURE_DOUBLE_TAP;
121      if (tap_count == 3)
122        return UMA_ET_GESTURE_TRIPLE_TAP;
123      NOTREACHED() << "Received tap with tapcount " << tap_count;
124      return UMA_ET_UNKNOWN;
125    }
126    case ui::ET_GESTURE_TAP_DOWN:
127      return UMA_ET_GESTURE_TAP_DOWN;
128    case ui::ET_GESTURE_BEGIN:
129      return UMA_ET_GESTURE_BEGIN;
130    case ui::ET_GESTURE_END:
131      return UMA_ET_GESTURE_END;
132    case ui::ET_GESTURE_TWO_FINGER_TAP:
133      return UMA_ET_GESTURE_TWO_FINGER_TAP;
134    case ui::ET_GESTURE_PINCH_BEGIN:
135      return UMA_ET_GESTURE_PINCH_BEGIN;
136    case ui::ET_GESTURE_PINCH_END:
137      return UMA_ET_GESTURE_PINCH_END;
138    case ui::ET_GESTURE_PINCH_UPDATE: {
139      const ui::GestureEvent& gesture =
140          static_cast<const ui::GestureEvent&>(event);
141      if (gesture.details().touch_points() >= 4)
142        return UMA_ET_GESTURE_PINCH_UPDATE_4P;
143      else if (gesture.details().touch_points() == 3)
144        return UMA_ET_GESTURE_PINCH_UPDATE_3;
145      return UMA_ET_GESTURE_PINCH_UPDATE;
146    }
147    case ui::ET_GESTURE_LONG_PRESS:
148      return UMA_ET_GESTURE_LONG_PRESS;
149    case ui::ET_GESTURE_LONG_TAP:
150      return UMA_ET_GESTURE_LONG_TAP;
151    case ui::ET_GESTURE_MULTIFINGER_SWIPE: {
152      const ui::GestureEvent& gesture =
153          static_cast<const ui::GestureEvent&>(event);
154      if (gesture.details().touch_points() >= 4)
155        return UMA_ET_GESTURE_MULTIFINGER_SWIPE_4P;
156      else if (gesture.details().touch_points() == 3)
157        return UMA_ET_GESTURE_MULTIFINGER_SWIPE_3;
158      return UMA_ET_GESTURE_MULTIFINGER_SWIPE;
159    }
160    case ui::ET_SCROLL:
161      return UMA_ET_SCROLL;
162    case ui::ET_SCROLL_FLING_START:
163      return UMA_ET_SCROLL_FLING_START;
164    case ui::ET_SCROLL_FLING_CANCEL:
165      return UMA_ET_SCROLL_FLING_CANCEL;
166    default:
167      return UMA_ET_UNKNOWN;
168  }
169}
170
171}
172
173namespace ash {
174
175// static
176TouchUMA* TouchUMA::GetInstance() {
177  return Singleton<TouchUMA>::get();
178}
179
180void TouchUMA::RecordGestureEvent(aura::Window* target,
181                                  const ui::GestureEvent& event) {
182  UMA_HISTOGRAM_ENUMERATION("Ash.GestureCreated",
183                            UMAEventTypeFromEvent(event),
184                            UMA_ET_COUNT);
185
186  GestureActionType action = FindGestureActionType(target, event);
187  RecordGestureAction(action);
188
189  if (event.type() == ui::ET_GESTURE_END &&
190      event.details().touch_points() == 2) {
191    WindowTouchDetails* details = target->GetProperty(kWindowTouchDetails);
192    if (!details) {
193      LOG(ERROR) << "Window received gesture events without receiving any touch"
194                    " events";
195      return;
196    }
197    details->last_mt_time_ = event.time_stamp();
198  }
199}
200
201void TouchUMA::RecordGestureAction(GestureActionType action) {
202  if (action == GESTURE_UNKNOWN || action >= GESTURE_ACTION_COUNT)
203    return;
204  UMA_HISTOGRAM_ENUMERATION("Ash.GestureTarget", action,
205                            GESTURE_ACTION_COUNT);
206}
207
208void TouchUMA::RecordTouchEvent(aura::Window* target,
209                                const ui::TouchEvent& event) {
210  UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchRadius",
211      static_cast<int>(std::max(event.radius_x(), event.radius_y())),
212      1, 500, 100);
213
214  UpdateBurstData(event);
215
216  WindowTouchDetails* details = target->GetProperty(kWindowTouchDetails);
217  if (!details) {
218    details = new WindowTouchDetails;
219    target->SetProperty(kWindowTouchDetails, details);
220  }
221
222  // Record the location of the touch points.
223  const int kBucketCountForLocation = 100;
224  const gfx::Rect bounds = target->GetRootWindow()->bounds();
225  const int bucket_size_x = std::max(1,
226                                     bounds.width() / kBucketCountForLocation);
227  const int bucket_size_y = std::max(1,
228                                     bounds.height() / kBucketCountForLocation);
229
230  gfx::Point position = event.root_location();
231
232  // Prefer raw event location (when available) over calibrated location.
233  if (event.HasNativeEvent()) {
234#if defined(USE_XI2_MT)
235    XEvent* xevent = event.native_event();
236    CHECK_EQ(GenericEvent, xevent->type);
237    XIEvent* xievent = static_cast<XIEvent*>(xevent->xcookie.data);
238    if (xievent->evtype == XI_TouchBegin ||
239        xievent->evtype == XI_TouchUpdate ||
240        xievent->evtype == XI_TouchEnd) {
241      XIDeviceEvent* device_event =
242          static_cast<XIDeviceEvent*>(xevent->xcookie.data);
243      position.SetPoint(static_cast<int>(device_event->event_x),
244                        static_cast<int>(device_event->event_y));
245    } else {
246      position = ui::EventLocationFromNative(event.native_event());
247    }
248#else
249    position = ui::EventLocationFromNative(event.native_event());
250#endif
251    position = gfx::ToFlooredPoint(
252        gfx::ScalePoint(position, 1. / target->layer()->device_scale_factor()));
253  }
254
255  position.set_x(std::min(bounds.width() - 1, std::max(0, position.x())));
256  position.set_y(std::min(bounds.height() - 1, std::max(0, position.y())));
257
258  UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchPositionX",
259      position.x() / bucket_size_x,
260      0, kBucketCountForLocation, kBucketCountForLocation + 1);
261  UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchPositionY",
262      position.y() / bucket_size_y,
263      0, kBucketCountForLocation, kBucketCountForLocation + 1);
264
265  if (event.type() == ui::ET_TOUCH_PRESSED) {
266    Shell::GetInstance()->delegate()->RecordUserMetricsAction(
267        UMA_TOUCHSCREEN_TAP_DOWN);
268
269    details->last_start_time_[event.touch_id()] = event.time_stamp();
270    details->start_touch_position_[event.touch_id()] = event.root_location();
271    details->last_touch_position_[event.touch_id()] = event.location();
272
273    if (details->last_release_time_.ToInternalValue()) {
274      // Measuring the interval between a touch-release and the next
275      // touch-start is probably less useful when doing multi-touch (e.g.
276      // gestures, or multi-touch friendly apps). So count this only if the user
277      // hasn't done any multi-touch during the last 30 seconds.
278      base::TimeDelta diff = event.time_stamp() - details->last_mt_time_;
279      if (diff.InSeconds() > 30) {
280        base::TimeDelta gap = event.time_stamp() - details->last_release_time_;
281        UMA_HISTOGRAM_COUNTS_10000("Ash.TouchStartAfterEnd",
282            gap.InMilliseconds());
283      }
284    }
285
286    // Record the number of touch-points currently active for the window.
287    const int kMaxTouchPoints = 10;
288    UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.ActiveTouchPoints",
289        details->last_start_time_.size(),
290        1, kMaxTouchPoints, kMaxTouchPoints + 1);
291  } else if (event.type() == ui::ET_TOUCH_RELEASED) {
292    if (details->last_start_time_.count(event.touch_id())) {
293      base::TimeDelta duration = event.time_stamp() -
294                                 details->last_start_time_[event.touch_id()];
295      UMA_HISTOGRAM_COUNTS_100("Ash.TouchDuration", duration.InMilliseconds());
296
297      // Look for touches that were [almost] stationary for a long time.
298      const double kLongStationaryTouchDuration = 10;
299      const int kLongStationaryTouchDistanceSquared = 100;
300      if (duration.InSecondsF() > kLongStationaryTouchDuration) {
301        gfx::Vector2d distance = event.root_location() -
302            details->start_touch_position_[event.touch_id()];
303        if (distance.LengthSquared() < kLongStationaryTouchDistanceSquared) {
304          UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.StationaryTouchDuration",
305              duration.InSeconds(),
306              kLongStationaryTouchDuration,
307              1000,
308              20);
309        }
310      }
311    }
312    details->last_start_time_.erase(event.touch_id());
313    details->last_move_time_.erase(event.touch_id());
314    details->start_touch_position_.erase(event.touch_id());
315    details->last_touch_position_.erase(event.touch_id());
316    details->last_release_time_ = event.time_stamp();
317  } else if (event.type() == ui::ET_TOUCH_MOVED) {
318    int distance = 0;
319    if (details->last_touch_position_.count(event.touch_id())) {
320      gfx::Point lastpos = details->last_touch_position_[event.touch_id()];
321      distance = abs(lastpos.x() - event.x()) + abs(lastpos.y() - event.y());
322    }
323
324    if (details->last_move_time_.count(event.touch_id())) {
325      base::TimeDelta move_delay = event.time_stamp() -
326                                   details->last_move_time_[event.touch_id()];
327      UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchMoveInterval",
328                                  move_delay.InMilliseconds(),
329                                  1, 50, 25);
330    }
331
332    UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchMoveSteps", distance, 1, 1000, 50);
333
334    details->last_move_time_[event.touch_id()] = event.time_stamp();
335    details->last_touch_position_[event.touch_id()] = event.location();
336  }
337}
338
339TouchUMA::TouchUMA()
340    : touch_in_progress_(false),
341      burst_length_(0) {
342}
343
344TouchUMA::~TouchUMA() {
345}
346
347void TouchUMA::UpdateBurstData(const ui::TouchEvent& event) {
348  if (event.type() == ui::ET_TOUCH_PRESSED) {
349    if (!touch_in_progress_) {
350      base::TimeDelta difference = event.time_stamp() - last_touch_down_time_;
351      if (difference > base::TimeDelta::FromMilliseconds(250)) {
352        if (burst_length_) {
353          UMA_HISTOGRAM_COUNTS_100("Ash.TouchStartBurst",
354                                   std::min(burst_length_, 100));
355        }
356        burst_length_ = 1;
357      } else {
358        ++burst_length_;
359      }
360    }
361    touch_in_progress_ = true;
362    last_touch_down_time_ = event.time_stamp();
363  } else if (event.type() == ui::ET_TOUCH_RELEASED) {
364    if (!aura::Env::GetInstance()->is_touch_down())
365      touch_in_progress_ = false;
366  }
367}
368
369TouchUMA::GestureActionType TouchUMA::FindGestureActionType(
370    aura::Window* window,
371    const ui::GestureEvent& event) {
372  if (!window || window->GetRootWindow() == window) {
373    if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN)
374      return GESTURE_BEZEL_SCROLL;
375    if (event.type() == ui::ET_GESTURE_BEGIN)
376      return GESTURE_BEZEL_DOWN;
377    return GESTURE_UNKNOWN;
378  }
379
380  std::string name = window ? window->name() : std::string();
381
382  const char kDesktopBackgroundView[] = "DesktopBackgroundView";
383  if (name == kDesktopBackgroundView) {
384    if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN)
385      return GESTURE_DESKTOP_SCROLL;
386    if (event.type() == ui::ET_GESTURE_PINCH_BEGIN)
387      return GESTURE_DESKTOP_PINCH;
388    return GESTURE_UNKNOWN;
389  }
390
391  const char kWebPage[] = "RenderWidgetHostViewAura";
392  if (name == kWebPage) {
393    if (event.type() == ui::ET_GESTURE_PINCH_BEGIN)
394      return GESTURE_WEBPAGE_PINCH;
395    if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN)
396      return GESTURE_WEBPAGE_SCROLL;
397    if (event.type() == ui::ET_GESTURE_TAP)
398      return GESTURE_WEBPAGE_TAP;
399    return GESTURE_UNKNOWN;
400  }
401
402  views::Widget* widget = views::Widget::GetWidgetForNativeView(window);
403  if (!widget)
404    return GESTURE_UNKNOWN;
405
406  views::View* view = widget->GetRootView()->
407      GetEventHandlerForPoint(event.location());
408  if (!view)
409    return GESTURE_UNKNOWN;
410
411  name = view->GetClassName();
412
413  const char kTabStrip[] = "TabStrip";
414  const char kTab[] = "BrowserTab";
415  if (name == kTabStrip || name == kTab) {
416    if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN)
417      return GESTURE_TABSTRIP_SCROLL;
418    if (event.type() == ui::ET_GESTURE_PINCH_BEGIN)
419      return GESTURE_TABSTRIP_PINCH;
420    if (event.type() == ui::ET_GESTURE_TAP)
421      return GESTURE_TABSTRIP_TAP;
422    return GESTURE_UNKNOWN;
423  }
424
425  const char kOmnibox[] = "BrowserOmniboxViewViews";
426  if (name == kOmnibox) {
427    if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN)
428      return GESTURE_OMNIBOX_SCROLL;
429    if (event.type() == ui::ET_GESTURE_PINCH_BEGIN)
430      return GESTURE_OMNIBOX_PINCH;
431    return GESTURE_UNKNOWN;
432  }
433
434  return GESTURE_UNKNOWN;
435}
436
437}  // namespace ash
438