// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "device/gamepad/xbox_data_fetcher_mac.h"
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/usb/USB.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <string>
#include "base/apple/foundation_util.h"
#include "base/containers/fixed_flat_set.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "device/gamepad/gamepad_id_list.h"
namespace device {
namespace {
// XboxDataFetcher recognizes the following devices connected over USB.
constexpr auto kSupportedDeviceIds = base::MakeFixedFlatSet<GamepadId>({
GamepadId::kAcerProduct1304, // Acer GC501 (X-INPUT mode)
GamepadId::kAcerProduct1305, // Acer Nitro (X-INPUT mode)
GamepadId::kAmazonProduct041a, // Amazon Luna Controller
GamepadId::kMicrosoftProduct028e, // Xbox 360
GamepadId::kMicrosoftProduct02d1, // Xbox One
GamepadId::kMicrosoftProduct02dd, // Xbox One, 2015 firmware
GamepadId::kMicrosoftProduct02e3, // Xbox Elite
GamepadId::kMicrosoftProduct02ea, // Xbox One S
GamepadId::kMicrosoftProduct0b00, // Xbox Elite 2
GamepadId::kMicrosoftProduct0b0a, // Xbox Adaptive
GamepadId::kMicrosoftProduct0b12, // Xbox Series X
GamepadId::kSteelSeriesProduct1430, // Stratus Duo receiver
GamepadId::kSteelSeriesProduct1431, // Stratus Duo
});
} // namespace
XboxDataFetcher::PendingController::PendingController(
XboxDataFetcher* fetcher,
std::unique_ptr<XboxControllerMac> controller)
: fetcher(fetcher), controller(std::move(controller)) {}
XboxDataFetcher::PendingController::~PendingController() {
if (controller)
controller->Shutdown();
}
XboxDataFetcher::XboxDataFetcher() = default;
XboxDataFetcher::~XboxDataFetcher() {
while (!controllers_.empty()) {
RemoveController(*controllers_.begin());
}
UnregisterFromNotifications();
}
GamepadSource XboxDataFetcher::source() {
return Factory::static_source();
}
void XboxDataFetcher::GetGamepadData(bool devices_changed_hint) {
// This just loops through all the connected pads and "pings" them to indicate
// that they're still active.
for (auto* controller : controllers_) {
GetPadState(controller->location_id());
}
}
void XboxDataFetcher::PlayEffect(
int source_id,
mojom::GamepadHapticEffectType type,
mojom::GamepadEffectParametersPtr params,
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback,
scoped_refptr<base::SequencedTaskRunner> callback_runner) {
XboxControllerMac* controller = ControllerForLocation(source_id);
if (!controller) {
RunVibrationCallback(
std::move(callback), std::move(callback_runner),
mojom::GamepadHapticsResult::GamepadHapticsResultError);
return;
}
controller->PlayEffect(type, std::move(params), std::move(callback),
std::move(callback_runner));
}
void XboxDataFetcher::ResetVibration(
int source_id,
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback,
scoped_refptr<base::SequencedTaskRunner> callback_runner) {
XboxControllerMac* controller = ControllerForLocation(source_id);
if (!controller) {
RunVibrationCallback(
std::move(callback), std::move(callback_runner),
mojom::GamepadHapticsResult::GamepadHapticsResultError);
return;
}
controller->ResetVibration(std::move(callback), std::move(callback_runner));
}
void XboxDataFetcher::OnAddedToProvider() {
RegisterForNotifications();
}
// static
void XboxDataFetcher::DeviceAdded(void* context, io_iterator_t iterator) {
DCHECK(context);
XboxDataFetcher* fetcher = static_cast<XboxDataFetcher*>(context);
io_service_t ref;
while ((ref = IOIteratorNext(iterator))) {
base::mac::ScopedIOObject<io_service_t> scoped_ref(ref);
fetcher->TryOpenDevice(ref);
}
}
// static
void XboxDataFetcher::DeviceRemoved(void* context, io_iterator_t iterator) {
DCHECK(context);
XboxDataFetcher* fetcher = static_cast<XboxDataFetcher*>(context);
io_service_t ref;
while ((ref = IOIteratorNext(iterator))) {
base::mac::ScopedIOObject<io_service_t> scoped_ref(ref);
base::apple::ScopedCFTypeRef<CFNumberRef> number(
base::apple::CFCastStrict<CFNumberRef>(IORegistryEntryCreateCFProperty(
ref, CFSTR(kUSBDevicePropertyLocationID), kCFAllocatorDefault,
kNilOptions)));
UInt32 location_id = 0;
CFNumberGetValue(number.get(), kCFNumberSInt32Type, &location_id);
fetcher->RemoveControllerByLocationID(location_id);
}
}
// static
void XboxDataFetcher::InterestCallback(void* context,
io_service_t service,
IOMessage message_type,
void* message_argument) {
if (message_type == kIOMessageServiceWasClosed) {
PendingController* pending = static_cast<PendingController*>(context);
pending->fetcher->PendingControllerBecameAvailable(service, pending);
}
}
void XboxDataFetcher::PendingControllerBecameAvailable(
io_service_t service,
PendingController* pending) {
// Destroying the PendingController object unregisters our interest
// notification.
auto it = pending_controllers_.find(pending);
if (it != pending_controllers_.end()) {
pending_controllers_.erase(it);
}
TryOpenDevice(service);
}
bool XboxDataFetcher::TryOpenDevice(io_service_t service) {
auto pending = std::make_unique<PendingController>(
this, std::make_unique<XboxControllerMac>(this));
bool did_register_interest =
RegisterForInterestNotifications(service, pending.get());
auto* controller = pending->controller.get();
XboxControllerMac::OpenDeviceResult result = controller->OpenDevice(service);
if (result == XboxControllerMac::OpenDeviceResult::OPEN_SUCCEEDED) {
AddController(pending->controller.release());
return true;
}
if (did_register_interest &&
result ==
XboxControllerMac::OpenDeviceResult::OPEN_FAILED_EXCLUSIVE_ACCESS) {
pending_controllers_.insert(std::move(pending));
}
return false;
}
bool XboxDataFetcher::RegisterForNotifications() {
if (listening_)
return true;
if (port_ == nullptr)
port_.reset(IONotificationPortCreate(kIOMasterPortDefault));
if (!port_.is_valid())
return false;
source_ = IONotificationPortGetRunLoopSource(port_.get());
if (!source_)
return false;
CFRunLoopAddSource(CFRunLoopGetCurrent(), source_, kCFRunLoopDefaultMode);
listening_ = true;
for (const auto& entry : kSupportedDeviceIds) {
auto ids = GamepadIdList::Get().GetDeviceIdsFromGamepadId(entry);
if (!RegisterForDeviceNotifications(ids.first, ids.second))
return false;
}
return true;
}
bool XboxDataFetcher::RegisterForDeviceNotifications(int vendor_id,
int product_id) {
base::apple::ScopedCFTypeRef<CFNumberRef> vendor_cf(
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendor_id));
base::apple::ScopedCFTypeRef<CFNumberRef> product_cf(
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &product_id));
base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> matching_dict(
IOServiceMatching(kIOUSBDeviceClassName));
if (!matching_dict)
return false;
CFDictionarySetValue(matching_dict.get(), CFSTR(kUSBVendorID),
vendor_cf.get());
CFDictionarySetValue(matching_dict.get(), CFSTR(kUSBProductID),
product_cf.get());
// IOServiceAddMatchingNotification() releases the dictionary when it's done.
// Retain it before each call to IOServiceAddMatchingNotification to keep
// things balanced.
CFRetain(matching_dict.get());
IOReturn ret;
base::mac::ScopedIOObject<io_iterator_t> added_iterator;
ret = IOServiceAddMatchingNotification(port_.get(), kIOFirstMatchNotification,
matching_dict.get(), DeviceAdded, this,
added_iterator.InitializeInto());
if (ret != kIOReturnSuccess) {
LOG(ERROR) << "Error listening for Xbox controller add events: " << ret;
return false;
}
DeviceAdded(this, added_iterator.get());
device_event_iterators_.push_back(std::move(added_iterator));
CFRetain(matching_dict.get());
base::mac::ScopedIOObject<io_iterator_t> removed_iterator;
ret = IOServiceAddMatchingNotification(
port_.get(), kIOTerminatedNotification, matching_dict.get(),
DeviceRemoved, this, removed_iterator.InitializeInto());
if (ret != kIOReturnSuccess) {
LOG(ERROR) << "Error listening for Xbox controller remove events: " << ret;
return false;
}
DeviceRemoved(this, removed_iterator.get());
device_event_iterators_.push_back(std::move(removed_iterator));
return true;
}
bool XboxDataFetcher::RegisterForInterestNotifications(
io_service_t service,
PendingController* pending) {
if (port_ == nullptr)
port_.reset(IONotificationPortCreate(kIOMasterPortDefault));
if (!port_.is_valid())
return false;
kern_return_t kr = IOServiceAddInterestNotification(
port_.get(), service, kIOGeneralInterest, InterestCallback, pending,
pending->notify.InitializeInto());
return kr == KERN_SUCCESS;
}
void XboxDataFetcher::UnregisterFromNotifications() {
if (!listening_)
return;
listening_ = false;
if (source_)
CFRunLoopSourceInvalidate(source_);
port_.reset();
pending_controllers_.clear();
}
XboxControllerMac* XboxDataFetcher::ControllerForLocation(UInt32 location_id) {
for (std::set<XboxControllerMac*>::iterator i = controllers_.begin();
i != controllers_.end(); ++i) {
if ((*i)->location_id() == location_id)
return *i;
}
return NULL;
}
void XboxDataFetcher::AddController(XboxControllerMac* controller) {
DCHECK(controller);
DCHECK(!ControllerForLocation(controller->location_id()))
<< "Controller with location ID " << controller->location_id()
<< " already exists in the set of controllers.";
PadState* state = GetPadState(controller->location_id());
if (!state) {
delete controller;
return; // No available slot for this device
}
controllers_.insert(controller);
controller->SetLEDPattern((XboxControllerMac::LEDPattern)(
XboxControllerMac::LED_FLASH_TOP_LEFT + controller->location_id()));
GamepadDataFetcher::UpdateGamepadStrings(
controller->product_name(), controller->vendor_id(),
controller->product_id(),
/*has_standard_mapping=*/true, state->data);
state->data.connected = true;
state->data.axes_length = 4;
state->data.buttons_length = 17;
state->data.timestamp = CurrentTimeInMicroseconds();
state->mapper = 0;
state->axis_mask = 0;
state->button_mask = 0;
if (GamepadIdList::Get().HasTriggerRumbleSupport(controller->gamepad_id())) {
state->data.vibration_actuator.type =
GamepadHapticActuatorType::kTriggerRumble;
} else {
state->data.vibration_actuator.type =
GamepadHapticActuatorType::kDualRumble;
}
state->data.vibration_actuator.not_null = controller->SupportsVibration();
}
void XboxDataFetcher::RemoveController(XboxControllerMac* controller) {
DCHECK(controller);
controller->Shutdown();
controllers_.erase(controller);
delete controller;
}
void XboxDataFetcher::RemoveControllerByLocationID(uint32_t location_id) {
XboxControllerMac* controller = NULL;
for (std::set<XboxControllerMac*>::iterator i = controllers_.begin();
i != controllers_.end(); ++i) {
if ((*i)->location_id() == location_id) {
controller = *i;
break;
}
}
if (controller)
RemoveController(controller);
}
void XboxDataFetcher::XboxControllerGotData(
XboxControllerMac* controller,
const XboxControllerMac::Data& data) {
PadState* state = GetPadState(controller->location_id());
if (!state)
return; // No available slot for this device
Gamepad& pad = state->data;
for (size_t i = 0; i < 6; i++) {
pad.buttons[i].pressed = data.buttons[i];
pad.buttons[i].value = data.buttons[i] ? 1.0f : 0.0f;
}
pad.buttons[6].pressed =
data.triggers[0] > GamepadButton::kDefaultButtonPressedThreshold;
pad.buttons[6].value = data.triggers[0];
pad.buttons[7].pressed =
data.triggers[1] > GamepadButton::kDefaultButtonPressedThreshold;
pad.buttons[7].value = data.triggers[1];
for (size_t i = 8; i < 16; i++) {
pad.buttons[i].pressed = data.buttons[i - 2];
pad.buttons[i].value = data.buttons[i - 2] ? 1.0f : 0.0f;
}
if (controller->xinput_type() == kXInputTypeXbox360) {
// Map the Xbox button on Xbox 360 to buttons[16].
pad.buttons[16].pressed = data.buttons[14];
pad.buttons[16].value = data.buttons[14] ? 1.0f : 0.0f;
}
if (controller->gamepad_id() == GamepadId::kMicrosoftProduct0b12) {
// Map the Share button on Xbox Series X to buttons[17].
pad.buttons[17].pressed = data.buttons[14];
pad.buttons[17].value = data.buttons[14] ? 1.0f : 0.0f;
pad.buttons_length = 18;
}
for (size_t i = 0; i < std::size(data.axes); i++) {
pad.axes[i] = data.axes[i];
}
pad.timestamp = CurrentTimeInMicroseconds();
}
void XboxDataFetcher::XboxControllerGotGuideData(XboxControllerMac* controller,
bool guide) {
PadState* state = GetPadState(controller->location_id());
if (!state)
return; // No available slot for this device
Gamepad& pad = state->data;
pad.buttons[16].pressed = guide;
pad.buttons[16].value = guide ? 1.0f : 0.0f;
pad.timestamp = CurrentTimeInMicroseconds();
}
void XboxDataFetcher::XboxControllerError(XboxControllerMac* controller) {
RemoveController(controller);
}
} // namespace device