Improve use of NeverDestroyed
[WebKit-https.git] / Source / WebCore / platform / gamepad / mac / HIDGamepadProvider.cpp
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) && PLATFORM(MAC)
30
31 #import "GamepadProviderClient.h"
32 #import "Logging.h"
33 #import "PlatformGamepad.h"
34 #import <wtf/NeverDestroyed.h>
35
36 namespace WebCore {
37
38 static const Seconds connectionDelayInterval { 500_ms };
39 static const Seconds inputNotificationDelay { 50_ms };
40
41 static RetainPtr<CFDictionaryRef> deviceMatchingDictionary(uint32_t usagePage, uint32_t usage)
42 {
43     ASSERT(usagePage);
44     ASSERT(usage);
45
46     RetainPtr<CFNumberRef> pageNumber = adoptCF(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usagePage));
47     RetainPtr<CFNumberRef> usageNumber = adoptCF(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage));
48
49     CFStringRef keys[] = { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) };
50     CFNumberRef values[] = { pageNumber.get(), usageNumber.get() };
51
52     return adoptCF(CFDictionaryCreate(kCFAllocatorDefault, (const void**)keys, (const void**)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
53 }
54
55 static void deviceAddedCallback(void* context, IOReturn, void*, IOHIDDeviceRef device)
56 {
57     HIDGamepadProvider* listener = static_cast<HIDGamepadProvider*>(context);
58     listener->deviceAdded(device);
59 }
60
61 static void deviceRemovedCallback(void* context, IOReturn, void*, IOHIDDeviceRef device)
62 {
63     HIDGamepadProvider* listener = static_cast<HIDGamepadProvider*>(context);
64     listener->deviceRemoved(device);
65 }
66
67 static void deviceValuesChangedCallback(void* context, IOReturn result, void*, IOHIDValueRef value)
68 {
69     // A non-zero result value indicates an error that we can do nothing about for input values.
70     if (result)
71         return;
72
73     HIDGamepadProvider* listener = static_cast<HIDGamepadProvider*>(context);
74     listener->valuesChanged(value);
75 }
76
77 HIDGamepadProvider& HIDGamepadProvider::singleton()
78 {
79     static NeverDestroyed<HIDGamepadProvider> sharedListener;
80     return sharedListener;
81 }
82
83 HIDGamepadProvider::HIDGamepadProvider()
84     : m_shouldDispatchCallbacks(false)
85     , m_connectionDelayTimer(*this, &HIDGamepadProvider::connectionDelayTimerFired)
86     , m_inputNotificationTimer(*this, &HIDGamepadProvider::inputNotificationTimerFired)
87 {
88     m_manager = adoptCF(IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone));
89
90     RetainPtr<CFDictionaryRef> joystickDictionary = deviceMatchingDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick);
91     RetainPtr<CFDictionaryRef> gamepadDictionary = deviceMatchingDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad);
92
93     CFDictionaryRef devices[] = { joystickDictionary.get(), gamepadDictionary.get() };
94
95     RetainPtr<CFArrayRef> matchingArray = adoptCF(CFArrayCreate(kCFAllocatorDefault, (const void**)devices, 2, &kCFTypeArrayCallBacks));
96
97     IOHIDManagerSetDeviceMatchingMultiple(m_manager.get(), matchingArray.get());
98     IOHIDManagerRegisterDeviceMatchingCallback(m_manager.get(), deviceAddedCallback, this);
99     IOHIDManagerRegisterDeviceRemovalCallback(m_manager.get(), deviceRemovedCallback, this);
100
101     startMonitoringInput();
102 }
103
104 void HIDGamepadProvider::stopMonitoringInput()
105 {
106 #pragma clang diagnostic push
107 #pragma clang diagnostic ignored "-Wnonnull"
108     IOHIDManagerRegisterInputValueCallback(m_manager.get(), nullptr, nullptr);
109 #pragma clang diagnostic pop
110 }
111
112 void HIDGamepadProvider::startMonitoringInput()
113 {
114     IOHIDManagerRegisterInputValueCallback(m_manager.get(), deviceValuesChangedCallback, this);
115 }
116
117 unsigned HIDGamepadProvider::indexForNewlyConnectedDevice()
118 {
119     unsigned index = 0;
120     while (index < m_gamepadVector.size() && m_gamepadVector[index])
121         ++index;
122
123     return index;
124 }
125
126 void HIDGamepadProvider::connectionDelayTimerFired()
127 {
128     m_shouldDispatchCallbacks = true;
129
130     for (auto* client : m_clients)
131         client->setInitialConnectedGamepads(m_gamepadVector);
132 }
133
134 void HIDGamepadProvider::openAndScheduleManager()
135 {
136     LOG(Gamepad, "HIDGamepadProvider opening/scheduling HID manager");
137
138     ASSERT(m_gamepadVector.isEmpty());
139     ASSERT(m_gamepadMap.isEmpty());
140
141     m_shouldDispatchCallbacks = false;
142
143     IOHIDManagerScheduleWithRunLoop(m_manager.get(), CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
144     IOHIDManagerOpen(m_manager.get(), kIOHIDOptionsTypeNone);
145
146     // Any connections we are notified of within the connectionDelayInterval of listening likely represent
147     // devices that were already connected, so we suppress notifying clients of these.
148     m_connectionDelayTimer.startOneShot(connectionDelayInterval);
149 }
150
151 void HIDGamepadProvider::closeAndUnscheduleManager()
152 {
153     LOG(Gamepad, "HIDGamepadProvider closing/unscheduling HID manager");
154
155     IOHIDManagerUnscheduleFromRunLoop(m_manager.get(), CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
156     IOHIDManagerClose(m_manager.get(), kIOHIDOptionsTypeNone);
157
158     m_gamepadVector.clear();
159     m_gamepadMap.clear();
160
161     m_connectionDelayTimer.stop();
162 }
163
164 void HIDGamepadProvider::startMonitoringGamepads(GamepadProviderClient& client)
165 {
166     bool shouldOpenAndScheduleManager = m_clients.isEmpty();
167
168     ASSERT(!m_clients.contains(&client));
169     m_clients.add(&client);
170
171     if (shouldOpenAndScheduleManager)
172         openAndScheduleManager();
173 }
174 void HIDGamepadProvider::stopMonitoringGamepads(GamepadProviderClient& client)
175 {
176     ASSERT(m_clients.contains(&client));
177
178     bool shouldCloseAndUnscheduleManager = m_clients.remove(&client) && m_clients.isEmpty();
179
180     if (shouldCloseAndUnscheduleManager)
181         closeAndUnscheduleManager();
182 }
183
184 void HIDGamepadProvider::deviceAdded(IOHIDDeviceRef device)
185 {
186     ASSERT(!m_gamepadMap.get(device));
187
188     LOG(Gamepad, "HIDGamepadProvider device %p added", device);
189
190     unsigned index = indexForNewlyConnectedDevice();
191     std::unique_ptr<HIDGamepad> gamepad = std::make_unique<HIDGamepad>(device, index);
192
193     if (m_gamepadVector.size() <= index)
194         m_gamepadVector.resize(index + 1);
195
196     m_gamepadVector[index] = gamepad.get();
197     m_gamepadMap.set(device, WTFMove(gamepad));
198
199     if (!m_shouldDispatchCallbacks) {
200         // This added device is the result of us starting to monitor gamepads.
201         // We'll get notified of all connected devices during this current spin of the runloop
202         // and we don't want to tell the client about any of them.
203         // The m_connectionDelayTimer fires in a subsequent spin of the runloop after which
204         // any connection events are actual new devices.
205         m_connectionDelayTimer.startOneShot(0_s);
206
207         LOG(Gamepad, "Device %p was added while suppressing callbacks, so this should be an 'already connected' event", device);
208
209         return;
210     }
211
212     for (auto& client : m_clients)
213         client->platformGamepadConnected(*m_gamepadVector[index]);
214 }
215
216 void HIDGamepadProvider::deviceRemoved(IOHIDDeviceRef device)
217 {
218     LOG(Gamepad, "HIDGamepadProvider device %p removed", device);
219
220     std::unique_ptr<HIDGamepad> removedGamepad = removeGamepadForDevice(device);
221     ASSERT(removedGamepad);
222
223     // Any time we get a device removed callback we know it's a real event and not an 'already connected' event.
224     // We should always stop suppressing callbacks when we receive such an event.
225     m_shouldDispatchCallbacks = true;
226
227     for (auto& client : m_clients)
228         client->platformGamepadDisconnected(*removedGamepad);
229 }
230
231 void HIDGamepadProvider::valuesChanged(IOHIDValueRef value)
232 {
233     IOHIDDeviceRef device = IOHIDElementGetDevice(IOHIDValueGetElement(value));
234
235     HIDGamepad* gamepad = m_gamepadMap.get(device);
236
237     // When starting monitoring we might get a value changed callback before we even know the device is connected.
238     if (!gamepad)
239         return;
240
241     if (gamepad->valueChanged(value) == HIDInputType::ButtonPress)
242         setShouldMakeGamepadsVisibile();
243
244     // This isActive check is necessary as we want to delay input notifications from the time of the first input,
245     // and not push the notification out on every subsequent input.
246     if (!m_inputNotificationTimer.isActive())
247         m_inputNotificationTimer.startOneShot(inputNotificationDelay);
248 }
249
250 void HIDGamepadProvider::inputNotificationTimerFired()
251 {
252     if (!m_shouldDispatchCallbacks)
253         return;
254
255     dispatchPlatformGamepadInputActivity();
256 }
257
258 std::unique_ptr<HIDGamepad> HIDGamepadProvider::removeGamepadForDevice(IOHIDDeviceRef device)
259 {
260     std::unique_ptr<HIDGamepad> result = m_gamepadMap.take(device);
261     ASSERT(result);
262
263     auto i = m_gamepadVector.find(result.get());
264     if (i != notFound)
265         m_gamepadVector[i] = nullptr;
266
267     return result;
268 }
269
270 } // namespace WebCore
271
272 #endif // ENABLE(GAMEPAD) && PLATFORM(MAC)