chromium/ash/wm/gestures/back_gesture/back_gesture_event_handler.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/wm/gestures/back_gesture/back_gesture_event_handler.h"

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/controls/contextual_tooltip.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/keyboard/keyboard_util.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/model/virtual_keyboard_model.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/gestures/back_gesture/back_gesture_affordance.h"
#include "ash/wm/gestures/back_gesture/back_gesture_contextual_nudge_controller_impl.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "base/containers/contains.h"
#include "base/debug/crash_logging.h"
#include "base/i18n/rtl.h"
#include "base/metrics/user_metrics.h"
#include "base/types/cxx23_to_underlying.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/aura/window.h"
#include "ui/display/screen.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// Distance from the divider's center point that reserved for splitview
// resizing in landscape orientation.
constexpr int kDistanceForSplitViewResize = 49;

// Called by CanStartGoingBack() to check whether we can start swiping from the
// split view divider to go back.
bool CanStartGoingBackFromSplitViewDivider(const gfx::Point& screen_location) {
  if (!IsCurrentScreenOrientationLandscape()) {
    return false;
  }

  // If virtual keyboard is visible when we swipe from the splitview divider
  // area, do not allow go back if the location is inside of the virtual
  // keyboard bounds.
  auto* keyboard_controller = keyboard::KeyboardUIController::Get();
  if (keyboard_controller->IsEnabled() &&
      keyboard_controller->GetVisualBoundsInScreen().Contains(
          screen_location)) {
    return false;
  }
  // Same thing for ARC virtual keyboard as well.
  SystemTrayModel* system_tray_model = Shell::Get()->system_tray_model();
  if (system_tray_model) {
    auto* virtual_keyboard = system_tray_model->virtual_keyboard();
    if (virtual_keyboard->arc_keyboard_visible() &&
        virtual_keyboard->arc_keyboard_bounds().Contains(screen_location)) {
      return false;
    }
  }

  auto* root_window = window_util::GetRootWindowAt(screen_location);
  auto* split_view_controller = SplitViewController::Get(root_window);
  if (!split_view_controller->InTabletSplitViewMode())
    return false;

  // Do not enable back gesture if |screen_location| is inside the extended
  // hotseat, let the hotseat handle the event instead.
  Shelf* shelf = Shelf::ForWindow(root_window);
  if (shelf->shelf_layout_manager()->hotseat_state() ==
          HotseatState::kExtended &&
      shelf->shelf_widget()
          ->hotseat_widget()
          ->GetWindowBoundsInScreen()
          .Contains(screen_location)) {
    return false;
  }

  // Do not enable back gesture if |screen_location| is inside the shelf widget,
  // let the shelf handle the event instead.
  if (shelf->shelf_widget()->GetWindowBoundsInScreen().Contains(
          screen_location)) {
    return false;
  }

  gfx::Rect divider_bounds =
      split_view_controller->split_view_divider()->GetDividerBoundsInScreen(
          /*is_dragging=*/false);
  const int y_center = divider_bounds.CenterPoint().y();
  // Do not enable back gesture if swiping starts from splitview divider's
  // resizable area.
  if (screen_location.y() >= (y_center - kDistanceForSplitViewResize) &&
      screen_location.y() <= (y_center + kDistanceForSplitViewResize)) {
    return false;
  }

  if (!base::i18n::IsRTL()) {
    divider_bounds.set_x(divider_bounds.x() - kSplitViewDividerExtraInset);
  } else {
    divider_bounds.set_x(divider_bounds.x() - kSplitViewDividerExtraInset -
                         BackGestureEventHandler::kStartGoingBackLeftEdgeInset);
  }

  divider_bounds.set_width(
      divider_bounds.width() + kSplitViewDividerExtraInset +
      BackGestureEventHandler::kStartGoingBackLeftEdgeInset);
  return divider_bounds.Contains(screen_location);
}

// Activate the given |window|.
void ActivateWindow(aura::Window* window) {
  if (!window)
    return;
  WindowState::Get(window)->Activate();
}

