// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/graphics/gestures/side_swipe_detector.h"
#include <deque>
#include "base/auto_reset.h"
#include "chromecast/base/chromecast_switches.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/events/event_rewriter.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace chromecast {
namespace {
// The number of pixels from the very left or right of the screen to consider as
// a valid origin for the left or right swipe gesture.
constexpr int kDefaultSideGestureStartWidth = 35;
// The number of pixels from the very top or bottom of the screen to consider as
// a valid origin for the top or bottom swipe gesture.
constexpr int kDefaultSideGestureStartHeight = 35;
// The amount of time after gesture start to allow events which occur in the
// margin to be stashed and replayed within. For example a tap event which
// occurs inside the gesture margin will be valid as long as it occurs within
// the time specified by this threshold.
constexpr base::TimeDelta kGestureMarginEventsTimeLimit =
base::Milliseconds(500);
// Get the correct bottom gesture start height by checking both margin flags in
// order, and then the default value if neither is set.
int BottomGestureStartHeight() {
return GetSwitchValueInt(
switches::kBottomSystemGestureStartHeight,
GetSwitchValueInt(switches::kSystemGestureStartHeight,
kDefaultSideGestureStartHeight));
}
} // namespace
SideSwipeDetector::SideSwipeDetector(CastGestureHandler* gesture_handler,
aura::Window* root_window)
: gesture_start_width_(GetSwitchValueInt(switches::kSystemGestureStartWidth,
kDefaultSideGestureStartWidth)),
gesture_start_height_(
GetSwitchValueInt(switches::kSystemGestureStartHeight,
kDefaultSideGestureStartHeight)),
bottom_gesture_start_height_(BottomGestureStartHeight()),
gesture_handler_(gesture_handler),
root_window_(root_window),
current_swipe_(CastSideSwipeOrigin::NONE),
current_pointer_id_(ui::kPointerIdUnknown) {
DCHECK(gesture_handler);
DCHECK(root_window);
root_window_->GetHost()->GetEventSource()->AddEventRewriter(this);
}
SideSwipeDetector::~SideSwipeDetector() {
root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this);
}
CastSideSwipeOrigin SideSwipeDetector::GetDragPosition(
const gfx::Point& point,
const gfx::Rect& screen_bounds) const {
if (point.y() < (screen_bounds.y() + gesture_start_height_)) {
return CastSideSwipeOrigin::TOP;
}
if (point.x() < (screen_bounds.x() + gesture_start_width_)) {
return CastSideSwipeOrigin::LEFT;
}
if (point.x() >
(screen_bounds.x() + screen_bounds.width() - gesture_start_width_)) {
return CastSideSwipeOrigin::RIGHT;
}
if (point.y() > (screen_bounds.y() + screen_bounds.height() -
bottom_gesture_start_height_)) {
return CastSideSwipeOrigin::BOTTOM;
}
return CastSideSwipeOrigin::NONE;
}
void SideSwipeDetector::StashEvent(const ui::TouchEvent& event) {
// If the time since the gesture start is longer than our threshold, do not
// stash the event (and clear the stashed events).
if (current_swipe_time_.Elapsed() > kGestureMarginEventsTimeLimit) {
stashed_events_.clear();
return;
}
stashed_events_.push_back(event);
}
ui::EventDispatchDetails SideSwipeDetector::RewriteEvent(
const ui::Event& event,
const Continuation continuation) {
if (!event.IsTouchEvent()) {
return SendEvent(continuation, &event);
}
const ui::TouchEvent* touch_event = event.AsTouchEvent();
// Touch events come through in screen pixels, but untransformed. This is the
// raw coordinate not yet mapped to the root window's coordinate system or the
// screen. Convert it into the root window's coordinate system, in DIP which
// is what the rest of this class expects.
gfx::Point touch_location = touch_event->root_location();
root_window_->GetHost()->ConvertPixelsToDIP(&touch_location);
gfx::Rect screen_bounds = display::Screen::GetScreen()
->GetDisplayNearestPoint(touch_location)
.bounds();
CastSideSwipeOrigin side_swipe_origin =
GetDragPosition(touch_location, screen_bounds);
// A located event has occurred inside the margin. It might be the start of
// our gesture, or a touch that we need to squash.
if (current_swipe_ == CastSideSwipeOrigin::NONE &&
side_swipe_origin != CastSideSwipeOrigin::NONE) {
// Check to see if we have any potential consumers of events on this side.
// If not, we can continue on without consuming it.
if (!gesture_handler_->CanHandleSwipe(side_swipe_origin)) {
return SendEvent(continuation, &event);
}
// Detect the beginning of a system gesture swipe.
if (touch_event->type() != ui::EventType::kTouchPressed) {
return SendEvent(continuation, &event);
}
current_swipe_ = side_swipe_origin;
current_pointer_id_ = touch_event->pointer_details().id;
// Let the subscribers know about the gesture begin.
gesture_handler_->HandleSideSwipe(CastSideSwipeEvent::BEGIN,
side_swipe_origin, touch_location);
DVLOG(1) << "side swipe gesture begin @ " << touch_location.ToString();
current_swipe_time_ = base::ElapsedTimer();
// Stash a copy of the event should we decide to reconstitute it later if we
// decide that this isn't in fact a side swipe.
StashEvent(*touch_event);
// Avoid corrupt gesture state caused by a missing kGestureScrollEnd event
// as we potentially transition between web views.
root_window_->CleanupGestureState();
// And then stop the original event from propagating.
return DiscardEvent(continuation);
}
// If no swipe in progress, just move on.
if (current_swipe_ == CastSideSwipeOrigin::NONE) {
return SendEvent(continuation, &event);
}
// If the finger involved is not the one we're looking for, discard it.
if (touch_event->pointer_details().id != current_pointer_id_) {
return DiscardEvent(continuation);
}
// A swipe is in progress, or has completed, so stop propagation of underlying
// gesture/touch events, after stashing a copy of the original event.
StashEvent(*touch_event);
// The finger has lifted, which means the end of the gesture, or if the finger
// hasn't travelled far enough, replay the original events.
if (touch_event->type() == ui::EventType::kTouchReleased) {
DVLOG(1) << "gesture release; time since press: "
<< current_swipe_time_.Elapsed().InMilliseconds() << "ms @ "
<< touch_location.ToString();
gesture_handler_->HandleSideSwipe(CastSideSwipeEvent::END, current_swipe_,
touch_location);
current_swipe_ = CastSideSwipeOrigin::NONE;
current_pointer_id_ = ui::kPointerIdUnknown;
// If the finger is still inside the touch margin at release, this is not
// really a side swipe. Stream out events we stashed for later retrieval.
if (side_swipe_origin != CastSideSwipeOrigin::NONE &&
!stashed_events_.empty()) {
ui::EventDispatchDetails details;
for (const auto& it : stashed_events_) {
details = SendEvent(continuation, &it);
if (details.dispatcher_destroyed)
break;
}
stashed_events_.clear();
return details;
}
// Otherwise, clear them.
stashed_events_.clear();
return DiscardEvent(continuation);
}
// The system gesture is ongoing...
gesture_handler_->HandleSideSwipe(CastSideSwipeEvent::CONTINUE,
current_swipe_, touch_location);
DVLOG(1) << "gesture continue; time since press: "
<< current_swipe_time_.Elapsed().InMilliseconds() << "ms @ "
<< touch_location.ToString();
return DiscardEvent(continuation);
}
} // namespace chromecast