gamepad_platform_data_fetcher_mac.mm revision 68043e1e95eeb07d5cae7aca370b26518b0867d6
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.
4
5#include "content/browser/gamepad/gamepad_platform_data_fetcher_mac.h"
6
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"
13
14#import <Foundation/Foundation.h>
15#include <IOKit/hid/IOHIDKeys.h>
16
17using WebKit::WebGamepad;
18using WebKit::WebGamepads;
19
20namespace content {
21
22namespace {
23
24NSDictionary* DeviceMatching(uint32_t usage_page, uint32_t usage) {
25  return [NSDictionary dictionaryWithObjectsAndKeys:
26      [NSNumber numberWithUnsignedInt:usage_page],
27          base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsagePageKey)),
28      [NSNumber numberWithUnsignedInt:usage],
29          base::mac::CFToNSCast(CFSTR(kIOHIDDeviceUsageKey)),
30      nil];
31}
32
33float NormalizeAxis(CFIndex value, CFIndex min, CFIndex max) {
34  return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
35}
36
37// http://www.usb.org/developers/hidpage
38const uint32_t kGenericDesktopUsagePage = 0x01;
39const uint32_t kButtonUsagePage = 0x09;
40const uint32_t kJoystickUsageNumber = 0x04;
41const uint32_t kGameUsageNumber = 0x05;
42const uint32_t kMultiAxisUsageNumber = 0x08;
43const uint32_t kAxisMinimumUsageNumber = 0x30;
44
45}  // namespace
46
47GamepadPlatformDataFetcherMac::GamepadPlatformDataFetcherMac()
48    : enabled_(true) {
49  memset(associated_, 0, sizeof(associated_));
50
51  xbox_fetcher_.reset(new XboxDataFetcher(this));
52  if (!xbox_fetcher_->RegisterForNotifications())
53    xbox_fetcher_.reset();
54
55  hid_manager_ref_.reset(IOHIDManagerCreate(kCFAllocatorDefault,
56                                            kIOHIDOptionsTypeNone));
57  if (CFGetTypeID(hid_manager_ref_) != IOHIDManagerGetTypeID()) {
58    enabled_ = false;
59    return;
60  }
61
62  base::scoped_nsobject<NSArray> criteria([[NSArray alloc] initWithObjects:
63      DeviceMatching(kGenericDesktopUsagePage, kJoystickUsageNumber),
64      DeviceMatching(kGenericDesktopUsagePage, kGameUsageNumber),
65      DeviceMatching(kGenericDesktopUsagePage, kMultiAxisUsageNumber),
66      nil]);
67  IOHIDManagerSetDeviceMatchingMultiple(
68      hid_manager_ref_,
69      base::mac::NSToCFCast(criteria));
70
71  RegisterForNotifications();
72}
73
74void GamepadPlatformDataFetcherMac::RegisterForNotifications() {
75  // Register for plug/unplug notifications.
76  IOHIDManagerRegisterDeviceMatchingCallback(
77      hid_manager_ref_,
78      &DeviceAddCallback,
79      this);
80  IOHIDManagerRegisterDeviceRemovalCallback(
81      hid_manager_ref_,
82      DeviceRemoveCallback,
83      this);
84
85  // Register for value change notifications.
86  IOHIDManagerRegisterInputValueCallback(
87      hid_manager_ref_,
88      ValueChangedCallback,
89      this);
90
91  IOHIDManagerScheduleWithRunLoop(
92      hid_manager_ref_,
93      CFRunLoopGetMain(),
94      kCFRunLoopDefaultMode);
95
96  enabled_ = IOHIDManagerOpen(hid_manager_ref_,
97                              kIOHIDOptionsTypeNone) == kIOReturnSuccess;
98
99  if (xbox_fetcher_)
100    xbox_fetcher_->RegisterForNotifications();
101}
102
103void GamepadPlatformDataFetcherMac::UnregisterFromNotifications() {
104  IOHIDManagerUnscheduleFromRunLoop(
105      hid_manager_ref_,
106      CFRunLoopGetCurrent(),
107      kCFRunLoopDefaultMode);
108  IOHIDManagerClose(hid_manager_ref_, kIOHIDOptionsTypeNone);
109  if (xbox_fetcher_)
110    xbox_fetcher_->UnregisterFromNotifications();
111}
112
113void GamepadPlatformDataFetcherMac::PauseHint(bool pause) {
114  if (pause)
115    UnregisterFromNotifications();
116  else
117    RegisterForNotifications();
118}
119
120GamepadPlatformDataFetcherMac::~GamepadPlatformDataFetcherMac() {
121  UnregisterFromNotifications();
122}
123
124GamepadPlatformDataFetcherMac*
125GamepadPlatformDataFetcherMac::InstanceFromContext(void* context) {
126  return reinterpret_cast<GamepadPlatformDataFetcherMac*>(context);
127}
128
129void GamepadPlatformDataFetcherMac::DeviceAddCallback(void* context,
130                                                      IOReturn result,
131                                                      void* sender,
132                                                      IOHIDDeviceRef ref) {
133  InstanceFromContext(context)->DeviceAdd(ref);
134}
135
136void GamepadPlatformDataFetcherMac::DeviceRemoveCallback(void* context,
137                                                         IOReturn result,
138                                                         void* sender,
139                                                         IOHIDDeviceRef ref) {
140  InstanceFromContext(context)->DeviceRemove(ref);
141}
142
143void GamepadPlatformDataFetcherMac::ValueChangedCallback(void* context,
144                                                         IOReturn result,
145                                                         void* sender,
146                                                         IOHIDValueRef ref) {
147  InstanceFromContext(context)->ValueChanged(ref);
148}
149
150void GamepadPlatformDataFetcherMac::AddButtonsAndAxes(NSArray* elements,
151                                                      size_t slot) {
152  WebGamepad& pad = data_.items[slot];
153  AssociatedData& associated = associated_[slot];
154  CHECK(!associated.is_xbox);
155
156  pad.axesLength = 0;
157  pad.buttonsLength = 0;
158  pad.timestamp = 0;
159  memset(pad.axes, 0, sizeof(pad.axes));
160  memset(pad.buttons, 0, sizeof(pad.buttons));
161
162  for (id elem in elements) {
163    IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
164    uint32_t usagePage = IOHIDElementGetUsagePage(element);
165    uint32_t usage = IOHIDElementGetUsage(element);
166    if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button &&
167        usagePage == kButtonUsagePage) {
168      uint32_t button_index = usage - 1;
169      if (button_index < WebGamepad::buttonsLengthCap) {
170        associated.hid.button_elements[button_index] = element;
171        pad.buttonsLength = std::max(pad.buttonsLength, button_index + 1);
172      }
173    }
174    else if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc) {
175      uint32_t axis_index = usage - kAxisMinimumUsageNumber;
176      if (axis_index < WebGamepad::axesLengthCap) {
177        associated.hid.axis_minimums[axis_index] =
178            IOHIDElementGetLogicalMin(element);
179        associated.hid.axis_maximums[axis_index] =
180            IOHIDElementGetLogicalMax(element);
181        associated.hid.axis_elements[axis_index] = element;
182        pad.axesLength = std::max(pad.axesLength, axis_index + 1);
183      }
184    }
185  }
186}
187
188size_t GamepadPlatformDataFetcherMac::GetEmptySlot() {
189  // Find a free slot for this device.
190  for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
191    if (!data_.items[slot].connected)
192      return slot;
193  }
194  return WebGamepads::itemsLengthCap;
195}
196
197size_t GamepadPlatformDataFetcherMac::GetSlotForDevice(IOHIDDeviceRef device) {
198  for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
199    // If we already have this device, and it's already connected, don't do
200    // anything now.
201    if (data_.items[slot].connected &&
202        !associated_[slot].is_xbox &&
203        associated_[slot].hid.device_ref == device)
204      return WebGamepads::itemsLengthCap;
205  }
206  return GetEmptySlot();
207}
208
209size_t GamepadPlatformDataFetcherMac::GetSlotForXboxDevice(
210    XboxController* device) {
211  for (size_t slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
212    if (associated_[slot].is_xbox &&
213        associated_[slot].xbox.location_id == device->location_id()) {
214      if (data_.items[slot].connected) {
215        // The device is already connected. No idea why we got a second "device
216        // added" call, but let's not add it twice.
217        DCHECK_EQ(associated_[slot].xbox.device, device);
218        return WebGamepads::itemsLengthCap;
219      } else {
220        // A device with the same location ID was previously connected, so put
221        // it in the same slot.
222        return slot;
223      }
224    }
225  }
226  return GetEmptySlot();
227}
228
229void GamepadPlatformDataFetcherMac::DeviceAdd(IOHIDDeviceRef device) {
230  using base::mac::CFToNSCast;
231  using base::mac::CFCastStrict;
232
233  if (!enabled_)
234    return;
235
236  // Find an index for this device.
237  size_t slot = GetSlotForDevice(device);
238
239  // We can't handle this many connected devices.
240  if (slot == WebGamepads::itemsLengthCap)
241    return;
242
243  NSNumber* vendor_id = CFToNSCast(CFCastStrict<CFNumberRef>(
244      IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))));
245  NSNumber* product_id = CFToNSCast(CFCastStrict<CFNumberRef>(
246      IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey))));
247  NSString* product = CFToNSCast(CFCastStrict<CFStringRef>(
248      IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey))));
249  int vendor_int = [vendor_id intValue];
250  int product_int = [product_id intValue];
251
252  char vendor_as_str[5], product_as_str[5];
253  snprintf(vendor_as_str, sizeof(vendor_as_str), "%04x", vendor_int);
254  snprintf(product_as_str, sizeof(product_as_str), "%04x", product_int);
255  associated_[slot].hid.mapper =
256      GetGamepadStandardMappingFunction(vendor_as_str, product_as_str);
257
258  NSString* ident = [NSString stringWithFormat:
259      @"%@ (%sVendor: %04x Product: %04x)",
260      product,
261      associated_[slot].hid.mapper ? "STANDARD GAMEPAD " : "",
262      vendor_int,
263      product_int];
264  NSData* as16 = [ident dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
265
266  const size_t kOutputLengthBytes = sizeof(data_.items[slot].id);
267  memset(&data_.items[slot].id, 0, kOutputLengthBytes);
268  [as16 getBytes:data_.items[slot].id
269          length:kOutputLengthBytes - sizeof(WebKit::WebUChar)];
270
271  base::ScopedCFTypeRef<CFArrayRef> elements(
272      IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone));
273  AddButtonsAndAxes(CFToNSCast(elements), slot);
274
275  associated_[slot].hid.device_ref = device;
276  data_.items[slot].connected = true;
277  if (slot >= data_.length)
278    data_.length = slot + 1;
279}
280
281void GamepadPlatformDataFetcherMac::DeviceRemove(IOHIDDeviceRef device) {
282  if (!enabled_)
283    return;
284
285  // Find the index for this device.
286  size_t slot;
287  for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
288    if (data_.items[slot].connected &&
289        !associated_[slot].is_xbox &&
290        associated_[slot].hid.device_ref == device)
291      break;
292  }
293  DCHECK(slot < WebGamepads::itemsLengthCap);
294  // Leave associated device_ref so that it will be reconnected in the same
295  // location. Simply mark it as disconnected.
296  data_.items[slot].connected = false;
297}
298
299void GamepadPlatformDataFetcherMac::ValueChanged(IOHIDValueRef value) {
300  if (!enabled_)
301    return;
302
303  IOHIDElementRef element = IOHIDValueGetElement(value);
304  IOHIDDeviceRef device = IOHIDElementGetDevice(element);
305
306  // Find device slot.
307  size_t slot;
308  for (slot = 0; slot < data_.length; ++slot) {
309    if (data_.items[slot].connected &&
310        !associated_[slot].is_xbox &&
311        associated_[slot].hid.device_ref == device)
312      break;
313  }
314  if (slot == data_.length)
315    return;
316
317  WebGamepad& pad = data_.items[slot];
318  AssociatedData& associated = associated_[slot];
319
320  // Find and fill in the associated button event, if any.
321  for (size_t i = 0; i < pad.buttonsLength; ++i) {
322    if (associated.hid.button_elements[i] == element) {
323      pad.buttons[i] = IOHIDValueGetIntegerValue(value) ? 1.f : 0.f;
324      pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
325      return;
326    }
327  }
328
329  // Find and fill in the associated axis event, if any.
330  for (size_t i = 0; i < pad.axesLength; ++i) {
331    if (associated.hid.axis_elements[i] == element) {
332      pad.axes[i] = NormalizeAxis(IOHIDValueGetIntegerValue(value),
333                                  associated.hid.axis_minimums[i],
334                                  associated.hid.axis_maximums[i]);
335      pad.timestamp = std::max(pad.timestamp, IOHIDValueGetTimeStamp(value));
336      return;
337    }
338  }
339}
340
341void GamepadPlatformDataFetcherMac::XboxDeviceAdd(XboxController* device) {
342  if (!enabled_)
343    return;
344
345  size_t slot = GetSlotForXboxDevice(device);
346
347  // We can't handle this many connected devices.
348  if (slot == WebGamepads::itemsLengthCap)
349    return;
350
351  device->SetLEDPattern(
352      (XboxController::LEDPattern)(XboxController::LED_FLASH_TOP_LEFT + slot));
353
354  NSString* ident =
355      [NSString stringWithFormat:
356          @"Xbox 360 Controller (STANDARD GAMEPAD Vendor: %04x Product: %04x)",
357              device->GetProductId(), device->GetVendorId()];
358  NSData* as16 = [ident dataUsingEncoding:NSUTF16StringEncoding];
359  const size_t kOutputLengthBytes = sizeof(data_.items[slot].id);
360  memset(&data_.items[slot].id, 0, kOutputLengthBytes);
361  [as16 getBytes:data_.items[slot].id
362          length:kOutputLengthBytes - sizeof(WebKit::WebUChar)];
363
364  associated_[slot].is_xbox = true;
365  associated_[slot].xbox.device = device;
366  associated_[slot].xbox.location_id = device->location_id();
367  data_.items[slot].connected = true;
368  data_.items[slot].axesLength = 4;
369  data_.items[slot].buttonsLength = 17;
370  data_.items[slot].timestamp = 0;
371  if (slot >= data_.length)
372    data_.length = slot + 1;
373}
374
375void GamepadPlatformDataFetcherMac::XboxDeviceRemove(XboxController* device) {
376  if (!enabled_)
377    return;
378
379  // Find the index for this device.
380  size_t slot;
381  for (slot = 0; slot < WebGamepads::itemsLengthCap; ++slot) {
382    if (data_.items[slot].connected &&
383        associated_[slot].is_xbox &&
384        associated_[slot].xbox.device == device)
385      break;
386  }
387  DCHECK(slot < WebGamepads::itemsLengthCap);
388  // Leave associated location id so that the controller will be reconnected in
389  // the same slot if it is plugged in again. Simply mark it as disconnected.
390  data_.items[slot].connected = false;
391}
392
393void GamepadPlatformDataFetcherMac::XboxValueChanged(
394    XboxController* device, const XboxController::Data& data) {
395  // Find device slot.
396  size_t slot;
397  for (slot = 0; slot < data_.length; ++slot) {
398    if (data_.items[slot].connected &&
399        associated_[slot].is_xbox &&
400        associated_[slot].xbox.device == device)
401      break;
402  }
403  if (slot == data_.length)
404    return;
405
406  WebGamepad& pad = data_.items[slot];
407
408  for (size_t i = 0; i < 6; i++) {
409    pad.buttons[i] = data.buttons[i] ? 1.0f : 0.0f;
410  }
411  pad.buttons[6] = data.triggers[0];
412  pad.buttons[7] = data.triggers[1];
413  for (size_t i = 8; i < 17; i++) {
414    pad.buttons[i] = data.buttons[i - 2] ? 1.0f : 0.0f;
415  }
416  for (size_t i = 0; i < arraysize(data.axes); i++) {
417    pad.axes[i] = data.axes[i];
418  }
419
420  pad.timestamp = base::TimeTicks::Now().ToInternalValue();
421}
422
423void GamepadPlatformDataFetcherMac::GetGamepadData(WebGamepads* pads, bool) {
424  if (!enabled_ && !xbox_fetcher_) {
425    pads->length = 0;
426    return;
427  }
428
429  // Copy to the current state to the output buffer, using the mapping
430  // function, if there is one available.
431  pads->length = WebGamepads::itemsLengthCap;
432  for (size_t i = 0; i < WebGamepads::itemsLengthCap; ++i) {
433    if (!associated_[i].is_xbox && associated_[i].hid.mapper)
434      associated_[i].hid.mapper(data_.items[i], &pads->items[i]);
435    else
436      pads->items[i] = data_.items[i];
437  }
438}
439
440}  // namespace content
441