// Activate the snapped window that is underneath the start |location| for back
// gesture. This is necessary since the snapped window that is underneath is not
// always the current active window.
void ActivateUnderneathWindowInSplitViewMode(
    const gfx::Point& location,
    bool dragged_from_splitview_divider) {
  auto* split_view_controller =
      SplitViewController::Get(window_util::GetRootWindowAt(location));
  if (!split_view_controller->InTabletSplitViewMode())
    return;

  const bool is_rtl = base::i18n::IsRTL();
  auto* left_window = is_rtl ? split_view_controller->secondary_window()
                             : split_view_controller->primary_window();
  auto* right_window = is_rtl ? split_view_controller->primary_window()
                              : split_view_controller->secondary_window();
  const chromeos::OrientationType current_orientation =
      GetCurrentScreenOrientation();
  if (current_orientation == chromeos::OrientationType::kLandscapePrimary) {
    ActivateWindow(dragged_from_splitview_divider ? right_window : left_window);
  } else if (current_orientation ==
             chromeos::OrientationType::kLandscapeSecondary) {
    ActivateWindow(dragged_from_splitview_divider ? left_window : right_window);
  } else {
    if (left_window &&
        split_view_controller
            ->GetSnappedWindowBoundsInScreen(
                SnapPosition::kPrimary,
                /*window_for_minimum_size=*/nullptr,
                chromeos::kDefaultSnapRatio, /*account_for_divider_width=*/true)
            .Contains(location)) {
      ActivateWindow(left_window);
    } else if (right_window && split_view_controller
                                   ->GetSnappedWindowBoundsInScreen(
                                       SnapPosition::kSecondary,
                                       /*window_for_minimum_size=*/nullptr,
                                       chromeos::kDefaultSnapRatio,
                                       /*account_for_divider_width=*/true)
                                   .Contains(location)) {
      ActivateWindow(right_window);
    } else if (split_view_controller->split_view_divider()
                   ->GetDividerBoundsInScreen(
                       /*is_dragging=*/false)
                   .Contains(location)) {
      // Activate the window that above the splitview divider if back gesture
      // starts from splitview divider.
      ActivateWindow(IsCurrentScreenOrientationPrimary() ? left_window
                                                         : right_window);
    }
  }
}

}  // namespace

BackGestureEventHandler::BackGestureEventHandler() {
  if (features::IsHideShelfControlsInTabletModeEnabled()) {
    nudge_controller_ =
        std::make_unique<BackGestureContextualNudgeControllerImpl>();
  }
}

BackGestureEventHandler::~BackGestureEventHandler() = default;

void BackGestureEventHandler::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t metrics) {
  // Cancel the left edge swipe back during screen rotation.
  if (metrics & DISPLAY_METRIC_ROTATION) {
    back_gesture_affordance_.reset();
    going_back_started_ = false;
  }
}

void BackGestureEventHandler::OnGestureEvent(ui::GestureEvent* event) {
  // Do not handle gesture events that are not generated from |first_touch_id_|.
  if (base::Contains(other_touch_event_ids_list_,
                     event->unique_touch_event_id())) {
    return;
  }

  if (should_wait_for_touch_ack_) {
    aura::Window* target = static_cast<aura::Window*>(event->target());
    gfx::Point screen_location = event->location();
    ::wm::ConvertPointToScreen(target, &screen_location);
    if (MaybeHandleBackGesture(event, screen_location))
      event->StopPropagation();

    // Reset |should_wait_for_touch_ack_| for the last gesture event in the
    // sequence.
    if (event->type() == ui::EventType::kGestureEnd) {
      should_wait_for_touch_ack_ = false;
    }
  }
}

