blob: c30d10ce363fe107972ccb49fe015feba519330f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/crosapi/test_controller_ash.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_observer.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "base/task/post_task.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/ash/crosapi/window_util.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/user_activity/user_activity_detector.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_source.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
namespace crosapi {
namespace {
// Returns whether the dispatcher or target was destroyed.
bool Dispatch(aura::WindowTreeHost* host, ui::Event* event) {
ui::EventDispatchDetails dispatch_details =
host->GetEventSource()->SendEventToSink(event);
return dispatch_details.dispatcher_destroyed ||
dispatch_details.target_destroyed;
}
// Returns whether the dispatcher or target was destroyed.
bool DispatchMouseEvent(aura::Window* window, ui::EventType type) {
const gfx::Point center = window->bounds().CenterPoint();
ui::MouseEvent press(type, center, center, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON);
return Dispatch(window->GetHost(), &press);
}
// Enables or disables tablet mode and waits for the transition to finish.
void SetTabletModeEnabled(bool enabled) {
// This does not use ShellTestApi or TabletModeControllerTestApi because those
// are implemented in test-only files.
ash::TabletMode::Waiter waiter(enabled);
ash::Shell::Get()->tablet_mode_controller()->SetEnabledForTest(enabled);
waiter.Wait();
}
} // namespace
TestControllerAsh::TestControllerAsh() = default;
TestControllerAsh::~TestControllerAsh() = default;
void TestControllerAsh::BindReceiver(
mojo::PendingReceiver<mojom::TestController> receiver) {
// This interface is not available on production devices. It's only needed for
// tests that run on Linux-chrome so no reason to expose it.
#if BUILDFLAG(IS_CHROMEOS_DEVICE)
LOG(ERROR) << "Ash does not support TestController on devices";
#else
receivers_.Add(this, std::move(receiver));
#endif
}
void TestControllerAsh::DoesWindowExist(const std::string& window_id,
DoesWindowExistCallback callback) {
aura::Window* window = GetShellSurfaceWindow(window_id);
std::move(callback).Run(window != nullptr);
}
void TestControllerAsh::ClickWindow(const std::string& window_id) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window)
return;
bool destroyed = DispatchMouseEvent(window, ui::ET_MOUSE_PRESSED);
if (!destroyed) {
DispatchMouseEvent(window, ui::ET_MOUSE_RELEASED);
}
}
void TestControllerAsh::EnterOverviewMode(EnterOverviewModeCallback callback) {
overview_waiters_.push_back(std::make_unique<OverviewWaiter>(
/*wait_for_enter=*/true, std::move(callback), this));
ash::Shell::Get()->overview_controller()->StartOverview(
ash::OverviewStartAction::kTests);
}
void TestControllerAsh::ExitOverviewMode(ExitOverviewModeCallback callback) {
overview_waiters_.push_back(std::make_unique<OverviewWaiter>(
/*wait_for_enter=*/false, std::move(callback), this));
ash::Shell::Get()->overview_controller()->EndOverview(
ash::OverviewEndAction::kTests);
}
void TestControllerAsh::EnterTabletMode(EnterTabletModeCallback callback) {
SetTabletModeEnabled(true);
std::move(callback).Run();
}
void TestControllerAsh::ExitTabletMode(ExitTabletModeCallback callback) {
SetTabletModeEnabled(false);
std::move(callback).Run();
}
void TestControllerAsh::SendTouchEvent(const std::string& window_id,
mojom::TouchEventType type,
uint8_t pointer_id,
const gfx::PointF& location_in_window,
SendTouchEventCallback cb) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window) {
std::move(cb).Run();
return;
}
// Newer lacros might send an enum we don't know about.
if (!mojom::IsKnownEnumValue(type)) {
LOG(WARNING) << "Unknown event type: " << type;
std::move(cb).Run();
return;
}
ui::EventType event_type;
switch (type) {
case mojom::TouchEventType::kUnknown:
// |type| is not optional, so kUnknown is never expected.
NOTREACHED();
return;
case mojom::TouchEventType::kPressed:
event_type = ui::ET_TOUCH_PRESSED;
break;
case mojom::TouchEventType::kMoved:
event_type = ui::ET_TOUCH_MOVED;
break;
case mojom::TouchEventType::kReleased:
event_type = ui::ET_TOUCH_RELEASED;
break;
case mojom::TouchEventType::kCancelled:
event_type = ui::ET_TOUCH_CANCELLED;
break;
}
// Compute location relative to display root window.
gfx::PointF location_in_root(location_in_window);
aura::Window::ConvertPointToTarget(window, window->GetRootWindow(),
&location_in_root);
ui::PointerDetails details(ui::EventPointerType::kTouch, pointer_id, 1.0f,
1.0f, 0.0f);
ui::TouchEvent touch_event(event_type, location_in_window, location_in_root,
ui::EventTimeForNow(), details);
Dispatch(window->GetHost(), &touch_event);
std::move(cb).Run();
}
void TestControllerAsh::GetWindowPositionInScreen(
const std::string& window_id,
GetWindowPositionInScreenCallback cb) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window) {
std::move(cb).Run(absl::nullopt);
return;
}
std::move(cb).Run(window->GetBoundsInScreen().origin());
}
void TestControllerAsh::GetMinimizeOnBackKeyWindowProperty(
const std::string& window_id,
GetMinimizeOnBackKeyWindowPropertyCallback cb) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window) {
std::move(cb).Run(mojom::OptionalBoolean::kUnknown);
return;
}
bool* value = window->GetProperty(ash::kMinimizeOnBackKey);
if (!value) {
std::move(cb).Run(mojom::OptionalBoolean::kUnknown);
return;
}
std::move(cb).Run(*value ? mojom::OptionalBoolean::kTrue
: mojom::OptionalBoolean::kFalse);
}
void TestControllerAsh::WaiterFinished(OverviewWaiter* waiter) {
for (size_t i = 0; i < overview_waiters_.size(); ++i) {
if (waiter == overview_waiters_[i].get()) {
std::unique_ptr<OverviewWaiter> waiter = std::move(overview_waiters_[i]);
overview_waiters_.erase(overview_waiters_.begin() + i);
// Delete asynchronously to avoid re-entrancy. This is safe because the
// class will never use |test_controller_| after this callback.
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE,
std::move(waiter));
break;
}
}
}
// This class waits for overview mode to either enter or exit and fires a
// callback. This class will fire the callback at most once.
class TestControllerAsh::OverviewWaiter : public ash::OverviewObserver {
public:
OverviewWaiter(bool wait_for_enter,
base::OnceClosure closure,
TestControllerAsh* test_controller)
: wait_for_enter_(wait_for_enter),
closure_(std::move(closure)),
test_controller_(test_controller) {
ash::Shell::Get()->overview_controller()->AddObserver(this);
}
OverviewWaiter(const OverviewWaiter&) = delete;
OverviewWaiter& operator=(const OverviewWaiter&) = delete;
~OverviewWaiter() override {
ash::Shell::Get()->overview_controller()->RemoveObserver(this);
}
// OverviewObserver:
void OnOverviewModeStartingAnimationComplete(bool canceled) override {
if (wait_for_enter_) {
if (closure_) {
std::move(closure_).Run();
DCHECK(test_controller_);
TestControllerAsh* controller = test_controller_;
test_controller_ = nullptr;
controller->WaiterFinished(this);
}
}
}
void OnOverviewModeEndingAnimationComplete(bool canceled) override {
if (!wait_for_enter_) {
if (closure_) {
std::move(closure_).Run();
DCHECK(test_controller_);
TestControllerAsh* controller = test_controller_;
test_controller_ = nullptr;
controller->WaiterFinished(this);
}
}
}
private:
// If true, waits for enter. Otherwise waits for exit.
const bool wait_for_enter_;
base::OnceClosure closure_;
// The test controller owns this object so is never invalid.
TestControllerAsh* test_controller_;
};
} // namespace crosapi