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 "device/gamepad/gamepad_platform_data_fetcher_mac.h" 6 7#include <stdint.h> 8#include <string.h> 9 10#include "base/mac/foundation_util.h" 11#include "base/mac/scoped_nsobject.h" 12#include "base/sequenced_task_runner.h" 13#include "base/strings/sys_string_conversions.h" 14#include "base/time/time.h" 15#include "device/gamepad/gamepad_blocklist.h" 16#include "device/gamepad/gamepad_device_mac.h" 17#include "device/gamepad/gamepad_id_list.h" 18#include "device/gamepad/gamepad_uma.h" 19#include "device/gamepad/nintendo_controller.h" 20 21#import <Foundation/Foundation.h> 22#include <IOKit/hid/IOHIDKeys.h> 23 24namespace device { 25 26namespace { 27 28// http://www.usb.org/developers/hidpage 29const uint16_t kGenericDesktopUsagePage = 0x01; 30const uint16_t kJoystickUsageNumber = 0x04; 31const uint16_t kGameUsageNumber = 0x05; 32const uint16_t kMultiAxisUsageNumber = 0x08; 33 34NSDictionary* DeviceMatching(uint32_t usage_page, uint32_t usage) { 35 return [NSDictionary 36 dictionaryWithObjectsAndKeys:[NSNumber numberWithUnsignedInt:usage_page], 37 base::mac::CFToNSCast( 38 CFSTR(kIOHIDDeviceUsagePageKey)), 39 [NSNumber numberWithUnsignedInt:usage], 40 base::mac::CFToNSCast( 41 CFSTR(kIOHIDDeviceUsageKey)), 42 nil]; 43} 44 45} // namespace 46 47GamepadPlatformDataFetcherMac::GamepadPlatformDataFetcherMac() = default; 48 49GamepadSource GamepadPlatformDataFetcherMac::source() { 50 return Factory::static_source(); 51} 52 53void GamepadPlatformDataFetcherMac::OnAddedToProvider() { 54 hid_manager_ref_.reset( 55 IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone)); 56 if (CFGetTypeID(hid_manager_ref_) != IOHIDManagerGetTypeID()) { 57 enabled_ = false; 58 return; 59 } 60 61 base::scoped_nsobject<NSArray> criteria( 62 [[NSArray alloc] initWithObjects:DeviceMatching(kGenericDesktopUsagePage, 63 kJoystickUsageNumber), 64 DeviceMatching(kGenericDesktopUsagePage, 65 kGameUsageNumber), 66 DeviceMatching(kGenericDesktopUsagePage, 67 kMultiAxisUsageNumber), 68 nil]); 69 IOHIDManagerSetDeviceMatchingMultiple(hid_manager_ref_, 70 base::mac::NSToCFCast(criteria)); 71 72 RegisterForNotifications(); 73} 74 75void GamepadPlatformDataFetcherMac::RegisterForNotifications() { 76 // Register for plug/unplug notifications. 77 IOHIDManagerRegisterDeviceMatchingCallback(hid_manager_ref_, 78 DeviceAddCallback, this); 79 IOHIDManagerRegisterDeviceRemovalCallback(hid_manager_ref_, 80 DeviceRemoveCallback, this); 81 82 // Register for value change notifications. 83 IOHIDManagerRegisterInputValueCallback(hid_manager_ref_, ValueChangedCallback, 84 this); 85 86 IOHIDManagerScheduleWithRunLoop(hid_manager_ref_, CFRunLoopGetCurrent(), 87 kCFRunLoopDefaultMode); 88 89 enabled_ = IOHIDManagerOpen(hid_manager_ref_, kIOHIDOptionsTypeNone) == 90 kIOReturnSuccess; 91} 92 93void GamepadPlatformDataFetcherMac::UnregisterFromNotifications() { 94 IOHIDManagerUnscheduleFromRunLoop(hid_manager_ref_, CFRunLoopGetCurrent(), 95 kCFRunLoopDefaultMode); 96 IOHIDManagerClose(hid_manager_ref_, kIOHIDOptionsTypeNone); 97} 98 99void GamepadPlatformDataFetcherMac::PauseHint(bool pause) { 100 paused_ = pause; 101} 102 103GamepadPlatformDataFetcherMac::~GamepadPlatformDataFetcherMac() { 104 UnregisterFromNotifications(); 105 for (auto& iter : devices_) { 106 iter.second->Shutdown(); 107 } 108} 109 110GamepadPlatformDataFetcherMac* 111GamepadPlatformDataFetcherMac::InstanceFromContext(void* context) { 112 return reinterpret_cast<GamepadPlatformDataFetcherMac*>(context); 113} 114 115void GamepadPlatformDataFetcherMac::DeviceAddCallback(void* context, 116 IOReturn result, 117 void* sender, 118 IOHIDDeviceRef ref) { 119 InstanceFromContext(context)->DeviceAdd(ref); 120} 121 122void GamepadPlatformDataFetcherMac::DeviceRemoveCallback(void* context, 123 IOReturn result, 124 void* sender, 125 IOHIDDeviceRef ref) { 126 InstanceFromContext(context)->DeviceRemove(ref); 127} 128 129void GamepadPlatformDataFetcherMac::ValueChangedCallback(void* context, 130 IOReturn result, 131 void* sender, 132 IOHIDValueRef ref) { 133 InstanceFromContext(context)->ValueChanged(ref); 134} 135 136GamepadDeviceMac* GamepadPlatformDataFetcherMac::GetGamepadFromHidDevice( 137 IOHIDDeviceRef device) { 138 for (auto& iter : devices_) { 139 if (iter.second->IsSameDevice(device)) { 140 return iter.second.get(); 141 } 142 } 143 144 return nullptr; 145} 146 147void GamepadPlatformDataFetcherMac::DeviceAdd(IOHIDDeviceRef device) { 148 using base::mac::CFToNSCast; 149 using base::mac::CFCastStrict; 150 151 if (!enabled_) 152 return; 153 154 NSNumber* location_id = CFToNSCast(CFCastStrict<CFNumberRef>( 155 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDLocationIDKey)))); 156 int location_int = [location_id intValue]; 157 158 NSNumber* vendor_id = CFToNSCast(CFCastStrict<CFNumberRef>( 159 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)))); 160 NSNumber* product_id = CFToNSCast(CFCastStrict<CFNumberRef>( 161 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)))); 162 NSNumber* version_number = CFToNSCast(CFCastStrict<CFNumberRef>( 163 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVersionNumberKey)))); 164 NSString* product = CFToNSCast(CFCastStrict<CFStringRef>( 165 IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)))); 166 uint16_t vendor_int = [vendor_id intValue]; 167 uint16_t product_int = [product_id intValue]; 168 uint16_t version_int = [version_number intValue]; 169 170 // Filter out devices that have gamepad-like HID usages but aren't gamepads. 171 if (GamepadIsExcluded(vendor_int, product_int)) 172 return; 173 174 // Nintendo devices are handled by the Nintendo data fetcher. 175 if (NintendoController::IsNintendoController(vendor_int, product_int)) 176 return; 177 178 // Record the device before excluding Made for iOS gamepads. This allows us to 179 // recognize these devices even though the GameController API masks the vendor 180 // and product IDs. XInput devices are recorded elsewhere. 181 const auto& gamepad_id_list = GamepadIdList::Get(); 182 DCHECK_EQ(kXInputTypeNone, 183 gamepad_id_list.GetXInputType(vendor_int, product_int)); 184 185 if (devices_.find(location_int) != devices_.end()) 186 return; 187 188 RecordConnectedGamepad(vendor_int, product_int); 189 190 // The SteelSeries Nimbus and other Made for iOS gamepads should be handled 191 // through the GameController interface. 192 if (gamepad_id_list.GetGamepadId(vendor_int, product_int) == 193 GamepadId::kSteelSeriesProduct1420) { 194 return; 195 } 196 197 bool is_recognized = gamepad_id_list.GetGamepadId(vendor_int, product_int) != 198 GamepadId::kUnknownGamepad; 199 200 PadState* state = GetPadState(location_int, is_recognized); 201 if (!state) 202 return; // No available slot for this device 203 204 state->mapper = GetGamepadStandardMappingFunction( 205 vendor_int, product_int, /*hid_specification_version=*/0, version_int, 206 GAMEPAD_BUS_UNKNOWN); 207 208 NSString* ident = 209 [NSString stringWithFormat:@"%@ (%sVendor: %04x Product: %04x)", product, 210 state->mapper ? "STANDARD GAMEPAD " : "", 211 vendor_int, product_int]; 212 state->data.SetID(base::SysNSStringToUTF16(ident)); 213 214 state->data.mapping = 215 state->mapper ? GamepadMapping::kStandard : GamepadMapping::kNone; 216 217 auto new_device = std::make_unique<GamepadDeviceMac>(location_int, device, 218 vendor_int, product_int); 219 if (!new_device->AddButtonsAndAxes(&state->data)) { 220 new_device->Shutdown(); 221 return; 222 } 223 224 state->data.vibration_actuator.type = GamepadHapticActuatorType::kDualRumble; 225 state->data.vibration_actuator.not_null = new_device->SupportsVibration(); 226 227 state->data.connected = true; 228 229 devices_.emplace(location_int, std::move(new_device)); 230} 231 232bool GamepadPlatformDataFetcherMac::DisconnectUnrecognizedGamepad( 233 int source_id) { 234 auto gamepad_iter = devices_.find(source_id); 235 if (gamepad_iter == devices_.end()) 236 return false; 237 gamepad_iter->second->Shutdown(); 238 devices_.erase(gamepad_iter); 239 return true; 240} 241 242void GamepadPlatformDataFetcherMac::DeviceRemove(IOHIDDeviceRef device) { 243 if (!enabled_) 244 return; 245 246 GamepadDeviceMac* gamepad_device = GetGamepadFromHidDevice(device); 247 248 if (!gamepad_device) 249 return; 250 251 gamepad_device->Shutdown(); 252 devices_.erase(gamepad_device->GetLocationId()); 253} 254 255void GamepadPlatformDataFetcherMac::ValueChanged(IOHIDValueRef value) { 256 if (!enabled_ || paused_) 257 return; 258 259 IOHIDElementRef element = IOHIDValueGetElement(value); 260 IOHIDDeviceRef device = IOHIDElementGetDevice(element); 261 262 GamepadDeviceMac* gamepad_device = GetGamepadFromHidDevice(device); 263 264 if (!gamepad_device) 265 return; 266 267 PadState* state = GetPadState(gamepad_device->GetLocationId()); 268 if (!state) 269 return; 270 271 gamepad_device->UpdateGamepadForValue(value, &state->data); 272} 273 274void GamepadPlatformDataFetcherMac::GetGamepadData(bool) { 275 if (!enabled_) 276 return; 277 278 // Loop through and GetPadState to indicate the devices are still connected. 279 for (const auto& iter : devices_) { 280 GetPadState(iter.first); 281 } 282} 283 284void GamepadPlatformDataFetcherMac::PlayEffect( 285 int source_id, 286 mojom::GamepadHapticEffectType type, 287 mojom::GamepadEffectParametersPtr params, 288 mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback, 289 scoped_refptr<base::SequencedTaskRunner> callback_runner) { 290 auto device_iter = devices_.find(source_id); 291 if (device_iter == devices_.end()) { 292 // No connected gamepad with this location. Probably the effect was issued 293 // while the gamepad was still connected, so handle this as if it were 294 // preempted by a disconnect. 295 RunVibrationCallback( 296 std::move(callback), std::move(callback_runner), 297 mojom::GamepadHapticsResult::GamepadHapticsResultPreempted); 298 return; 299 } 300 device_iter->second->PlayEffect(type, std::move(params), std::move(callback), 301 std::move(callback_runner)); 302} 303 304void GamepadPlatformDataFetcherMac::ResetVibration( 305 int source_id, 306 mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback, 307 scoped_refptr<base::SequencedTaskRunner> callback_runner) { 308 auto device_iter = devices_.find(source_id); 309 if (device_iter == devices_.end()) { 310 // No connected gamepad with this location. Since the gamepad is already 311 // disconnected, allow the reset to report success. 312 RunVibrationCallback( 313 std::move(callback), std::move(callback_runner), 314 mojom::GamepadHapticsResult::GamepadHapticsResultComplete); 315 return; 316 } 317 device_iter->second->ResetVibration(std::move(callback), 318 std::move(callback_runner)); 319} 320 321} // namespace device 322