void BackGestureEventHandler::OnTouchEvent(ui::TouchEvent* event) {
  // Do not handle PEN and ERASER events for back gesture. PEN events can come
  // from stylus device.
  if (event->pointer_details().pointer_type == ui::EventPointerType::kPen ||
      event->pointer_details().pointer_type == ui::EventPointerType::kEraser) {
    return;
  }

  // Update |first_touch_id_| on first EventType::kTouchPressed only.
  // EventType::kTouchPressed type check is needed because there could be
  // EventType::kTouchCancelled after EventType::kTouchReleased event.
  if (first_touch_id_ == ui::kPointerIdUnknown &&
      event->type() == ui::EventType::kTouchPressed) {
    first_touch_id_ = event->pointer_details().id;
  }

  if (event->pointer_details().id != first_touch_id_) {
    other_touch_event_ids_list_.insert(event->unique_event_id());
    return;
  }

  if (event->type() == ui::EventType::kTouchReleased) {
    first_touch_id_ = ui::kPointerIdUnknown;
    other_touch_event_ids_list_.clear();
  }

  if (event->type() == ui::EventType::kTouchPressed) {
    x_drag_amount_ = y_drag_amount_ = 0;
    during_reverse_dragging_ = false;
  } else {
    // TODO(oshima): Convert to PointF/float.
    const gfx::Point current_location = event->location();
    x_drag_amount_ += (current_location.x() - last_touch_point_.x());
    y_drag_amount_ += (current_location.y() - last_touch_point_.y());

    // Do not update |during_reverse_dragging_| if touch point's location
    // doesn't change.
    if (current_location.x() != last_touch_point_.x()) {
      during_reverse_dragging_ = current_location.x() < last_touch_point_.x();
      if (base::i18n::IsRTL())
        during_reverse_dragging_ = !during_reverse_dragging_;
    }
  }
  last_touch_point_ = event->location();

  // Get the event target from TouchEvent since target of the GestureEvent
  // from GetAndResetPendingGestures is nullptr. The coordinate conversion is
  // done outside the loop as the previous gesture events in a sequence may
  // invalidate the target, for example given a sequence of
  // {EventType::kGestureScrollEnd, EventType::kGestureEnd} on a non-resizable
  // window, the first gesture will trigger a minimize event which will delete
  // the backdrop, which was the target. See http://crbug.com/1064618.
  aura::Window* target = static_cast<aura::Window*>(event->target());
  gfx::Point screen_location = event->location();
  ::wm::ConvertPointToScreen(target, &screen_location);

  if (event->type() == ui::EventType::kTouchPressed &&
      ShouldWaitForTouchPressAck(screen_location)) {
    should_wait_for_touch_ack_ = true;
    return;
  }

  if (!should_wait_for_touch_ack_) {
    ui::TouchEvent touch_event_copy = *event;
    if (!gesture_provider_.OnTouchEvent(&touch_event_copy))
      return;

    gesture_provider_.OnTouchEventAck(
        touch_event_copy.unique_event_id(), /*event_consumed=*/false,
        /*is_source_touch_event_set_blocking=*/false);

    std::vector<std::unique_ptr<ui::GestureEvent>> gestures =
        gesture_provider_.GetAndResetPendingGestures();
    for (const auto& gesture : gestures) {
      if (MaybeHandleBackGesture(gesture.get(), screen_location))
        event->StopPropagation();
    }
  }
}

const std::string& BackGestureEventHandler::GetName() const {
  static const std::string name("BackGestureEventHandler");
  return name;
}

void BackGestureEventHandler::OnGestureEvent(GestureConsumer* consumer,
                                             ui::GestureEvent* event) {
  // Gesture events here are generated by |gesture_provider_|, and they're
  // handled at OnTouchEvent() by calling MaybeHandleBackGesture().
}

