gecko-dev/dom/gamepad/GamepadManager.cpp
Tim Huang 226c880153 Bug 1885936 - Part 1: Introduce a chrome only function for getting all gamepads. r=cmartin,webidl,emilio
To collect the gamepad information on the user device, we introduce a
chrome only function called RequestAllGamepads to Navigator. The
function will return a promise which resolves when we get all gamepads
info from the parent process.

Note that we cannot rely on The existing navigator.getGamepads()
function because it cannot allow us collecting gamepads info
without the user interacts with the gamepads.

Differential Revision: https://phabricator.services.mozilla.com/D207979
2024-05-15 16:48:58 +00:00

697 lines
21 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/GamepadManager.h"
#include "mozilla/dom/Gamepad.h"
#include "mozilla/dom/GamepadAxisMoveEvent.h"
#include "mozilla/dom/GamepadButtonEvent.h"
#include "mozilla/dom/GamepadEvent.h"
#include "mozilla/dom/GamepadEventChannelChild.h"
#include "mozilla/dom/GamepadMonitoring.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "nsContentUtils.h"
#include "nsGlobalWindowInner.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsThreadUtils.h"
#include "VRManagerChild.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"
#include <cstddef>
using namespace mozilla::ipc;
namespace mozilla::dom {
namespace {
const nsTArray<RefPtr<nsGlobalWindowInner>>::index_type NoIndex =
nsTArray<RefPtr<nsGlobalWindowInner>>::NoIndex;
bool sShutdown = false;
StaticRefPtr<GamepadManager> gGamepadManagerSingleton;
// A threshold value of axis move to determine the first
// intent.
const float AXIS_FIRST_INTENT_THRESHOLD_VALUE = 0.1f;
} // namespace
NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver)
GamepadManager::GamepadManager()
: mEnabled(false),
mNonstandardEventsEnabled(false),
mShuttingDown(false),
mPromiseID(0) {}
nsresult GamepadManager::Init() {
mEnabled = StaticPrefs::dom_gamepad_enabled();
mNonstandardEventsEnabled =
StaticPrefs::dom_gamepad_non_standard_events_enabled();
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (NS_WARN_IF(!observerService)) {
return NS_ERROR_FAILURE;
}
nsresult rv;
rv = observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
GamepadManager::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService) {
observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
}
BeginShutdown();
return NS_OK;
}
void GamepadManager::StopMonitoring() {
if (mChannelChild) {
PGamepadEventChannelChild::Send__delete__(mChannelChild);
mChannelChild = nullptr;
}
if (gfx::VRManagerChild::IsCreated()) {
gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
vm->SendControllerListenerRemoved();
}
mGamepads.Clear();
}
void GamepadManager::BeginShutdown() {
mShuttingDown = true;
StopMonitoring();
// Don't let windows call back to unregister during shutdown
for (uint32_t i = 0; i < mListeners.Length(); i++) {
mListeners[i]->SetHasGamepadEventListener(false);
}
mListeners.Clear();
sShutdown = true;
}
void GamepadManager::AddListener(nsGlobalWindowInner* aWindow) {
MOZ_ASSERT(aWindow);
MOZ_ASSERT(NS_IsMainThread());
// IPDL child has not been created
if (!mChannelChild) {
PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actor)) {
// We are probably shutting down.
return;
}
RefPtr<GamepadEventChannelChild> child(GamepadEventChannelChild::Create());
if (!actor->SendPGamepadEventChannelConstructor(child.get())) {
// We are probably shutting down.
return;
}
mChannelChild = child;
if (gfx::VRManagerChild::IsCreated()) {
// Construct VRManagerChannel and ask adding the connected
// VR controllers to GamepadManager
gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
vm->SendControllerListenerAdded();
}
}
if (!mEnabled || mShuttingDown ||
aWindow->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
return;
}
if (mListeners.IndexOf(aWindow) != NoIndex) {
return; // already exists
}
mListeners.AppendElement(aWindow);
}
void GamepadManager::RemoveListener(nsGlobalWindowInner* aWindow) {
MOZ_ASSERT(aWindow);
if (mShuttingDown) {
// Doesn't matter at this point. It's possible we're being called
// as a result of our own destructor here, so just bail out.
return;
}
if (mListeners.IndexOf(aWindow) == NoIndex) {
return; // doesn't exist
}
for (const auto& key : mGamepads.Keys()) {
aWindow->RemoveGamepad(key);
}
mListeners.RemoveElement(aWindow);
if (mListeners.IsEmpty()) {
StopMonitoring();
}
}
already_AddRefed<Gamepad> GamepadManager::GetGamepad(
GamepadHandle aHandle) const {
RefPtr<Gamepad> gamepad;
if (mGamepads.Get(aHandle, getter_AddRefs(gamepad))) {
return gamepad.forget();
}
return nullptr;
}
void GamepadManager::AddGamepad(GamepadHandle aHandle, const nsAString& aId,
GamepadMappingType aMapping, GamepadHand aHand,
uint32_t aDisplayID, uint32_t aNumButtons,
uint32_t aNumAxes, uint32_t aNumHaptics,
uint32_t aNumLightIndicator,
uint32_t aNumTouchEvents) {
// TODO: bug 852258: get initial button/axis state
RefPtr<Gamepad> newGamepad =
new Gamepad(nullptr, aId,
0, // index is set by global window
aHandle, aMapping, aHand, aDisplayID, aNumButtons, aNumAxes,
aNumHaptics, aNumLightIndicator, aNumTouchEvents);
// We store the gamepad related to its index given by the parent process,
// and no duplicate index is allowed.
MOZ_ASSERT(!mGamepads.Contains(aHandle));
mGamepads.InsertOrUpdate(aHandle, std::move(newGamepad));
NewConnectionEvent(aHandle, true);
}
void GamepadManager::RemoveGamepad(GamepadHandle aHandle) {
RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
if (!gamepad) {
NS_WARNING("Trying to delete gamepad with invalid index");
return;
}
gamepad->SetConnected(false);
NewConnectionEvent(aHandle, false);
mGamepads.Remove(aHandle);
}
void GamepadManager::FireButtonEvent(EventTarget* aTarget, Gamepad* aGamepad,
uint32_t aButton, double aValue) {
nsString name =
aValue == 1.0L ? u"gamepadbuttondown"_ns : u"gamepadbuttonup"_ns;
GamepadButtonEventInit init;
init.mBubbles = false;
init.mCancelable = false;
init.mGamepad = aGamepad;
init.mButton = aButton;
RefPtr<GamepadButtonEvent> event =
GamepadButtonEvent::Constructor(aTarget, name, init);
event->SetTrusted(true);
aTarget->DispatchEvent(*event);
}
void GamepadManager::FireAxisMoveEvent(EventTarget* aTarget, Gamepad* aGamepad,
uint32_t aAxis, double aValue) {
GamepadAxisMoveEventInit init;
init.mBubbles = false;
init.mCancelable = false;
init.mGamepad = aGamepad;
init.mAxis = aAxis;
init.mValue = aValue;
RefPtr<GamepadAxisMoveEvent> event =
GamepadAxisMoveEvent::Constructor(aTarget, u"gamepadaxismove"_ns, init);
event->SetTrusted(true);
aTarget->DispatchEvent(*event);
}
void GamepadManager::NewConnectionEvent(GamepadHandle aHandle,
bool aConnected) {
if (mShuttingDown) {
return;
}
RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
if (!gamepad) {
return;
}
// Hold on to listeners in a separate array because firing events
// can mutate the mListeners array.
nsTArray<RefPtr<nsGlobalWindowInner>> listeners(mListeners.Clone());
if (aConnected) {
for (uint32_t i = 0; i < listeners.Length(); i++) {
#ifdef NIGHTLY_BUILD
// Don't fire a gamepadconnected event unless it's a secure context
if (!listeners[i]->IsSecureContext()) {
continue;
}
#endif
// Do not fire gamepadconnected and gamepaddisconnected events when
// privacy.resistFingerprinting is true.
if (listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
continue;
}
// Only send events to non-background windows
if (!listeners[i]->IsCurrentInnerWindow() ||
listeners[i]->GetOuterWindow()->IsBackground()) {
continue;
}
// We don't fire a connected event here unless the window
// has seen input from at least one device.
if (!listeners[i]->HasSeenGamepadInput()) {
continue;
}
SetWindowHasSeenGamepad(listeners[i], aHandle);
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aHandle);
if (listenerGamepad) {
// Fire event
FireConnectionEvent(listeners[i], listenerGamepad, aConnected);
}
}
} else {
// For disconnection events, fire one at every window that has received
// data from this gamepad.
for (uint32_t i = 0; i < listeners.Length(); i++) {
// Even background windows get these events, so we don't have to
// deal with the hassle of syncing the state of removed gamepads.
// Do not fire gamepadconnected and gamepaddisconnected events when
// privacy.resistFingerprinting is true.
if (listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
continue;
}
if (WindowHasSeenGamepad(listeners[i], aHandle)) {
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aHandle);
if (listenerGamepad) {
listenerGamepad->SetConnected(false);
// Fire event
FireConnectionEvent(listeners[i], listenerGamepad, false);
listeners[i]->RemoveGamepad(aHandle);
}
}
}
}
}
void GamepadManager::FireConnectionEvent(EventTarget* aTarget,
Gamepad* aGamepad, bool aConnected) {
nsString name =
aConnected ? u"gamepadconnected"_ns : u"gamepaddisconnected"_ns;
GamepadEventInit init;
init.mBubbles = false;
init.mCancelable = false;
init.mGamepad = aGamepad;
RefPtr<GamepadEvent> event = GamepadEvent::Constructor(aTarget, name, init);
event->SetTrusted(true);
aTarget->DispatchEvent(*event);
}
void GamepadManager::SyncGamepadState(GamepadHandle aHandle,
nsGlobalWindowInner* aWindow,
Gamepad* aGamepad) {
if (mShuttingDown || !mEnabled ||
aWindow->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
return;
}
RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
if (!gamepad) {
return;
}
aGamepad->SyncState(gamepad);
}
// static
bool GamepadManager::IsServiceRunning() { return !!gGamepadManagerSingleton; }
// static
already_AddRefed<GamepadManager> GamepadManager::GetService() {
if (sShutdown) {
return nullptr;
}
if (!gGamepadManagerSingleton) {
RefPtr<GamepadManager> manager = new GamepadManager();
nsresult rv = manager->Init();
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
gGamepadManagerSingleton = manager;
ClearOnShutdown(&gGamepadManagerSingleton);
}
RefPtr<GamepadManager> service(gGamepadManagerSingleton);
return service.forget();
}
bool GamepadManager::AxisMoveIsFirstIntent(nsGlobalWindowInner* aWindow,
GamepadHandle aHandle,
const GamepadChangeEvent& aEvent) {
const GamepadChangeEventBody& body = aEvent.body();
if (!WindowHasSeenGamepad(aWindow, aHandle) &&
body.type() == GamepadChangeEventBody::TGamepadAxisInformation) {
// Some controllers would send small axis values even they are just idle.
// To avoid controllers be activated without its first intent.
const GamepadAxisInformation& a = body.get_GamepadAxisInformation();
if (abs(a.value()) < AXIS_FIRST_INTENT_THRESHOLD_VALUE) {
return false;
}
}
return true;
}
bool GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindowInner* aWindow,
GamepadHandle aHandle) {
if (!WindowHasSeenGamepad(aWindow, aHandle)) {
// This window hasn't seen this gamepad before, so
// send a connection event first.
SetWindowHasSeenGamepad(aWindow, aHandle);
return false;
}
return true;
}
bool GamepadManager::WindowHasSeenGamepad(nsGlobalWindowInner* aWindow,
GamepadHandle aHandle) const {
RefPtr<Gamepad> gamepad = aWindow->GetGamepad(aHandle);
return gamepad != nullptr;
}
void GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindowInner* aWindow,
GamepadHandle aHandle,
bool aHasSeen) {
MOZ_ASSERT(aWindow);
if (mListeners.IndexOf(aWindow) == NoIndex) {
// This window isn't even listening for gamepad events.
return;
}
if (aHasSeen) {
aWindow->SetHasSeenGamepadInput(true);
nsCOMPtr<nsISupports> window = ToSupports(aWindow);
RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
if (!gamepad) {
return;
}
RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window);
aWindow->AddGamepad(aHandle, clonedGamepad);
} else {
aWindow->RemoveGamepad(aHandle);
}
}
void GamepadManager::Update(const GamepadChangeEvent& aEvent) {
if (!mEnabled || mShuttingDown) {
return;
}
const GamepadHandle handle = aEvent.handle();
GamepadChangeEventBody body = aEvent.body();
if (body.type() == GamepadChangeEventBody::TGamepadAdded) {
const GamepadAdded& a = body.get_GamepadAdded();
AddGamepad(handle, a.id(), static_cast<GamepadMappingType>(a.mapping()),
static_cast<GamepadHand>(a.hand()), a.display_id(),
a.num_buttons(), a.num_axes(), a.num_haptics(), a.num_lights(),
a.num_touches());
return;
}
if (body.type() == GamepadChangeEventBody::TGamepadRemoved) {
RemoveGamepad(handle);
return;
}
if (!SetGamepadByEvent(aEvent)) {
return;
}
// Hold on to listeners in a separate array because firing events
// can mutate the mListeners array.
nsTArray<RefPtr<nsGlobalWindowInner>> listeners(mListeners.Clone());
for (uint32_t i = 0; i < listeners.Length(); i++) {
// Only send events to non-background windows
if (!listeners[i]->IsCurrentInnerWindow() ||
listeners[i]->GetOuterWindow()->IsBackground() ||
listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
continue;
}
SetGamepadByEvent(aEvent, listeners[i]);
MaybeConvertToNonstandardGamepadEvent(aEvent, listeners[i]);
}
}
void GamepadManager::MaybeConvertToNonstandardGamepadEvent(
const GamepadChangeEvent& aEvent, nsGlobalWindowInner* aWindow) {
MOZ_ASSERT(aWindow);
if (!mNonstandardEventsEnabled) {
return;
}
GamepadHandle handle = aEvent.handle();
RefPtr<Gamepad> gamepad = aWindow->GetGamepad(handle);
const GamepadChangeEventBody& body = aEvent.body();
if (gamepad) {
switch (body.type()) {
case GamepadChangeEventBody::TGamepadButtonInformation: {
const GamepadButtonInformation& a = body.get_GamepadButtonInformation();
FireButtonEvent(aWindow, gamepad, a.button(), a.value());
break;
}
case GamepadChangeEventBody::TGamepadAxisInformation: {
const GamepadAxisInformation& a = body.get_GamepadAxisInformation();
FireAxisMoveEvent(aWindow, gamepad, a.axis(), a.value());
break;
}
default:
break;
}
}
}
bool GamepadManager::SetGamepadByEvent(const GamepadChangeEvent& aEvent,
nsGlobalWindowInner* aWindow) {
bool ret = false;
bool firstTime = false;
GamepadHandle handle = aEvent.handle();
if (aWindow) {
if (!AxisMoveIsFirstIntent(aWindow, handle, aEvent)) {
return false;
}
firstTime = !MaybeWindowHasSeenGamepad(aWindow, handle);
}
RefPtr<Gamepad> gamepad =
aWindow ? aWindow->GetGamepad(handle) : GetGamepad(handle);
const GamepadChangeEventBody& body = aEvent.body();
if (gamepad) {
switch (body.type()) {
case GamepadChangeEventBody::TGamepadButtonInformation: {
const GamepadButtonInformation& a = body.get_GamepadButtonInformation();
gamepad->SetButton(a.button(), a.pressed(), a.touched(), a.value());
break;
}
case GamepadChangeEventBody::TGamepadAxisInformation: {
const GamepadAxisInformation& a = body.get_GamepadAxisInformation();
gamepad->SetAxis(a.axis(), a.value());
break;
}
case GamepadChangeEventBody::TGamepadPoseInformation: {
const GamepadPoseInformation& a = body.get_GamepadPoseInformation();
gamepad->SetPose(a.pose_state());
break;
}
case GamepadChangeEventBody::TGamepadLightIndicatorTypeInformation: {
const GamepadLightIndicatorTypeInformation& a =
body.get_GamepadLightIndicatorTypeInformation();
gamepad->SetLightIndicatorType(a.light(), a.type());
break;
}
case GamepadChangeEventBody::TGamepadTouchInformation: {
// Avoid GamepadTouch's touchId be accessed in cross-origin tracking.
for (uint32_t i = 0; i < mListeners.Length(); i++) {
RefPtr<Gamepad> listenerGamepad = mListeners[i]->GetGamepad(handle);
if (listenerGamepad && mListeners[i]->IsCurrentInnerWindow() &&
!mListeners[i]->GetOuterWindow()->IsBackground()) {
const GamepadTouchInformation& a =
body.get_GamepadTouchInformation();
listenerGamepad->SetTouchEvent(a.index(), a.touch_state());
}
}
break;
}
case GamepadChangeEventBody::TGamepadHandInformation: {
const GamepadHandInformation& a = body.get_GamepadHandInformation();
gamepad->SetHand(a.hand());
break;
}
default:
MOZ_ASSERT(false);
break;
}
ret = true;
}
if (aWindow && firstTime) {
FireConnectionEvent(aWindow, gamepad, true);
}
return ret;
}
already_AddRefed<Promise> GamepadManager::VibrateHaptic(
GamepadHandle aHandle, uint32_t aHapticIndex, double aIntensity,
double aDuration, nsIGlobalObject* aGlobal, ErrorResult& aRv) {
RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
if (NS_WARN_IF(aRv.Failed())) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
if (StaticPrefs::dom_gamepad_haptic_feedback_enabled()) {
if (aHandle.GetKind() == GamepadHandleKind::VR) {
if (gfx::VRManagerChild::IsCreated()) {
gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
vm->AddPromise(mPromiseID, promise);
vm->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity, aDuration,
mPromiseID);
}
} else {
if (mChannelChild) {
mChannelChild->AddPromise(mPromiseID, promise);
mChannelChild->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity,
aDuration, mPromiseID);
}
}
}
++mPromiseID;
return promise.forget();
}
void GamepadManager::StopHaptics() {
if (!StaticPrefs::dom_gamepad_haptic_feedback_enabled()) {
return;
}
for (const auto& entry : mGamepads) {
const GamepadHandle handle = entry.GetWeak()->GetHandle();
if (handle.GetKind() == GamepadHandleKind::VR) {
if (gfx::VRManagerChild::IsCreated()) {
gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
vm->SendStopVibrateHaptic(handle);
}
} else {
if (mChannelChild) {
mChannelChild->SendStopVibrateHaptic(handle);
}
}
}
}
already_AddRefed<Promise> GamepadManager::SetLightIndicatorColor(
GamepadHandle aHandle, uint32_t aLightColorIndex, uint8_t aRed,
uint8_t aGreen, uint8_t aBlue, nsIGlobalObject* aGlobal, ErrorResult& aRv) {
RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
if (NS_WARN_IF(aRv.Failed())) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
if (StaticPrefs::dom_gamepad_extensions_lightindicator()) {
MOZ_RELEASE_ASSERT(aHandle.GetKind() != GamepadHandleKind::VR,
"We don't support light indicator in VR.");
if (mChannelChild) {
mChannelChild->AddPromise(mPromiseID, promise);
mChannelChild->SendLightIndicatorColor(aHandle, aLightColorIndex, aRed,
aGreen, aBlue, mPromiseID);
}
}
++mPromiseID;
return promise.forget();
}
already_AddRefed<Promise> GamepadManager::RequestAllGamepads(
nsIGlobalObject* aGlobal, ErrorResult& aRv) {
RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
if (!mChannelChild) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
mChannelChild->SendRequestAllGamepads(
[promise](const nsTArray<GamepadAdded>& aAddedGamepads) {
nsTArray<RefPtr<Gamepad>> gamepads;
for (const auto& addedGamepad : aAddedGamepads) {
RefPtr<Gamepad> gamepad = new Gamepad(
nullptr, addedGamepad.id(), 0, GamepadHandle(),
addedGamepad.mapping(), addedGamepad.hand(),
addedGamepad.display_id(), addedGamepad.num_buttons(),
addedGamepad.num_axes(), addedGamepad.num_haptics(),
addedGamepad.num_lights(), addedGamepad.num_touches());
gamepads.AppendElement(gamepad);
}
promise->MaybeResolve(gamepads);
},
[promise](mozilla::ipc::ResponseRejectReason) {
promise->MaybeReject(NS_ERROR_UNEXPECTED);
});
return promise.forget();
}
} // namespace mozilla::dom