Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / browser / gamepad / gamepad_platform_data_fetcher_mac.mm
blob28c3c5ddf58f125c6dc2e29cabd32d956634e992
1 // Copyright (c) 2012 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 "content/browser/gamepad/gamepad_platform_data_fetcher_mac.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/mac/scoped_nsobject.h"
9 #include "base/strings/string16.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "base/time/time.h"
14 #import <Foundation/Foundation.h>
15 #include <IOKit/hid/IOHIDKeys.h>
17 using blink::WebGamepad;
18 using blink::WebGamepads;
20 namespace content {
22 namespace {
24 void CopyNSStringAsUTF16LittleEndian(
25     NSString* src, blink::WebUChar* dest, size_t dest_len) {
26   NSData* as16 = [src dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
27   memset(dest, 0, dest_len);
28   [as16 getBytes:dest length:dest_len - sizeof(blink::WebUChar)];
31 NSDictionary* DeviceMatching(uint32_t usage_page, uint32_t usage) {
32   return [NSDictionary dictionaryWithObjectsAndKeys:
33       [NSNumber numberWithUnsignedInt:usage_page],
34           base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsagePageKey)),
35       [NSNumber numberWithUnsignedInt:usage],
36           base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsageKey)),
37       nil];
40 float NormalizeAxis(CFIndex value, CFIndex min, CFIndex max) {
41   return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
44 // http://www.usb.org/developers/hidpage
45 const uint32_t kGenericDesktopUsagePage = 0x01;
46 const uint32_t kGameControlsUsagePage = 0x05;
47 const uint32_t kButtonUsagePage = 0x09;
48 const uint32_t kJoystickUsageNumber = 0x04;
49 const uint32_t kGameUsageNumber = 0x05;
50 const uint32_t kMultiAxisUsageNumber = 0x08;
51 const uint32_t kAxisMinimumUsageNumber = 0x30;
53 }  // namespace
55 GamepadPlatformDataFetcherMac::GamepadPlatformDataFetcherMac()
56     : enabled_(true) {
57   memset(associated_, 0, sizeof(associated_));
59   xbox_fetcher_.reset(new XboxDataFetcher(this));
60   if (!xbox_fetcher_->RegisterForNotifications())
61     xbox_fetcher_.reset();
63   hid_manager_ref_.reset(IOHIDManagerCreate(kCFAllocatorDefault,
64                                             kIOHIDOptionsTypeNone));
65   if (CFGetTypeID(hid_manager_ref_) != IOHIDManagerGetTypeID()) {
66     enabled_ = false;
67     return;
68   }
70   base::scoped_nsobject<NSArray> criteria([[NSArray alloc] initWithObjects:
71       DeviceMatching(kGenericDesktopUsagePage, kJoystickUsageNumber),
72       DeviceMatching(kGenericDesktopUsagePage, kGameUsageNumber),
73       DeviceMatching(kGenericDesktopUsagePage, kMultiAxisUsageNumber),
74       nil]);
75   IOHIDManagerSetDeviceMatchingMultiple(
76       hid_manager_ref_,
77       base::mac::NSToCFCast(criteria));
79   RegisterForNotifications();
82 void GamepadPlatformDataFetcherMac::RegisterForNotifications() {
83   // Register for plug/unplug notifications.
84   IOHIDManagerRegisterDeviceMatchingCallback(
85       hid_manager_ref_,
86       &DeviceAddCallback,
87       this);
88   IOHIDManagerRegisterDeviceRemovalCallback(
89       hid_manager_ref_,
90       DeviceRemoveCallback,
91       this);
93   // Register for value change notifications.
94   IOHIDManagerRegisterInputValueCallback(
95       hid_manager_ref_,
96       ValueChangedCallback,
97       this);
99   IOHIDManagerScheduleWithRunLoop(
100       hid_manager_ref_,
101       CFRunLoopGetMain(),
102       kCFRunLoopDefaultMode);
104   enabled_ = IOHIDManagerOpen(hid_manager_ref_,
105                               kIOHIDOptionsTypeNone) == kIOReturnSuccess;
107   if (xbox_fetcher_)
108     xbox_fetcher_->RegisterForNotifications();
111 void GamepadPlatformDataFetcherMac::UnregisterFromNotifications() {
112   IOHIDManagerUnscheduleFromRunLoop(
113       hid_manager_ref_,
114       CFRunLoopGetCurrent(),
115       kCFRunLoopDefaultMode);
116   IOHIDManagerClose(hid_manager_ref_, kIOHIDOptionsTypeNone);
117   if (xbox_fetcher_)
118     xbox_fetcher_->UnregisterFromNotifications();
121 void GamepadPlatformDataFetcherMac::PauseHint(bool pause) {
122   if (pause)
123     UnregisterFromNotifications();
124   else
125     RegisterForNotifications();
128 GamepadPlatformDataFetcherMac::~GamepadPlatformDataFetcherMac() {
129   UnregisterFromNotifications();
132 GamepadPlatformDataFetcherMac*
133 GamepadPlatformDataFetcherMac::InstanceFromContext(void* context) {
134   return reinterpret_cast<GamepadPlatformDataFetcherMac*>(context);
137 void GamepadPlatformDataFetcherMac::DeviceAddCallback(void* context,
138                                                       IOReturn result,
139                                                       void* sender,
140                                                       IOHIDDeviceRef ref) {
141   InstanceFromContext(context)->DeviceAdd(ref);
144 void GamepadPlatformDataFetcherMac::DeviceRemoveCallback(void* context,
145                                                          IOReturn result,
146                                                          void* sender,
147                                                          IOHIDDeviceRef ref) {
148   InstanceFromContext(context)->DeviceRemove(ref);
151 void GamepadPlatformDataFetcherMac::ValueChangedCallback(void* context,
152                                                          IOReturn result,
153                                                          void* sender,
154                                                          IOHIDValueRef ref) {
155   InstanceFromContext(context)->ValueChanged(ref);
158 bool GamepadPlatformDataFetcherMac::AddButtonsAndAxes(NSArray* elements,
159                                                       size_t slot) {
160   WebGamepad& pad = data_.items[slot];
161   AssociatedData& associated = associated_[slot];
162   CHECK(!associated.is_xbox);
164   pad.axesLength = 0;
165   pad.buttonsLength = 0;
166   pad.timestamp = 0;
167   memset(pad.axes, 0, sizeof(pad.axes));
168   memset(pad.buttons, 0, sizeof(pad.buttons));
170   bool mapped_all_axes = true;
172   for (id elem in elements) {
173     IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
174     uint32_t usage_page = IOHIDElementGetUsagePage(element);
175     uint32_t usage = IOHIDElementGetUsage(element);
176     if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button &&
177         usage_page == kButtonUsagePage) {
178       uint32_t button_index = usage - 1;
179       if (button_index < WebGamepad::buttonsLengthCap) {
180         associated.hid.button_elements[button_index] = element;
181         pad.buttonsLength = std::max(pad.buttonsLength, button_index + 1);
182       }
183     }
184     else if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc) {
185       uint32_t axis_index = usage - kAxisMinimumUsageNumber;
186       if (axis_index < WebGamepad::axesLengthCap) {
187         associated.hid.axis_minimums[axis_index] =
188             IOHIDElementGetLogicalMin(element);
189         associated.hid.axis_maximums[axis_index] =
190             IOHIDElementGetLogicalMax(element);
191         associated.hid.axis_elements[axis_index] = element;
192         pad.axesLength = std::max(pad.axesLength, axis_index + 1);
193       } else {
194         mapped_all_axes = false;
195       }
196     }
197   }
199   if (!mapped_all_axes) {
200     // For axes who's usage puts them outside the standard axesLengthCap range.
201     uint32_t next_index = 0;
202     for (id elem in elements) {
203       IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
204       uint32_t usage_page = IOHIDElementGetUsagePage(element);
205       uint32_t usage = IOHIDElementGetUsage(element);
206       if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc &&
207           usage - kAxisMinimumUsageNumber >= WebGamepad::axesLengthCap &&
208           usage_page <= kGameControlsUsagePage) {
209         for (; next_index < WebGamepad::axesLengthCap; ++next_index) {
210           if (associated.hid.axis_elements[next_index] == NULL)
211             break;
212         }
213         if (next_index < WebGamepad::axesLengthCap) {
214           associated.hid.axis_minimums[next_index] =
215               IOHIDElementGetLogicalMin(element);
216           associated.hid.axis_maximums[next_index] =
217               IOHIDElementGetLogicalMax(element);
218           associated.hid.axis_elements[next_index] = element;
219           pad.axesLength = std::max(pad.axesLength, next_index + 1);
220         }
221       }
223       if (next_index >= WebGamepad::axesLengthCap)
224         break;
225     }
226   }
228   return (pad.axesLength > 0 || pad.buttonsLength > 0);
231 size_t GamepadPlatformDataFetcherMac::GetEmptySlot() {
232   // Find a free slot for this device.
233   for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
234     if (!data_.items[slot].connected)
235       return slot;
236   }
237   return WebGamepads::itemsLengthCap;
240 size_t GamepadPlatformDataFetcherMac::GetSlotForDevice(IOHIDDeviceRef device) {
241   for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
242     // If we already have this device, and it's already connected, don't do
243     // anything now.
244     if (data_.items[slot].connected &&
245         !associated_[slot].is_xbox &&
246         associated_[slot].hid.device_ref == device)
247       return WebGamepads::itemsLengthCap;
248   }
249   return GetEmptySlot();
252 size_t GamepadPlatformDataFetcherMac::GetSlotForXboxDevice(
253     XboxController* device) {
254   for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
255     if (associated_[slot].is_xbox &&
256         associated_[slot].xbox.location_id == device->location_id()) {
257       if (data_.items[slot].connected) {
258         // The device is already connected. No idea why we got a second "device
259         // added" call, but let's not add it twice.
260         DCHECK_EQ(associated_[slot].xbox.device, device);
261         return WebGamepads::itemsLengthCap;
262       } else {
263         // A device with the same location ID was previously connected, so put
264         // it in the same slot.
265         return slot;
266       }
267     }
268   }
269   return GetEmptySlot();
272 void GamepadPlatformDataFetcherMac::DeviceAdd(IOHIDDeviceRef device) {
273   using base::mac::CFToNSCast;
274   using base::mac::CFCastStrict;
276   if (!enabled_)
277     return;
279   // Find an index for this device.
280   size_t slot = GetSlotForDevice(device);
282   // We can't handle this many connected devices.
283   if (slot == WebGamepads::itemsLengthCap)
284     return;
286   // Clear some state that may have been left behind by previous gamepads
287   memset(&associated_[slot], 0, sizeof(AssociatedData));
289   NSNumber* vendor_id = CFToNSCast(CFCastStrict<CFNumberRef>(
290       IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))));
291   NSNumber* product_id = CFToNSCast(CFCastStrict<CFNumberRef>(
292       IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey))));
293   NSString* product = CFToNSCast(CFCastStrict<CFStringRef>(
294       IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey))));
295   int vendor_int = [vendor_id intValue];
296   int product_int = [product_id intValue];
298   char vendor_as_str[5], product_as_str[5];
299   snprintf(vendor_as_str, sizeof(vendor_as_str), "%04x", vendor_int);
300   snprintf(product_as_str, sizeof(product_as_str), "%04x", product_int);
301   associated_[slot].hid.mapper =
302       GetGamepadStandardMappingFunction(vendor_as_str, product_as_str);
304   NSString* ident = [NSString stringWithFormat:
305       @"%@ (%sVendor: %04x Product: %04x)",
306       product,
307       associated_[slot].hid.mapper ? "STANDARD GAMEPAD " : "",
308       vendor_int,
309       product_int];
310   CopyNSStringAsUTF16LittleEndian(
311       ident,
312       data_.items[slot].id,
313       sizeof(data_.items[slot].id));
315   if (associated_[slot].hid.mapper) {
316     CopyNSStringAsUTF16LittleEndian(
317       @"standard",
318       data_.items[slot].mapping,
319       sizeof(data_.items[slot].mapping));
320   } else {
321     data_.items[slot].mapping[0] = 0;
322   }
324   base::ScopedCFTypeRef<CFArrayRef> elements(
325       IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone));
327   if (!AddButtonsAndAxes(CFToNSCast(elements), slot))
328     return;
330   associated_[slot].hid.device_ref = device;
331   data_.items[slot].connected = true;
332   if (slot >= data_.length)
333     data_.length = slot + 1;
336 void GamepadPlatformDataFetcherMac::DeviceRemove(IOHIDDeviceRef device) {
337   if (!enabled_)
338     return;
340   // Find the index for this device.
341   size_t slot;
342   for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
343     if (data_.items[slot].connected &&
344         !associated_[slot].is_xbox &&
345         associated_[slot].hid.device_ref == device)
346       break;
347   }
348   DCHECK(slot < WebGamepads::itemsLengthCap);
349   // Leave associated device_ref so that it will be reconnected in the same
350   // location. Simply mark it as disconnected.
351   data_.items[slot].connected = false;
354 void GamepadPlatformDataFetcherMac::ValueChanged(IOHIDValueRef value) {
355   if (!enabled_)
356     return;
358   IOHIDElementRef element = IOHIDValueGetElement(value);
359   IOHIDDeviceRef device = IOHIDElementGetDevice(element);
361   // Find device slot.
362   size_t slot;
363   for (slot = 0; slot < data_.length; ++slot) {
364     if (data_.items[slot].connected &&
365         !associated_[slot].is_xbox &&
366         associated_[slot].hid.device_ref == device)
367       break;
368   }
369   if (slot == data_.length)
370     return;
372   WebGamepad& pad = data_.items[slot];
373   AssociatedData& associated = associated_[slot];
375   uint32_t value_length = IOHIDValueGetLength(value);
376   if (value_length > 4) {
377     // Workaround for bizarre issue with PS3 controllers that try to return
378     // massive (30+ byte) values and crash IOHIDValueGetIntegerValue
379     return;
380   }
382   // Find and fill in the associated button event, if any.
383   for (size_t i = 0; i < pad.buttonsLength; ++i) {
384     if (associated.hid.button_elements[i] == element) {
385       pad.buttons[i].pressed = IOHIDValueGetIntegerValue(value);
386       pad.buttons[i].value = pad.buttons[i].pressed ? 1.f : 0.f;
387       pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
388       return;
389     }
390   }
392   // Find and fill in the associated axis event, if any.
393   for (size_t i = 0; i < pad.axesLength; ++i) {
394     if (associated.hid.axis_elements[i] == element) {
395       pad.axes[i] = NormalizeAxis(IOHIDValueGetIntegerValue(value),
396                                   associated.hid.axis_minimums[i],
397                                   associated.hid.axis_maximums[i]);
398       pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
399       return;
400     }
401   }
404 void GamepadPlatformDataFetcherMac::XboxDeviceAdd(XboxController* device) {
405   if (!enabled_)
406     return;
408   size_t slot = GetSlotForXboxDevice(device);
410   // We can't handle this many connected devices.
411   if (slot == WebGamepads::itemsLengthCap)
412     return;
414   device->SetLEDPattern(
415       (XboxController::LEDPattern)(XboxController::LED_FLASH_TOP_LEFT + slot));
417   NSString* ident =
418       [NSString stringWithFormat:
419           @"%@ (STANDARD GAMEPAD Vendor: %04x Product: %04x)",
420               device->GetControllerType() == XboxController::XBOX_360_CONTROLLER
421                   ? @"Xbox 360 Controller"
422                   : @"Xbox One Controller",
423               device->GetProductId(), device->GetVendorId()];
424   CopyNSStringAsUTF16LittleEndian(
425       ident,
426       data_.items[slot].id,
427       sizeof(data_.items[slot].id));
429   CopyNSStringAsUTF16LittleEndian(
430     @"standard",
431     data_.items[slot].mapping,
432     sizeof(data_.items[slot].mapping));
434   associated_[slot].is_xbox = true;
435   associated_[slot].xbox.device = device;
436   associated_[slot].xbox.location_id = device->location_id();
437   data_.items[slot].connected = true;
438   data_.items[slot].axesLength = 4;
439   data_.items[slot].buttonsLength = 17;
440   data_.items[slot].timestamp = 0;
441   if (slot >= data_.length)
442     data_.length = slot + 1;
445 void GamepadPlatformDataFetcherMac::XboxDeviceRemove(XboxController* device) {
446   if (!enabled_)
447     return;
449   // Find the index for this device.
450   size_t slot;
451   for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
452     if (data_.items[slot].connected &&
453         associated_[slot].is_xbox &&
454         associated_[slot].xbox.device == device)
455       break;
456   }
457   DCHECK(slot < WebGamepads::itemsLengthCap);
458   // Leave associated location id so that the controller will be reconnected in
459   // the same slot if it is plugged in again. Simply mark it as disconnected.
460   data_.items[slot].connected = false;
463 void GamepadPlatformDataFetcherMac::XboxValueChanged(
464     XboxController* device, const XboxController::Data& data) {
465   // Find device slot.
466   size_t slot;
467   for (slot = 0; slot < data_.length; ++slot) {
468     if (data_.items[slot].connected &&
469         associated_[slot].is_xbox &&
470         associated_[slot].xbox.device == device)
471       break;
472   }
473   if (slot == data_.length)
474     return;
476   WebGamepad& pad = data_.items[slot];
478   for (size_t i = 0; i < 6; i++) {
479     pad.buttons[i].pressed = data.buttons[i];
480     pad.buttons[i].value = data.buttons[i] ? 1.0f : 0.0f;
481   }
482   pad.buttons[6].pressed = data.triggers[0] > kDefaultButtonPressedThreshold;
483   pad.buttons[6].value = data.triggers[0];
484   pad.buttons[7].pressed = data.triggers[1] > kDefaultButtonPressedThreshold;
485   pad.buttons[7].value = data.triggers[1];
486   for (size_t i = 8; i < 17; i++) {
487     pad.buttons[i].pressed = data.buttons[i - 2];
488     pad.buttons[i].value = data.buttons[i - 2] ? 1.0f : 0.0f;
489   }
490   for (size_t i = 0; i < arraysize(data.axes); i++) {
491     pad.axes[i] = data.axes[i];
492   }
494   pad.timestamp = base::TimeTicks::Now().ToInternalValue();
497 void GamepadPlatformDataFetcherMac::GetGamepadData(WebGamepads* pads, bool) {
498   if (!enabled_ && !xbox_fetcher_) {
499     pads->length = 0;
500     return;
501   }
503   // Copy to the current state to the output buffer, using the mapping
504   // function, if there is one available.
505   pads->length = WebGamepads::itemsLengthCap;
506   for (size_t i = 0; i < WebGamepads::itemsLengthCap; ++i) {
507     if (!associated_[i].is_xbox && associated_[i].hid.mapper)
508       associated_[i].hid.mapper(data_.items[i], &pads->items[i]);
509     else
510       pads->items[i] = data_.items[i];
511   }
514 }  // namespace content