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.
4
5 #include "device/hid/hid_service_mac.h"
6
7 #include <CoreFoundation/CoreFoundation.h>
8 #include <IOKit/hid/IOHIDDevice.h>
9
10 #include <set>
11 #include <string>
12 #include <vector>
13
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"
25
26 namespace device {
27
28 namespace {
29
TryGetHidIntProperty(IOHIDDeviceRef device,CFStringRef key,int32_t * result)30 bool TryGetHidIntProperty(IOHIDDeviceRef device,
31 CFStringRef key,
32 int32_t* result) {
33 CFNumberRef ref =
34 base::mac::CFCast<CFNumberRef>(IOHIDDeviceGetProperty(device, key));
35 return ref && CFNumberGetValue(ref, kCFNumberSInt32Type, result);
36 }
37
GetHidIntProperty(IOHIDDeviceRef device,CFStringRef key)38 int32_t GetHidIntProperty(IOHIDDeviceRef device, CFStringRef key) {
39 int32_t value;
40 if (TryGetHidIntProperty(device, key, &value))
41 return value;
42 return 0;
43 }
44
TryGetHidStringProperty(IOHIDDeviceRef device,CFStringRef key,std::string * result)45 bool TryGetHidStringProperty(IOHIDDeviceRef device,
46 CFStringRef key,
47 std::string* result) {
48 CFStringRef ref =
49 base::mac::CFCast<CFStringRef>(IOHIDDeviceGetProperty(device, key));
50 if (!ref) {
51 return false;
52 }
53 *result = base::SysCFStringRefToUTF8(ref);
54 return true;
55 }
56
GetHidStringProperty(IOHIDDeviceRef device,CFStringRef key)57 std::string GetHidStringProperty(IOHIDDeviceRef device, CFStringRef key) {
58 std::string value;
59 TryGetHidStringProperty(device, key, &value);
60 return value;
61 }
62
GetReportIds(IOHIDElementRef element,std::set<int> * reportIDs)63 void GetReportIds(IOHIDElementRef element, std::set<int>* reportIDs) {
64 uint32_t reportID = IOHIDElementGetReportID(element);
65 if (reportID) {
66 reportIDs->insert(reportID);
67 }
68
69 CFArrayRef children = IOHIDElementGetChildren(element);
70 if (!children) {
71 return;
72 }
73
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);
79 }
80 }
81
GetCollectionInfos(IOHIDDeviceRef device,bool * has_report_id,std::vector<HidCollectionInfo> * top_level_collections)82 void GetCollectionInfos(IOHIDDeviceRef device,
83 bool* has_report_id,
84 std::vector<HidCollectionInfo>* top_level_collections) {
85 STLClearObject(top_level_collections);
86 CFMutableDictionaryRef collections_filter =
87 CFDictionaryCreateMutable(kCFAllocatorDefault,
88 0,
89 &kCFTypeDictionaryKeyCallBacks,
90 &kCFTypeDictionaryValueCallBacks);
91 const int kCollectionTypeValue = kIOHIDElementTypeCollection;
92 CFNumberRef collection_type_id = CFNumberCreate(
93 kCFAllocatorDefault, kCFNumberIntType, &kCollectionTypeValue);
94 CFDictionarySetValue(
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 uint16_t usage = IOHIDElementGetUsage(collection);
110 collection_info.usage = HidUsageAndPage(usage, page);
111 // Explore children recursively and retrieve their report IDs
112 GetReportIds(collection, &collection_info.report_ids);
113 if (collection_info.report_ids.size() > 0) {
114 *has_report_id = true;
115 }
116 top_level_collections->push_back(collection_info);
117 }
118 }
119 }
120
PopulateDeviceInfo(io_service_t service,HidDeviceInfo * device_info)121 void PopulateDeviceInfo(io_service_t service, HidDeviceInfo* device_info) {
122 io_string_t service_path;
123 IOReturn result =
124 IORegistryEntryGetPath(service, kIOServicePlane, service_path);
125 if (result != kIOReturnSuccess) {
126 VLOG(1) << "Failed to get IOService path: "
127 << base::StringPrintf("0x%04x", result);
128 return;
129 }
130
131 base::ScopedCFTypeRef<IOHIDDeviceRef> hid_device(
132 IOHIDDeviceCreate(kCFAllocatorDefault, service));
133 device_info->device_id = service_path;
134 device_info->vendor_id =
135 GetHidIntProperty(hid_device, CFSTR(kIOHIDVendorIDKey));
136 device_info->product_id =
137 GetHidIntProperty(hid_device, CFSTR(kIOHIDProductIDKey));
138 device_info->product_name =
139 GetHidStringProperty(hid_device, CFSTR(kIOHIDProductKey));
140 device_info->serial_number =
141 GetHidStringProperty(hid_device, CFSTR(kIOHIDSerialNumberKey));
142 GetCollectionInfos(
143 hid_device, &device_info->has_report_id, &device_info->collections);
144 device_info->max_input_report_size =
145 GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxInputReportSizeKey));
146 if (device_info->has_report_id && device_info->max_input_report_size > 0) {
147 device_info->max_input_report_size--;
148 }
149 device_info->max_output_report_size =
150 GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxOutputReportSizeKey));
151 if (device_info->has_report_id && device_info->max_output_report_size > 0) {
152 device_info->max_output_report_size--;
153 }
154 device_info->max_feature_report_size =
155 GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxFeatureReportSizeKey));
156 if (device_info->has_report_id && device_info->max_feature_report_size > 0) {
157 device_info->max_feature_report_size--;
158 }
159 }
160
161 } // namespace
162
HidServiceMac(scoped_refptr<base::SingleThreadTaskRunner> file_task_runner)163 HidServiceMac::HidServiceMac(
164 scoped_refptr<base::SingleThreadTaskRunner> file_task_runner)
165 : file_task_runner_(file_task_runner) {
166 task_runner_ = base::ThreadTaskRunnerHandle::Get();
167 DCHECK(task_runner_.get());
168
169 notify_port_ = IONotificationPortCreate(kIOMasterPortDefault);
170 CFRunLoopAddSource(CFRunLoopGetMain(),
171 IONotificationPortGetRunLoopSource(notify_port_),
172 kCFRunLoopDefaultMode);
173
174 io_iterator_t iterator;
175 IOReturn result =
176 IOServiceAddMatchingNotification(notify_port_,
177 kIOFirstMatchNotification,
178 IOServiceMatching(kIOHIDDeviceKey),
179 FirstMatchCallback,
180 this,
181 &iterator);
182 if (result != kIOReturnSuccess) {
183 LOG(ERROR) << "Failed to listen for device arrival: "
184 << base::StringPrintf("0x%04x", result);
185 return;
186 }
187
188 // Drain the iterator to arm the notification.
189 devices_added_iterator_.reset(iterator);
190 AddDevices();
191 iterator = IO_OBJECT_NULL;
192
193 result = IOServiceAddMatchingNotification(notify_port_,
194 kIOTerminatedNotification,
195 IOServiceMatching(kIOHIDDeviceKey),
196 TerminatedCallback,
197 this,
198 &iterator);
199 if (result != kIOReturnSuccess) {
200 LOG(ERROR) << "Failed to listen for device removal: "
201 << base::StringPrintf("0x%04x", result);
202 return;
203 }
204
205 // Drain devices_added_iterator_ to arm the notification.
206 devices_removed_iterator_.reset(iterator);
207 RemoveDevices();
208 }
209
~HidServiceMac()210 HidServiceMac::~HidServiceMac() {
211 }
212
213 // static
FirstMatchCallback(void * context,io_iterator_t iterator)214 void HidServiceMac::FirstMatchCallback(void* context, io_iterator_t iterator) {
215 DCHECK_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent());
216 HidServiceMac* service = static_cast<HidServiceMac*>(context);
217 DCHECK_EQ(service->devices_added_iterator_, iterator);
218 service->AddDevices();
219 }
220
221 // static
TerminatedCallback(void * context,io_iterator_t iterator)222 void HidServiceMac::TerminatedCallback(void* context, io_iterator_t iterator) {
223 DCHECK_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent());
224 HidServiceMac* service = static_cast<HidServiceMac*>(context);
225 DCHECK_EQ(service->devices_removed_iterator_, iterator);
226 service->RemoveDevices();
227 }
228
AddDevices()229 void HidServiceMac::AddDevices() {
230 DCHECK(thread_checker_.CalledOnValidThread());
231
232 io_service_t device;
233 while ((device = IOIteratorNext(devices_added_iterator_)) != IO_OBJECT_NULL) {
234 HidDeviceInfo device_info;
235 PopulateDeviceInfo(device, &device_info);
236 AddDevice(device_info);
237 // The reference retained by IOIteratorNext is released below in
238 // RemoveDevices when the device is removed.
239 }
240 }
241
RemoveDevices()242 void HidServiceMac::RemoveDevices() {
243 DCHECK(thread_checker_.CalledOnValidThread());
244
245 io_service_t device;
246 while ((device = IOIteratorNext(devices_removed_iterator_)) !=
247 IO_OBJECT_NULL) {
248 io_string_t service_path;
249 IOReturn result =
250 IORegistryEntryGetPath(device, kIOServicePlane, service_path);
251 if (result == kIOReturnSuccess) {
252 RemoveDevice(service_path);
253 }
254
255 // Release reference retained by AddDevices above.
256 IOObjectRelease(device);
257 // Release the reference retained by IOIteratorNext.
258 IOObjectRelease(device);
259 }
260 }
261
Connect(const HidDeviceId & device_id)262 scoped_refptr<HidConnection> HidServiceMac::Connect(
263 const HidDeviceId& device_id) {
264 DCHECK(thread_checker_.CalledOnValidThread());
265
266 const auto& map_entry = devices().find(device_id);
267 if (map_entry == devices().end()) {
268 return NULL;
269 }
270 const HidDeviceInfo& device_info = map_entry->second;
271
272 io_string_t service_path;
273 strncpy(service_path, device_id.c_str(), sizeof service_path);
274 base::mac::ScopedIOObject<io_service_t> service(
275 IORegistryEntryFromPath(kIOMasterPortDefault, service_path));
276 if (!service.get()) {
277 VLOG(1) << "IOService not found for path: " << device_id;
278 return NULL;
279 }
280
281 base::ScopedCFTypeRef<IOHIDDeviceRef> hid_device(
282 IOHIDDeviceCreate(kCFAllocatorDefault, service));
283 IOReturn result = IOHIDDeviceOpen(hid_device, kIOHIDOptionsTypeNone);
284 if (result != kIOReturnSuccess) {
285 VLOG(1) << "Failed to open device: "
286 << base::StringPrintf("0x%04x", result);
287 return NULL;
288 }
289
290 return make_scoped_refptr(new HidConnectionMac(
291 hid_device.release(), device_info, file_task_runner_));
292 }
293
294 } // namespace device
295