#include "ash/events/accessibility_event_rewriter.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/accessibility/magnifier/fullscreen_magnifier_controller.h"
#include "ash/accessibility/mouse_keys/mouse_keys_controller.h"
#include "ash/accessibility/switch_access/point_scan_controller.h"
#include "ash/constants/ash_constants.h"
#include "ash/keyboard/keyboard_util.h"
#include "ash/public/cpp/accessibility_event_rewriter_delegate.h"
#include "ash/shell.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/system/sys_info.h"
#include "base/task/sequenced_task_runner.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/events/ash/event_rewriter_ash.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/types/event_type.h"
namespace ash {
namespace {
ui::InputDeviceType GetInputDeviceType(
const std::string& switch_access_device_type) {
if (switch_access_device_type == kSwitchAccessInternalDevice) {
return ui::INPUT_DEVICE_INTERNAL;
} else if (switch_access_device_type == kSwitchAccessUsbDevice) {
return ui::INPUT_DEVICE_USB;
} else if (switch_access_device_type == kSwitchAccessBluetoothDevice) {
return ui::INPUT_DEVICE_BLUETOOTH;
} else {
return ui::INPUT_DEVICE_UNKNOWN;
}
}
#if !defined(NDEBUG)
void MaybeLogEventDispatchError(
const ui::Event* event,
const ui::EventRewriter::Continuation& continuation,
const ui::EventDispatchDetails& details) {
const char* failure_reason = nullptr;
if (details.dispatcher_destroyed) {
failure_reason = "destroyed dispatcher";
} else if (details.target_destroyed) {
failure_reason = "destroyed target";
} else if (continuation.WasInvalidated()) {
failure_reason = "destroyed source";
} else {
failure_reason = "no prior rewrite";
}
if (failure_reason) {
VLOG(0) << "Undispatched key " << event->AsKeyEvent()->key_code()
<< " due to " << failure_reason << ".";
}
}
#endif
}
AccessibilityEventRewriter::PendingEventInfo::PendingEventInfo(
unsigned int id,
std::unique_ptr<ui::Event> event,
ui::EventRewriter::Continuation continuation) {
this->id = id;
this->event = std::move(event);
this->continuation = std::move(continuation);
}
AccessibilityEventRewriter::PendingEventInfo::~PendingEventInfo() = default;
AccessibilityEventRewriter::AccessibilityEventRewriter(
ui::EventRewriterAsh* event_rewriter_ash,
AccessibilityEventRewriterDelegate* delegate)
: delegate_(delegate), event_rewriter_ash_(event_rewriter_ash) {
Shell::Get()->accessibility_controller()->SetAccessibilityEventRewriter(this);
observation_.Observe(input_method::InputMethodManager::Get());
}
AccessibilityEventRewriter::~AccessibilityEventRewriter() {
Shell::Get()->accessibility_controller()->SetAccessibilityEventRewriter(
nullptr);
}
void AccessibilityEventRewriter::OnUnhandledSpokenFeedbackEvent(
std::unique_ptr<ui::Event> event) const {
DCHECK(event->IsKeyEvent()) << "Unexpected unhandled event type";
if (::features::IsAccessibilityManifestV3EnabledForChromeVox()) {
return;
}
if (chromevox_continuation_) {
SendEventHelper(chromevox_continuation_, event.get());
}
}
void AccessibilityEventRewriter::ProcessPendingSpokenFeedbackEvent(
unsigned int id,
bool propagate) {
CHECK(Shell::Get()->accessibility_controller()->spoken_feedback().enabled());
CHECK(::features::IsAccessibilityManifestV3EnabledForChromeVox());
CHECK(chromevox_mv3_key_handling_enabled_);
CHECK(!pending_key_events_.empty());
const auto& pending_event_info = pending_key_events_.front();
CHECK_EQ(id, pending_event_info.id);
if (propagate) {
SendEventHelper(pending_event_info.continuation,
pending_event_info.event.get());
}
pending_key_events_.pop();
}
void AccessibilityEventRewriter::SendEventHelper(
const ui::EventRewriter::Continuation continuation,
const ui::Event* event) const {
#if !defined(NDEBUG)
ui::EventDispatchDetails details = SendEvent(continuation, event);
MaybeLogEventDispatchError(event, continuation, details);
#else
std::ignore = SendEvent(continuation, event);
#endif
}
void AccessibilityEventRewriter::SetKeyCodesForSwitchAccessCommand(
const std::map<int, std::set<std::string>>& key_codes,
SwitchAccessCommand command) {
for (auto it = key_code_to_switch_access_command_.begin();
it != key_code_to_switch_access_command_.end();) {
if (it->second == command) {
switch_access_key_codes_to_capture_.erase(it->first);
it = key_code_to_switch_access_command_.erase(it);
} else {
it++;
}
}
for (const auto& key_code : key_codes) {
switch_access_key_codes_to_capture_.erase(key_code.first);
key_code_to_switch_access_command_.erase(key_code.first);
std::set<ui::InputDeviceType> device_types;
for (const std::string& switch_access_device : key_code.second)
device_types.insert(GetInputDeviceType(switch_access_device));
switch_access_key_codes_to_capture_.insert({key_code.first, device_types});
key_code_to_switch_access_command_.insert({key_code.first, command});
}
}
void AccessibilityEventRewriter::SetSpokenFeedbackMv3KeyHandlingEnabled(
bool enabled) {
CHECK(::features::IsAccessibilityManifestV3EnabledForChromeVox());
if (chromevox_mv3_key_handling_enabled_ == enabled) {
return;
}
if (enabled) {
CHECK_EQ(0u, next_pending_event_id_);
CHECK(pending_key_events_.empty());
} else {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&AccessibilityEventRewriter::SendAllPendingSpokenFeedbackEvents,
GetWeakPtr()));
}
chromevox_mv3_key_handling_enabled_ = enabled;
}
bool AccessibilityEventRewriter::RewriteEventForChromeVox(
const ui::Event& event,
const Continuation continuation) {
chromevox_continuation_ = continuation;
if (!Shell::Get()->accessibility_controller()->spoken_feedback().enabled() ||
!event.IsKeyEvent()) {
return false;
}
if (::features::IsAccessibilityManifestV3EnabledForChromeVox() &&
!chromevox_mv3_key_handling_enabled_) {
return false;
}
const ui::KeyEvent* key_event = event.AsKeyEvent();
ui::EventRewriterAsh::MutableKeyState state(key_event);
int original_flags = state.flags;
state.flags = original_flags & ~ui::EF_COMMAND_DOWN;
event_rewriter_ash_->RewriteFunctionKeys(*key_event, &state);
state.flags = original_flags;
std::unique_ptr<ui::Event> rewritten_event;
ui::EventRewriterAsh::BuildRewrittenKeyEvent(*key_event, state,
&rewritten_event);
ui::KeyEvent* rewritten_key_event = rewritten_event.get()->AsKeyEvent();
if (try_rewriting_positional_keys_for_chromevox_) {
const ui::KeyboardCode remapped_key_code =
ui::KeycodeConverter::MapPositionalDomCodeToUSShortcutKey(
key_event->code(), key_event->key_code());
if (remapped_key_code != ui::VKEY_UNKNOWN) {
rewritten_key_event->set_key_code(remapped_key_code);
}
}
if (::features::IsAccessibilityManifestV3EnabledForChromeVox() &&
chromevox_mv3_key_handling_enabled_) {
if (pending_key_events_.size() >= kMaxPendingEvents) {
std::ostringstream errorStream;
errorStream
<< "AccessibilityEventRewriter: dropping key event due to full "
"queue"
<< rewritten_key_event->ToString();
LOG(ERROR) << errorStream.str();
static auto* const crash_key = base::debug::AllocateCrashKeyString(
"chromevox_mv3_key_events", base::debug::CrashKeySize::Size1024);
base::debug::SetCrashKeyString(crash_key, errorStream.str());
base::debug::DumpWithoutCrashing();
return true;
}
pending_key_events_.emplace(next_pending_event_id_,
rewritten_key_event->Clone(), continuation);
delegate_->DispatchKeyEventToChromeVoxMv3(next_pending_event_id_,
rewritten_key_event->Clone());
delegate_->DispatchKeyEventToChromeVox(rewritten_key_event->Clone(), true);
++next_pending_event_id_;
return true;
}
bool capture = chromevox_capture_all_keys_;
capture |= rewritten_key_event->IsCommandDown() ||
rewritten_key_event->key_code() == ui::VKEY_LWIN ||
rewritten_key_event->key_code() == ui::VKEY_RWIN;
if (rewritten_key_event->GetDomKey() == ui::DomKey::TAB) {
capture = false;
}
delegate_->DispatchKeyEventToChromeVox(rewritten_key_event->Clone(), capture);
return capture;
}
bool AccessibilityEventRewriter::RewriteEventForSwitchAccess(
const ui::Event& event,
const Continuation continuation) {
if (!event.IsKeyEvent() || suspend_switch_access_key_handling_)
return false;
const ui::KeyEvent* key_event = event.AsKeyEvent();
ui::EventRewriterAsh::MutableKeyState state(key_event);
event_rewriter_ash_->RewriteFunctionKeys(*key_event, &state);
std::unique_ptr<ui::Event> rewritten_event;
ui::EventRewriterAsh::BuildRewrittenKeyEvent(*key_event, state,
&rewritten_event);
ui::KeyEvent* rewritten_key_event = rewritten_event.get()->AsKeyEvent();
const auto& key =
switch_access_key_codes_to_capture_.find(rewritten_key_event->key_code());
if (key == switch_access_key_codes_to_capture_.end())
return false;
int source_device_id = key_event->source_device_id();
ui::InputDeviceType keyboard_type = ui::INPUT_DEVICE_UNKNOWN;
for (const auto& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (source_device_id == keyboard.id) {
keyboard_type = keyboard.type;
break;
}
}
if (source_device_id != ui::ED_UNKNOWN_DEVICE &&
key->second.count(keyboard_type) == 0) {
return false;
}
if (key_event->type() == ui::EventType::kKeyPressed) {
AccessibilityController* accessibility_controller =
Shell::Get()->accessibility_controller();
if (accessibility_controller->IsPointScanEnabled()) {
PointScanController* point_scan_controller =
accessibility_controller->GetPointScanController();
std::optional<gfx::PointF> point = point_scan_controller->OnPointSelect();
if (point.has_value()) {
delegate_->SendPointScanPoint(point.value());
}
} else {
SwitchAccessCommand command =
key_code_to_switch_access_command_[rewritten_key_event->key_code()];
delegate_->SendSwitchAccessCommand(command);
}
}
return true;
}
bool AccessibilityEventRewriter::RewriteEventForMagnifier(
const ui::Event& event,
const Continuation continuation) {
if (!event.IsKeyEvent())
return false;
const ui::KeyEvent* key_event = event.AsKeyEvent();
if (!keyboard_util::IsArrowKeyCode(key_event->key_code()) ||
!key_event->IsControlDown() || !key_event->IsAltDown()) {
return false;
}
if (key_event->type() == ui::EventType::kKeyPressed) {
if (!(key_event->flags() & ui::EF_IS_REPEAT))
OnMagnifierKeyPressed(key_event);
return true;
}
if (key_event->type() == ui::EventType::kKeyReleased) {
OnMagnifierKeyReleased(key_event);
return true;
}
return false;
}
void AccessibilityEventRewriter::OnMagnifierKeyPressed(
const ui::KeyEvent* event) {
FullscreenMagnifierController* controller =
Shell::Get()->fullscreen_magnifier_controller();
switch (event->key_code()) {
case ui::VKEY_UP:
controller->SetScrollDirection(FullscreenMagnifierController::SCROLL_UP);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveUp);
break;
case ui::VKEY_DOWN:
controller->SetScrollDirection(
FullscreenMagnifierController::SCROLL_DOWN);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveDown);
break;
case ui::VKEY_LEFT:
controller->SetScrollDirection(
FullscreenMagnifierController::SCROLL_LEFT);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveLeft);
break;
case ui::VKEY_RIGHT:
controller->SetScrollDirection(
FullscreenMagnifierController::SCROLL_RIGHT);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveRight);
break;
default:
NOTREACHED() << "Unexpected keyboard_code:" << event->key_code();
}
}
void AccessibilityEventRewriter::OnMagnifierKeyReleased(
const ui::KeyEvent* event) {
FullscreenMagnifierController* controller =
Shell::Get()->fullscreen_magnifier_controller();
controller->SetScrollDirection(FullscreenMagnifierController::SCROLL_NONE);
delegate_->SendMagnifierCommand(MagnifierCommand::kMoveStop);
}
void AccessibilityEventRewriter::MaybeSendMouseEvent(const ui::Event& event) {
AccessibilityController* accessibility_controller =
Shell::Get()->accessibility_controller();
if (send_mouse_events_ &&
(event.type() == ui::EventType::kMouseMoved ||
event.type() == ui::EventType::kMouseDragged) &&
(accessibility_controller->fullscreen_magnifier().enabled() ||
accessibility_controller->docked_magnifier().enabled() ||
accessibility_controller->spoken_feedback().enabled() ||
accessibility_controller->face_gaze().enabled())) {
delegate_->DispatchMouseEvent(event.Clone());
}
}
ui::EventDispatchDetails AccessibilityEventRewriter::RewriteEvent(
const ui::Event& event,
const Continuation continuation) {
bool captured = false;
if (!delegate_)
return SendEvent(continuation, &event);
if (::features::IsAccessibilityMouseKeysEnabled()) {
captured = Shell::Get()->mouse_keys_controller()->RewriteEvent(event);
}
if (!captured &&
Shell::Get()->accessibility_controller()->IsSwitchAccessRunning()) {
captured = RewriteEventForSwitchAccess(event, continuation);
}
if (!captured && Shell::Get()
->accessibility_controller()
->fullscreen_magnifier()
.enabled()) {
captured = RewriteEventForMagnifier(event, continuation);
}
if (!captured) {
captured = RewriteEventForChromeVox(event, continuation);
}
if (!captured) {
MaybeSendMouseEvent(event);
}
return captured ? DiscardEvent(continuation)
: SendEvent(continuation, &event);
}
void AccessibilityEventRewriter::InputMethodChanged(
input_method::InputMethodManager* manager,
Profile* profile,
bool show_message) {
try_rewriting_positional_keys_for_chromevox_ =
manager->ArePositionalShortcutsUsedByCurrentInputMethod();
}
void AccessibilityEventRewriter::SendAllPendingSpokenFeedbackEvents() {
while (!pending_key_events_.empty()) {
const auto& pending_event_info = pending_key_events_.front();
SendEventHelper(pending_event_info.continuation,
pending_event_info.event.get());
pending_key_events_.pop();
}
next_pending_event_id_ = 0;
}
}