bool BackGestureEventHandler::MaybeHandleBackGesture(
    ui::GestureEvent* event,
    const gfx::Point& screen_location) {
  switch (event->type()) {
    case ui::EventType::kGestureTapDown:
      going_back_started_ = CanStartGoingBack(screen_location);
      if (!going_back_started_)
        break;
      back_gesture_affordance_ = std::make_unique<BackGestureAffordance>(
          screen_location, dragged_from_splitview_divider_);
      if (features::IsHideShelfControlsInTabletModeEnabled()) {
        // Cancel the in-waiting or in-progress back nudge animation.
        nudge_controller_->OnBackGestureStarted();
        contextual_tooltip::HandleGesturePerformed(
            Shell::Get()->session_controller()->GetActivePrefService(),
            contextual_tooltip::TooltipType::kBackGesture);
      }
      return true;
    case ui::EventType::kGestureScrollBegin:
      if (!going_back_started_)
        break;
      back_start_location_ = screen_location;

      base::RecordAction(base::UserMetricsAction("Ash_Tablet_BackGesture"));
      back_gesture_start_scenario_type_ = GetStartScenarioType(
          dragged_from_splitview_divider_, back_start_location_);
      RecordStartScenarioType(back_gesture_start_scenario_type_);
      break;
    case ui::EventType::kGestureScrollUpdate:
      if (!going_back_started_)
        break;
      CHECK(back_gesture_affordance_);
      back_gesture_affordance_->Update(x_drag_amount_, y_drag_amount_,
                                       during_reverse_dragging_);
      return true;
    case ui::EventType::kGestureScrollEnd:
    case ui::EventType::kScrollFlingStart:
    case ui::EventType::kGestureEnd: {
      if (!going_back_started_)
        break;
      CHECK(back_gesture_affordance_);
      // Complete the back gesture if the affordance is activated or fling
      // with large enough velocity. Note, complete can be different actions
      // while in different scenarios, but always fading out the affordance at
      // the end.
      SCOPED_CRASH_KEY_BOOL("286590216", "back_gesture_affordance_1",
                            back_gesture_affordance_ != nullptr);
      SCOPED_CRASH_KEY_BOOL("286590216", "going_back_started_1",
                            going_back_started_);
      SCOPED_CRASH_KEY_NUMBER("286590216", "event.type",
                              base::to_underlying(event->type()));
      if (back_gesture_affordance_->IsActivated() ||
          (event->type() == ui::EventType::kScrollFlingStart &&
           event->details().velocity_x() >= kFlingVelocityForGoingBack)) {
        auto* shell = Shell::Get();
        if (!keyboard_util::CloseKeyboardIfActive()) {
          ActivateUnderneathWindowInSplitViewMode(
              back_start_location_, dragged_from_splitview_divider_);
          if (shell->app_list_controller()->IsHomeScreenVisible()) {
            DCHECK(shell->app_list_controller()->GetAppListViewState() ==
                   AppListViewState::kFullscreenSearch);
            // Exit home screen search and go back to home screen all apps page.
            shell->app_list_controller()->Back();
          } else {
            auto* top_window = window_util::GetTopWindow();
            auto* top_window_state = WindowState::Get(top_window);
            if (top_window_state && top_window_state->IsFullscreen() &&
                !shell->overview_controller()->InOverviewSession()) {
              // For fullscreen ARC apps, show the hotseat and shelf on the
              // first back swipe, and send a back event on the second back
              // swipe. For other fullscreen apps, exit fullscreen.
              const bool arc_app =
                  top_window_state->window()->GetProperty(
                      chromeos::kAppTypeKey) == chromeos::AppType::ARC_APP;
              if (arc_app) {
                // Go back to the previous page if the shelf was already shown,
                // otherwise record as showing shelf.
                if (Shelf::ForWindow(top_window_state->window())->IsVisible()) {
                  SendBackEvent(screen_location);
                } else {
                  Shelf::ForWindow(top_window_state->window())
                      ->shelf_layout_manager()
                      ->UpdateVisibilityStateForBackGesture();
                  RecordEndScenarioType(
                      BackGestureEndScenarioType::kShowShelfAndHotseat);
                }
              } else {
                // Complete as exiting the fullscreen mode of the underneath
                // window.
                const WMEvent wm_event(WM_EVENT_TOGGLE_FULLSCREEN);
                top_window_state->OnWMEvent(&wm_event);
                RecordEndScenarioType(
                    BackGestureEndScenarioType::kExitFullscreen);
              }
            } else if (window_util::ShouldMinimizeTopWindowOnBack()) {
              // Complete as minimizing the underneath window.
              top_window_state->Minimize();
              RecordEndScenarioType(
                  GetEndScenarioType(back_gesture_start_scenario_type_,
                                     BackGestureEndType::kMinimize));
            } else {
              // Complete as going back to the previous page of the underneath
              // window.
              SendBackEvent(screen_location);
            }
            // |top_window| could be nullptr while in overview mode since back
            // gesture is allowed in overview mode even no window opens.
            if (top_window) {
              RecordUnderneathWindowType(
                  GetUnderneathWindowType(back_gesture_start_scenario_type_));
            }
          }
        }
        SCOPED_CRASH_KEY_BOOL("286590216", "back_gesture_affordance_2",
                              back_gesture_affordance_ != nullptr);
        SCOPED_CRASH_KEY_BOOL("286590216", "going_back_started_2",
                              going_back_started_);
        // `back_gesture_affordance_` could be reset after receiving the event,
        // depends on the timing that receives the display rotation
        // notification.
        if (back_gesture_affordance_) {
          back_gesture_affordance_->Complete();
        }
      } else {
        back_gesture_affordance_->Abort();
        RecordEndScenarioType(GetEndScenarioType(
            back_gesture_start_scenario_type_, BackGestureEndType::kAbort));
      }
      going_back_started_ = false;
      dragged_from_splitview_divider_ = false;

      return true;
    }
    default:
      break;
  }

  return going_back_started_;
}

