blob: 9b037b4b3683315b410b80bf583b79048f47b8be [file] [log] [blame]
// Copyright (c) 2014 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 "content/browser/accessibility/accessibility_event_recorder.h"
#import <Cocoa/Cocoa.h>
#include <string>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
namespace content {
// Implementation of AccessibilityEventRecorder that uses AXObserver to
// watch for NSAccessibility events.
class AccessibilityEventRecorderMac : public AccessibilityEventRecorder {
public:
AccessibilityEventRecorderMac(BrowserAccessibilityManager* manager,
base::ProcessId pid);
~AccessibilityEventRecorderMac() override;
// Callback executed every time we receive an event notification.
void EventReceived(AXUIElementRef element, CFStringRef notification);
private:
// Add one notification to the list of notifications monitored by our
// observer.
void AddNotification(NSString* notification);
// Convenience function to get the value of an AX attribute from
// an AXUIElementRef as a string.
std::string GetAXAttributeValue(
AXUIElementRef element, NSString* attribute_name);
// The AXUIElement for the Chrome application.
base::ScopedCFTypeRef<AXUIElementRef> application_;
// The AXObserver we use to monitor AX notifications.
base::ScopedCFTypeRef<AXObserverRef> observer_ref_;
CFRunLoopSourceRef observer_run_loop_source_;
DISALLOW_COPY_AND_ASSIGN(AccessibilityEventRecorderMac);
};
// Callback function registered using AXObserverCreate.
static void EventReceivedThunk(
AXObserverRef observer_ref,
AXUIElementRef element,
CFStringRef notification,
void *refcon) {
AccessibilityEventRecorderMac* this_ptr =
static_cast<AccessibilityEventRecorderMac*>(refcon);
this_ptr->EventReceived(element, notification);
}
// static
std::unique_ptr<AccessibilityEventRecorder> AccessibilityEventRecorder::Create(
BrowserAccessibilityManager* manager,
base::ProcessId pid,
const base::StringPiece& application_name_match_pattern) {
if (!application_name_match_pattern.empty()) {
LOG(ERROR) << "Recording accessibility events from an application name "
"match pattern not supported on this platform yet.";
NOTREACHED();
}
return std::make_unique<AccessibilityEventRecorderMac>(manager, pid);
}
AccessibilityEventRecorderMac::AccessibilityEventRecorderMac(
BrowserAccessibilityManager* manager,
base::ProcessId pid)
: AccessibilityEventRecorder(manager), observer_run_loop_source_(NULL) {
if (kAXErrorSuccess != AXObserverCreate(pid, EventReceivedThunk,
observer_ref_.InitializeInto())) {
LOG(FATAL) << "Failed to create AXObserverRef";
}
// Get an AXUIElement for the Chrome application.
application_.reset(AXUIElementCreateApplication(pid));
if (!application_.get())
LOG(FATAL) << "Failed to create AXUIElement for application.";
// Add the notifications we care about to the observer.
AddNotification(@"AXAutocorrectionOccurred");
AddNotification(@"AXExpandedChanged");
AddNotification(@"AXInvalidStatusChanged");
AddNotification(@"AXLiveRegionChanged");
AddNotification(@"AXLiveRegionCreated");
AddNotification(@"AXLoadComplete");
AddNotification(@"AXMenuItemSelected");
AddNotification(@"AXRowCollapsed");
AddNotification(@"AXRowExpanded");
AddNotification(NSAccessibilityFocusedUIElementChangedNotification);
AddNotification(NSAccessibilityRowCollapsedNotification);
AddNotification(NSAccessibilityRowCountChangedNotification);
AddNotification(NSAccessibilitySelectedChildrenChangedNotification);
AddNotification(NSAccessibilitySelectedRowsChangedNotification);
AddNotification(NSAccessibilitySelectedTextChangedNotification);
AddNotification(NSAccessibilityValueChangedNotification);
// Add the observer to the current message loop.
observer_run_loop_source_ = AXObserverGetRunLoopSource(observer_ref_.get());
CFRunLoopAddSource(CFRunLoopGetCurrent(), observer_run_loop_source_,
kCFRunLoopDefaultMode);
}
AccessibilityEventRecorderMac::~AccessibilityEventRecorderMac() {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), observer_run_loop_source_,
kCFRunLoopDefaultMode);
}
void AccessibilityEventRecorderMac::AddNotification(NSString* notification) {
AXObserverAddNotification(observer_ref_, application_,
base::mac::NSToCFCast(notification), this);
}
std::string AccessibilityEventRecorderMac::GetAXAttributeValue(
AXUIElementRef element,
NSString* attribute_name) {
base::ScopedCFTypeRef<CFTypeRef> value;
AXError err = AXUIElementCopyAttributeValue(
element, base::mac::NSToCFCast(attribute_name), value.InitializeInto());
if (err != kAXErrorSuccess)
return std::string();
CFStringRef value_string = base::mac::CFCast<CFStringRef>(value.get());
if (value_string)
return base::SysCFStringRefToUTF8(value_string);
// TODO(dmazzoni): And if it's not a string, can we return something better?
return {};
}
void AccessibilityEventRecorderMac::EventReceived(AXUIElementRef element,
CFStringRef notification) {
std::string notification_str = base::SysCFStringRefToUTF8(notification);
std::string role = GetAXAttributeValue(element, NSAccessibilityRoleAttribute);
if (role.empty())
return;
std::string log =
base::StringPrintf("%s on %s", notification_str.c_str(), role.c_str());
std::string title =
GetAXAttributeValue(element, NSAccessibilityTitleAttribute);
if (!title.empty())
log += base::StringPrintf(" AXTitle=\"%s\"", title.c_str());
std::string description =
GetAXAttributeValue(element, NSAccessibilityDescriptionAttribute);
if (!description.empty())
log += base::StringPrintf(" AXDescription=\"%s\"", description.c_str());
std::string value =
GetAXAttributeValue(element, NSAccessibilityValueAttribute);
if (!value.empty())
log += base::StringPrintf(" AXValue=\"%s\"", value.c_str());
OnEvent(log);
}
} // namespace content