1/* 2 * Copyright (C) 2014 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26#include "config.h" 27#include "HIDGamepadProvider.h" 28 29#if ENABLE(GAMEPAD) 30 31#include "GamepadProviderClient.h" 32#include "Logging.h" 33#include "PlatformGamepad.h" 34 35namespace WebCore { 36 37static const double ConnectionDelayInterval = 0.5; 38static const double InputNotificationDelay = 0.05; 39 40static RetainPtr<CFDictionaryRef> deviceMatchingDictionary(uint32_t usagePage, uint32_t usage) 41{ 42 ASSERT(usagePage); 43 ASSERT(usage); 44 45 RetainPtr<CFNumberRef> pageNumber = adoptCF(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usagePage)); 46 RetainPtr<CFNumberRef> usageNumber = adoptCF(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage)); 47 48 CFStringRef keys[] = { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }; 49 CFNumberRef values[] = { pageNumber.get(), usageNumber.get() }; 50 51 return adoptCF(CFDictionaryCreate(kCFAllocatorDefault, (const void**)keys, (const void**)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); 52} 53 54static void deviceAddedCallback(void* context, IOReturn, void*, IOHIDDeviceRef device) 55{ 56 HIDGamepadProvider* listener = static_cast<HIDGamepadProvider*>(context); 57 listener->deviceAdded(device); 58} 59 60static void deviceRemovedCallback(void* context, IOReturn, void*, IOHIDDeviceRef device) 61{ 62 HIDGamepadProvider* listener = static_cast<HIDGamepadProvider*>(context); 63 listener->deviceRemoved(device); 64} 65 66static void deviceValuesChangedCallback(void* context, IOReturn result, void*, IOHIDValueRef value) 67{ 68 // A non-zero result value indicates an error that we can do nothing about for input values. 69 if (result) 70 return; 71 72 HIDGamepadProvider* listener = static_cast<HIDGamepadProvider*>(context); 73 listener->valuesChanged(value); 74} 75 76HIDGamepadProvider& HIDGamepadProvider::shared() 77{ 78 static NeverDestroyed<HIDGamepadProvider> sharedListener; 79 return sharedListener; 80} 81 82HIDGamepadProvider::HIDGamepadProvider() 83 : m_shouldDispatchCallbacks(false) 84 , m_connectionDelayTimer(this, &HIDGamepadProvider::connectionDelayTimerFired) 85 , m_inputNotificationTimer(this, &HIDGamepadProvider::inputNotificationTimerFired) 86{ 87 m_manager = adoptCF(IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone)); 88 89 RetainPtr<CFDictionaryRef> joystickDictionary = deviceMatchingDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick); 90 RetainPtr<CFDictionaryRef> gamepadDictionary = deviceMatchingDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad); 91 92 CFDictionaryRef devices[] = { joystickDictionary.get(), gamepadDictionary.get() }; 93 94 RetainPtr<CFArrayRef> matchingArray = adoptCF(CFArrayCreate(kCFAllocatorDefault, (const void**)devices, 2, &kCFTypeArrayCallBacks)); 95 96 IOHIDManagerSetDeviceMatchingMultiple(m_manager.get(), matchingArray.get()); 97 IOHIDManagerRegisterDeviceMatchingCallback(m_manager.get(), deviceAddedCallback, this); 98 IOHIDManagerRegisterDeviceRemovalCallback(m_manager.get(), deviceRemovedCallback, this); 99 IOHIDManagerRegisterInputValueCallback(m_manager.get(), deviceValuesChangedCallback, this); 100} 101 102unsigned HIDGamepadProvider::indexForNewlyConnectedDevice() 103{ 104 unsigned index = 0; 105 while (index < m_gamepadVector.size() && m_gamepadVector[index]) 106 ++index; 107 108 return index; 109} 110 111void HIDGamepadProvider::connectionDelayTimerFired(Timer<HIDGamepadProvider>&) 112{ 113 m_shouldDispatchCallbacks = true; 114} 115 116void HIDGamepadProvider::openAndScheduleManager() 117{ 118 LOG(Gamepad, "HIDGamepadProvider opening/scheduling HID manager"); 119 120 ASSERT(m_gamepadVector.isEmpty()); 121 ASSERT(m_gamepadMap.isEmpty()); 122 123 m_shouldDispatchCallbacks = false; 124 125 IOHIDManagerScheduleWithRunLoop(m_manager.get(), CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 126 IOHIDManagerOpen(m_manager.get(), kIOHIDOptionsTypeNone); 127 128 // Any connections we are notified of within the ConnectionDelayInterval of listening likely represent 129 // devices that were already connected, so we suppress notifying clients of these. 130 m_connectionDelayTimer.startOneShot(ConnectionDelayInterval); 131} 132 133void HIDGamepadProvider::closeAndUnscheduleManager() 134{ 135 LOG(Gamepad, "HIDGamepadProvider closing/unscheduling HID manager"); 136 137 IOHIDManagerUnscheduleFromRunLoop(m_manager.get(), CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 138 IOHIDManagerClose(m_manager.get(), kIOHIDOptionsTypeNone); 139 140 m_gamepadVector.clear(); 141 m_gamepadMap.clear(); 142 143 m_connectionDelayTimer.stop(); 144} 145 146void HIDGamepadProvider::startMonitoringGamepads(GamepadProviderClient* client) 147{ 148 bool shouldOpenAndScheduleManager = m_clients.isEmpty(); 149 150 ASSERT(!m_clients.contains(client)); 151 m_clients.add(client); 152 153 if (shouldOpenAndScheduleManager) 154 openAndScheduleManager(); 155} 156void HIDGamepadProvider::stopMonitoringGamepads(GamepadProviderClient* client) 157{ 158 ASSERT(m_clients.contains(client)); 159 160 bool shouldCloseAndUnscheduleManager = m_clients.remove(client) && m_clients.isEmpty(); 161 162 if (shouldCloseAndUnscheduleManager) 163 closeAndUnscheduleManager(); 164} 165 166void HIDGamepadProvider::deviceAdded(IOHIDDeviceRef device) 167{ 168 ASSERT(!m_gamepadMap.get(device)); 169 170 LOG(Gamepad, "HIDGamepadProvider device %p added", device); 171 172 unsigned index = indexForNewlyConnectedDevice(); 173 std::unique_ptr<HIDGamepad> gamepad = std::make_unique<HIDGamepad>(device, index); 174 175 if (m_gamepadVector.size() <= index) 176 m_gamepadVector.resize(index + 1); 177 178 m_gamepadVector[index] = gamepad.get(); 179 m_gamepadMap.set(device, WTF::move(gamepad)); 180 181 if (!m_shouldDispatchCallbacks) { 182 // This added device is the result of us starting to monitor gamepads. 183 // We'll get notified of all connected devices during this current spin of the runloop 184 // and we don't want to tell the client about any of them. 185 // The m_connectionDelayTimer fires in a subsequent spin of the runloop after which 186 // any connection events are actual new devices. 187 m_connectionDelayTimer.startOneShot(0); 188 189 LOG(Gamepad, "Device %p was added while suppressing callbacks, so this should be an 'already connected' event", device); 190 191 return; 192 } 193 194 for (auto& client : m_clients) 195 client->platformGamepadConnected(*m_gamepadVector[index]); 196} 197 198void HIDGamepadProvider::deviceRemoved(IOHIDDeviceRef device) 199{ 200 LOG(Gamepad, "HIDGamepadProvider device %p removed", device); 201 202 std::unique_ptr<HIDGamepad> removedGamepad = removeGamepadForDevice(device); 203 ASSERT(removedGamepad); 204 205 // Any time we get a device removed callback we know it's a real event and not an 'already connected' event. 206 // We should always stop supressing callbacks when we receive such an event. 207 m_shouldDispatchCallbacks = true; 208 209 for (auto& client : m_clients) 210 client->platformGamepadDisconnected(*removedGamepad); 211} 212 213void HIDGamepadProvider::valuesChanged(IOHIDValueRef value) 214{ 215 IOHIDDeviceRef device = IOHIDElementGetDevice(IOHIDValueGetElement(value)); 216 217 HIDGamepad* gamepad = m_gamepadMap.get(device); 218 219 // When starting monitoring we might get a value changed callback before we even know the device is connected. 220 if (!gamepad) 221 return; 222 223 gamepad->valueChanged(value); 224 225 // This isActive check is necessary as we want to delay input notifications from the time of the first input, 226 // and not push the notification out on every subsequent input. 227 if (!m_inputNotificationTimer.isActive()) 228 m_inputNotificationTimer.startOneShot(InputNotificationDelay); 229} 230 231void HIDGamepadProvider::inputNotificationTimerFired(Timer<HIDGamepadProvider>&) 232{ 233 if (!m_shouldDispatchCallbacks) 234 return; 235 236 for (auto& client : m_clients) 237 client->platformGamepadInputActivity(); 238} 239 240std::unique_ptr<HIDGamepad> HIDGamepadProvider::removeGamepadForDevice(IOHIDDeviceRef device) 241{ 242 std::unique_ptr<HIDGamepad> result = m_gamepadMap.take(device); 243 ASSERT(result); 244 245 auto i = m_gamepadVector.find(result.get()); 246 if (i != notFound) 247 m_gamepadVector[i] = nullptr; 248 249 return result; 250} 251 252} // namespace WebCore 253 254#endif // ENABLE(GAMEPAD) 255