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