blob: 6e091a851e7da88fb94950558f82520755e3591e [file] [log] [blame]
// Copyright 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 "device/hid/hid_service_mac.h"
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/hid/IOHIDDevice.h>
#include <set>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/thread_task_runner_handle.h"
#include "base/threading/thread_restrictions.h"
#include "device/hid/hid_connection_mac.h"
namespace device {
namespace {
bool TryGetHidIntProperty(IOHIDDeviceRef device,
CFStringRef key,
int32_t* result) {
CFNumberRef ref =
base::mac::CFCast<CFNumberRef>(IOHIDDeviceGetProperty(device, key));
return ref && CFNumberGetValue(ref, kCFNumberSInt32Type, result);
}
int32_t GetHidIntProperty(IOHIDDeviceRef device, CFStringRef key) {
int32_t value;
if (TryGetHidIntProperty(device, key, &value))
return value;
return 0;
}
bool TryGetHidStringProperty(IOHIDDeviceRef device,
CFStringRef key,
std::string* result) {
CFStringRef ref =
base::mac::CFCast<CFStringRef>(IOHIDDeviceGetProperty(device, key));
if (!ref) {
return false;
}
*result = base::SysCFStringRefToUTF8(ref);
return true;
}
std::string GetHidStringProperty(IOHIDDeviceRef device, CFStringRef key) {
std::string value;
TryGetHidStringProperty(device, key, &value);
return value;
}
void GetReportIds(IOHIDElementRef element, std::set<int>* reportIDs) {
uint32_t reportID = IOHIDElementGetReportID(element);
if (reportID) {
reportIDs->insert(reportID);
}
CFArrayRef children = IOHIDElementGetChildren(element);
if (!children) {
return;
}
CFIndex childrenCount = CFArrayGetCount(children);
for (CFIndex j = 0; j < childrenCount; ++j) {
const IOHIDElementRef child = static_cast<IOHIDElementRef>(
const_cast<void*>(CFArrayGetValueAtIndex(children, j)));
GetReportIds(child, reportIDs);
}
}
void GetCollectionInfos(IOHIDDeviceRef device,
bool* has_report_id,
std::vector<HidCollectionInfo>* top_level_collections) {
STLClearObject(top_level_collections);
CFMutableDictionaryRef collections_filter =
CFDictionaryCreateMutable(kCFAllocatorDefault,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
const int kCollectionTypeValue = kIOHIDElementTypeCollection;
CFNumberRef collection_type_id = CFNumberCreate(
kCFAllocatorDefault, kCFNumberIntType, &kCollectionTypeValue);
CFDictionarySetValue(
collections_filter, CFSTR(kIOHIDElementTypeKey), collection_type_id);
CFRelease(collection_type_id);
CFArrayRef collections = IOHIDDeviceCopyMatchingElements(
device, collections_filter, kIOHIDOptionsTypeNone);
CFIndex collectionsCount = CFArrayGetCount(collections);
*has_report_id = false;
for (CFIndex i = 0; i < collectionsCount; i++) {
const IOHIDElementRef collection = static_cast<IOHIDElementRef>(
const_cast<void*>(CFArrayGetValueAtIndex(collections, i)));
// Top-Level Collection has no parent
if (IOHIDElementGetParent(collection) == 0) {
HidCollectionInfo collection_info;
HidUsageAndPage::Page page = static_cast<HidUsageAndPage::Page>(
IOHIDElementGetUsagePage(collection));
uint32_t usage = IOHIDElementGetUsage(collection);
if (usage > std::numeric_limits<uint16_t>::max())
continue;
collection_info.usage =
HidUsageAndPage(static_cast<uint16_t>(usage), page);
// Explore children recursively and retrieve their report IDs
GetReportIds(collection, &collection_info.report_ids);
if (collection_info.report_ids.size() > 0) {
*has_report_id = true;
}
top_level_collections->push_back(collection_info);
}
}
}
void PopulateDeviceInfo(io_service_t service, HidDeviceInfo* device_info) {
io_string_t service_path;
IOReturn result =
IORegistryEntryGetPath(service, kIOServicePlane, service_path);
if (result != kIOReturnSuccess) {
VLOG(1) << "Failed to get IOService path: "
<< base::StringPrintf("0x%04x", result);
return;
}
base::ScopedCFTypeRef<IOHIDDeviceRef> hid_device(
IOHIDDeviceCreate(kCFAllocatorDefault, service));
device_info->device_id = service_path;
device_info->vendor_id =
GetHidIntProperty(hid_device, CFSTR(kIOHIDVendorIDKey));
device_info->product_id =
GetHidIntProperty(hid_device, CFSTR(kIOHIDProductIDKey));
device_info->product_name =
GetHidStringProperty(hid_device, CFSTR(kIOHIDProductKey));
device_info->serial_number =
GetHidStringProperty(hid_device, CFSTR(kIOHIDSerialNumberKey));
GetCollectionInfos(
hid_device, &device_info->has_report_id, &device_info->collections);
device_info->max_input_report_size =
GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxInputReportSizeKey));
if (device_info->has_report_id && device_info->max_input_report_size > 0) {
device_info->max_input_report_size--;
}
device_info->max_output_report_size =
GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxOutputReportSizeKey));
if (device_info->has_report_id && device_info->max_output_report_size > 0) {
device_info->max_output_report_size--;
}
device_info->max_feature_report_size =
GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxFeatureReportSizeKey));
if (device_info->has_report_id && device_info->max_feature_report_size > 0) {
device_info->max_feature_report_size--;
}
}
} // namespace
HidServiceMac::HidServiceMac(
scoped_refptr<base::SingleThreadTaskRunner> file_task_runner)
: file_task_runner_(file_task_runner) {
task_runner_ = base::ThreadTaskRunnerHandle::Get();
DCHECK(task_runner_.get());
notify_port_ = IONotificationPortCreate(kIOMasterPortDefault);
CFRunLoopAddSource(CFRunLoopGetMain(),
IONotificationPortGetRunLoopSource(notify_port_),
kCFRunLoopDefaultMode);
io_iterator_t iterator;
IOReturn result =
IOServiceAddMatchingNotification(notify_port_,
kIOFirstMatchNotification,
IOServiceMatching(kIOHIDDeviceKey),
FirstMatchCallback,
this,
&iterator);
if (result != kIOReturnSuccess) {
LOG(ERROR) << "Failed to listen for device arrival: "
<< base::StringPrintf("0x%04x", result);
return;
}
// Drain the iterator to arm the notification.
devices_added_iterator_.reset(iterator);
AddDevices();
iterator = IO_OBJECT_NULL;
result = IOServiceAddMatchingNotification(notify_port_,
kIOTerminatedNotification,
IOServiceMatching(kIOHIDDeviceKey),
TerminatedCallback,
this,
&iterator);
if (result != kIOReturnSuccess) {
LOG(ERROR) << "Failed to listen for device removal: "
<< base::StringPrintf("0x%04x", result);
return;
}
// Drain devices_added_iterator_ to arm the notification.
devices_removed_iterator_.reset(iterator);
RemoveDevices();
}
HidServiceMac::~HidServiceMac() {
}
// static
void HidServiceMac::FirstMatchCallback(void* context, io_iterator_t iterator) {
DCHECK_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent());
HidServiceMac* service = static_cast<HidServiceMac*>(context);
DCHECK_EQ(service->devices_added_iterator_, iterator);
service->AddDevices();
}
// static
void HidServiceMac::TerminatedCallback(void* context, io_iterator_t iterator) {
DCHECK_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent());
HidServiceMac* service = static_cast<HidServiceMac*>(context);
DCHECK_EQ(service->devices_removed_iterator_, iterator);
service->RemoveDevices();
}
void HidServiceMac::AddDevices() {
DCHECK(thread_checker_.CalledOnValidThread());
io_service_t device;
while ((device = IOIteratorNext(devices_added_iterator_)) != IO_OBJECT_NULL) {
HidDeviceInfo device_info;
PopulateDeviceInfo(device, &device_info);
AddDevice(device_info);
// The reference retained by IOIteratorNext is released below in
// RemoveDevices when the device is removed.
}
}
void HidServiceMac::RemoveDevices() {
DCHECK(thread_checker_.CalledOnValidThread());
io_service_t device;
while ((device = IOIteratorNext(devices_removed_iterator_)) !=
IO_OBJECT_NULL) {
io_string_t service_path;
IOReturn result =
IORegistryEntryGetPath(device, kIOServicePlane, service_path);
if (result == kIOReturnSuccess) {
RemoveDevice(service_path);
}
// Release reference retained by AddDevices above.
IOObjectRelease(device);
// Release the reference retained by IOIteratorNext.
IOObjectRelease(device);
}
}
void HidServiceMac::Connect(const HidDeviceId& device_id,
const ConnectCallback& callback) {
DCHECK(thread_checker_.CalledOnValidThread());
const auto& map_entry = devices().find(device_id);
if (map_entry == devices().end()) {
task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr));
return;
}
const HidDeviceInfo& device_info = map_entry->second;
io_string_t service_path;
strncpy(service_path, device_id.c_str(), sizeof service_path);
base::mac::ScopedIOObject<io_service_t> service(
IORegistryEntryFromPath(kIOMasterPortDefault, service_path));
if (!service.get()) {
VLOG(1) << "IOService not found for path: " << device_id;
task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr));
return;
}
base::ScopedCFTypeRef<IOHIDDeviceRef> hid_device(
IOHIDDeviceCreate(kCFAllocatorDefault, service));
IOReturn result = IOHIDDeviceOpen(hid_device, kIOHIDOptionsTypeNone);
if (result != kIOReturnSuccess) {
VLOG(1) << "Failed to open device: "
<< base::StringPrintf("0x%04x", result);
task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr));
return;
}
task_runner_->PostTask(
FROM_HERE,
base::Bind(callback,
make_scoped_refptr(new HidConnectionMac(
hid_device.release(), device_info, file_task_runner_))));
}
} // namespace device