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