1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "device/hid/hid_service_mac.h"
7 #include <CoreFoundation/CoreFoundation.h>
8 #include <IOKit/hid/IOHIDDevice.h>
14 #include "base/bind.h"
15 #include "base/location.h"
16 #include "base/logging.h"
17 #include "base/mac/foundation_util.h"
18 #include "base/single_thread_task_runner.h"
19 #include "base/stl_util.h"
20 #include "base/strings/stringprintf.h"
21 #include "base/strings/sys_string_conversions.h"
22 #include "base/thread_task_runner_handle.h"
23 #include "base/threading/thread_restrictions.h"
24 #include "device/hid/hid_connection_mac.h"
30 bool TryGetHidIntProperty(IOHIDDeviceRef device
,
34 base::mac::CFCast
<CFNumberRef
>(IOHIDDeviceGetProperty(device
, key
));
35 return ref
&& CFNumberGetValue(ref
, kCFNumberSInt32Type
, result
);
38 int32_t GetHidIntProperty(IOHIDDeviceRef device
, CFStringRef key
) {
40 if (TryGetHidIntProperty(device
, key
, &value
))
45 bool TryGetHidStringProperty(IOHIDDeviceRef device
,
47 std::string
* result
) {
49 base::mac::CFCast
<CFStringRef
>(IOHIDDeviceGetProperty(device
, key
));
53 *result
= base::SysCFStringRefToUTF8(ref
);
57 std::string
GetHidStringProperty(IOHIDDeviceRef device
, CFStringRef key
) {
59 TryGetHidStringProperty(device
, key
, &value
);
63 void GetReportIds(IOHIDElementRef element
, std::set
<int>* reportIDs
) {
64 uint32_t reportID
= IOHIDElementGetReportID(element
);
66 reportIDs
->insert(reportID
);
69 CFArrayRef children
= IOHIDElementGetChildren(element
);
74 CFIndex childrenCount
= CFArrayGetCount(children
);
75 for (CFIndex j
= 0; j
< childrenCount
; ++j
) {
76 const IOHIDElementRef child
= static_cast<IOHIDElementRef
>(
77 const_cast<void*>(CFArrayGetValueAtIndex(children
, j
)));
78 GetReportIds(child
, reportIDs
);
82 void GetCollectionInfos(IOHIDDeviceRef device
,
84 std::vector
<HidCollectionInfo
>* top_level_collections
) {
85 STLClearObject(top_level_collections
);
86 CFMutableDictionaryRef collections_filter
=
87 CFDictionaryCreateMutable(kCFAllocatorDefault
,
89 &kCFTypeDictionaryKeyCallBacks
,
90 &kCFTypeDictionaryValueCallBacks
);
91 const int kCollectionTypeValue
= kIOHIDElementTypeCollection
;
92 CFNumberRef collection_type_id
= CFNumberCreate(
93 kCFAllocatorDefault
, kCFNumberIntType
, &kCollectionTypeValue
);
95 collections_filter
, CFSTR(kIOHIDElementTypeKey
), collection_type_id
);
96 CFRelease(collection_type_id
);
97 CFArrayRef collections
= IOHIDDeviceCopyMatchingElements(
98 device
, collections_filter
, kIOHIDOptionsTypeNone
);
99 CFIndex collectionsCount
= CFArrayGetCount(collections
);
100 *has_report_id
= false;
101 for (CFIndex i
= 0; i
< collectionsCount
; i
++) {
102 const IOHIDElementRef collection
= static_cast<IOHIDElementRef
>(
103 const_cast<void*>(CFArrayGetValueAtIndex(collections
, i
)));
104 // Top-Level Collection has no parent
105 if (IOHIDElementGetParent(collection
) == 0) {
106 HidCollectionInfo collection_info
;
107 HidUsageAndPage::Page page
= static_cast<HidUsageAndPage::Page
>(
108 IOHIDElementGetUsagePage(collection
));
109 uint32_t usage
= IOHIDElementGetUsage(collection
);
110 if (usage
> std::numeric_limits
<uint16_t>::max())
112 collection_info
.usage
=
113 HidUsageAndPage(static_cast<uint16_t>(usage
), page
);
114 // Explore children recursively and retrieve their report IDs
115 GetReportIds(collection
, &collection_info
.report_ids
);
116 if (collection_info
.report_ids
.size() > 0) {
117 *has_report_id
= true;
119 top_level_collections
->push_back(collection_info
);
124 void PopulateDeviceInfo(io_service_t service
, HidDeviceInfo
* device_info
) {
125 io_string_t service_path
;
127 IORegistryEntryGetPath(service
, kIOServicePlane
, service_path
);
128 if (result
!= kIOReturnSuccess
) {
129 VLOG(1) << "Failed to get IOService path: "
130 << base::StringPrintf("0x%04x", result
);
134 base::ScopedCFTypeRef
<IOHIDDeviceRef
> hid_device(
135 IOHIDDeviceCreate(kCFAllocatorDefault
, service
));
136 device_info
->device_id
= service_path
;
137 device_info
->vendor_id
=
138 GetHidIntProperty(hid_device
, CFSTR(kIOHIDVendorIDKey
));
139 device_info
->product_id
=
140 GetHidIntProperty(hid_device
, CFSTR(kIOHIDProductIDKey
));
141 device_info
->product_name
=
142 GetHidStringProperty(hid_device
, CFSTR(kIOHIDProductKey
));
143 device_info
->serial_number
=
144 GetHidStringProperty(hid_device
, CFSTR(kIOHIDSerialNumberKey
));
146 hid_device
, &device_info
->has_report_id
, &device_info
->collections
);
147 device_info
->max_input_report_size
=
148 GetHidIntProperty(hid_device
, CFSTR(kIOHIDMaxInputReportSizeKey
));
149 if (device_info
->has_report_id
&& device_info
->max_input_report_size
> 0) {
150 device_info
->max_input_report_size
--;
152 device_info
->max_output_report_size
=
153 GetHidIntProperty(hid_device
, CFSTR(kIOHIDMaxOutputReportSizeKey
));
154 if (device_info
->has_report_id
&& device_info
->max_output_report_size
> 0) {
155 device_info
->max_output_report_size
--;
157 device_info
->max_feature_report_size
=
158 GetHidIntProperty(hid_device
, CFSTR(kIOHIDMaxFeatureReportSizeKey
));
159 if (device_info
->has_report_id
&& device_info
->max_feature_report_size
> 0) {
160 device_info
->max_feature_report_size
--;
166 HidServiceMac::HidServiceMac(
167 scoped_refptr
<base::SingleThreadTaskRunner
> file_task_runner
)
168 : file_task_runner_(file_task_runner
) {
169 task_runner_
= base::ThreadTaskRunnerHandle::Get();
170 DCHECK(task_runner_
.get());
172 notify_port_
= IONotificationPortCreate(kIOMasterPortDefault
);
173 CFRunLoopAddSource(CFRunLoopGetMain(),
174 IONotificationPortGetRunLoopSource(notify_port_
),
175 kCFRunLoopDefaultMode
);
177 io_iterator_t iterator
;
179 IOServiceAddMatchingNotification(notify_port_
,
180 kIOFirstMatchNotification
,
181 IOServiceMatching(kIOHIDDeviceKey
),
185 if (result
!= kIOReturnSuccess
) {
186 LOG(ERROR
) << "Failed to listen for device arrival: "
187 << base::StringPrintf("0x%04x", result
);
191 // Drain the iterator to arm the notification.
192 devices_added_iterator_
.reset(iterator
);
194 iterator
= IO_OBJECT_NULL
;
196 result
= IOServiceAddMatchingNotification(notify_port_
,
197 kIOTerminatedNotification
,
198 IOServiceMatching(kIOHIDDeviceKey
),
202 if (result
!= kIOReturnSuccess
) {
203 LOG(ERROR
) << "Failed to listen for device removal: "
204 << base::StringPrintf("0x%04x", result
);
208 // Drain devices_added_iterator_ to arm the notification.
209 devices_removed_iterator_
.reset(iterator
);
213 HidServiceMac::~HidServiceMac() {
217 void HidServiceMac::FirstMatchCallback(void* context
, io_iterator_t iterator
) {
218 DCHECK_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent());
219 HidServiceMac
* service
= static_cast<HidServiceMac
*>(context
);
220 DCHECK_EQ(service
->devices_added_iterator_
, iterator
);
221 service
->AddDevices();
225 void HidServiceMac::TerminatedCallback(void* context
, io_iterator_t iterator
) {
226 DCHECK_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent());
227 HidServiceMac
* service
= static_cast<HidServiceMac
*>(context
);
228 DCHECK_EQ(service
->devices_removed_iterator_
, iterator
);
229 service
->RemoveDevices();
232 void HidServiceMac::AddDevices() {
233 DCHECK(thread_checker_
.CalledOnValidThread());
236 while ((device
= IOIteratorNext(devices_added_iterator_
)) != IO_OBJECT_NULL
) {
237 HidDeviceInfo device_info
;
238 PopulateDeviceInfo(device
, &device_info
);
239 AddDevice(device_info
);
240 // The reference retained by IOIteratorNext is released below in
241 // RemoveDevices when the device is removed.
245 void HidServiceMac::RemoveDevices() {
246 DCHECK(thread_checker_
.CalledOnValidThread());
249 while ((device
= IOIteratorNext(devices_removed_iterator_
)) !=
251 io_string_t service_path
;
253 IORegistryEntryGetPath(device
, kIOServicePlane
, service_path
);
254 if (result
== kIOReturnSuccess
) {
255 RemoveDevice(service_path
);
258 // Release reference retained by AddDevices above.
259 IOObjectRelease(device
);
260 // Release the reference retained by IOIteratorNext.
261 IOObjectRelease(device
);
265 void HidServiceMac::Connect(const HidDeviceId
& device_id
,
266 const ConnectCallback
& callback
) {
267 DCHECK(thread_checker_
.CalledOnValidThread());
269 const auto& map_entry
= devices().find(device_id
);
270 if (map_entry
== devices().end()) {
271 task_runner_
->PostTask(FROM_HERE
, base::Bind(callback
, nullptr));
274 const HidDeviceInfo
& device_info
= map_entry
->second
;
276 io_string_t service_path
;
277 strncpy(service_path
, device_id
.c_str(), sizeof service_path
);
278 base::mac::ScopedIOObject
<io_service_t
> service(
279 IORegistryEntryFromPath(kIOMasterPortDefault
, service_path
));
280 if (!service
.get()) {
281 VLOG(1) << "IOService not found for path: " << device_id
;
282 task_runner_
->PostTask(FROM_HERE
, base::Bind(callback
, nullptr));
286 base::ScopedCFTypeRef
<IOHIDDeviceRef
> hid_device(
287 IOHIDDeviceCreate(kCFAllocatorDefault
, service
));
288 IOReturn result
= IOHIDDeviceOpen(hid_device
, kIOHIDOptionsTypeNone
);
289 if (result
!= kIOReturnSuccess
) {
290 VLOG(1) << "Failed to open device: "
291 << base::StringPrintf("0x%04x", result
);
292 task_runner_
->PostTask(FROM_HERE
, base::Bind(callback
, nullptr));
296 task_runner_
->PostTask(
299 make_scoped_refptr(new HidConnectionMac(
300 hid_device
.release(), device_info
, file_task_runner_
))));
303 } // namespace device