bool BackGestureEventHandler::CanStartGoingBack(
    const gfx::Point& screen_location) {
  Shell* shell = Shell::Get();
  // Do not enable back gesture in Kiosk mode, as we are never supposed to leave
  // fullscreen mode there.
  if (shell->session_controller()->IsRunningInAppMode())
    return false;

  if (!display::Screen::GetScreen()->InTabletMode()) {
    return false;
  }

  // Do not enable back gesture if it is not in an ACTIVE session. e.g, login
  // screen, lock screen.
  if (shell->session_controller()->GetSessionState() !=
      session_manager::SessionState::ACTIVE) {
    return false;
  }

  // Do not enable back gesture if `screen_location` is inside the tuck handle,
  // let `FloatController` handle the event instead.
  if (aura::Window* floated_window =
          window_util::GetFloatedWindowForActiveDesk()) {
    auto* float_controller = Shell::Get()->float_controller();
    if (float_controller->IsFloatedWindowTuckedForTablet(floated_window)) {
      auto* tuck_handle_widget =
          float_controller->GetTuckHandleWidget(floated_window);
      if (tuck_handle_widget &&
          tuck_handle_widget->GetWindowBoundsInScreen().Contains(
              screen_location)) {
        return false;
      }
    }
  }

  gfx::Rect hit_bounds_in_screen(
      display::Screen::GetScreen()
          ->GetDisplayNearestWindow(
              window_util::GetRootWindowAt(screen_location))
          .work_area());
  if (base::i18n::IsRTL()) {
    hit_bounds_in_screen.set_x(hit_bounds_in_screen.right() -
                               kStartGoingBackLeftEdgeInset);
  }

  hit_bounds_in_screen.set_width(kStartGoingBackLeftEdgeInset);
  const bool hit_in_limited_area =
      hit_bounds_in_screen.Contains(screen_location);

  const bool is_home_launcher_visible =
      shell->app_list_controller()->IsHomeScreenVisible();
  const bool is_fullscreen_search_state =
      shell->app_list_controller()->GetAppListViewState() ==
      AppListViewState::kFullscreenSearch;

  // Enable back gesture if it is in home screen |kFullScreenSearch| state and
  // swipe from the left limited area. Otherwise, always disable the back
  // gesture when it is in home screen.
  if (is_home_launcher_visible) {
    if (is_fullscreen_search_state)
      return hit_in_limited_area;
    return false;
  }

  aura::Window* top_window = window_util::GetTopWindow();
  // Do not enable back gesture if MRU window list is empty and it is not in
  // overview mode.
  if (!top_window && !shell->overview_controller()->InOverviewSession())
    return false;

  // If the event location falls into the window's gesture exclusion zone, do
  // not handle it.
  for (aura::Window* window = top_window; window; window = window->parent()) {
    SkRegion* gesture_exclusion =
        window->GetProperty(kSystemGestureExclusionKey);
    if (gesture_exclusion) {
      gfx::Point location_in_window = screen_location;
      ::wm::ConvertPointFromScreen(window, &location_in_window);
      if (gesture_exclusion->contains(location_in_window.x(),
                                      location_in_window.y())) {
        return false;
      }
    }
  }

  // If the target window does not allow touch action, do not handle it.
  if (!Shell::Get()->shell_delegate()->AllowDefaultTouchActions(top_window))
    return false;

  if (hit_in_limited_area)
    return true;

  dragged_from_splitview_divider_ =
      CanStartGoingBackFromSplitViewDivider(screen_location);
  return dragged_from_splitview_divider_;
}

void BackGestureEventHandler::SendBackEvent(const gfx::Point& screen_location) {
  window_util::SendBackKeyEvent(window_util::GetRootWindowAt(screen_location));
  RecordEndScenarioType(GetEndScenarioType(back_gesture_start_scenario_type_,
                                           BackGestureEndType::kBack));
}

bool BackGestureEventHandler::ShouldWaitForTouchPressAck(
    const gfx::Point& screen_location) {
  aura::Window* top_window = window_util::GetTopWindow();
  if (!top_window || !CanStartGoingBack(screen_location))
    return false;

  return !top_window->GetProperty(chromeos::kIsShowingInOverviewKey) &&
         Shell::Get()->shell_delegate()->ShouldWaitForTouchPressAck(top_window);
}

}  // namespace ash