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