1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/GamepadManager.h"
9 #include "mozilla/dom/Gamepad.h"
10 #include "mozilla/dom/GamepadAxisMoveEvent.h"
11 #include "mozilla/dom/GamepadButtonEvent.h"
12 #include "mozilla/dom/GamepadEvent.h"
13 #include "mozilla/dom/GamepadEventChannelChild.h"
14 #include "mozilla/dom/GamepadMonitoring.h"
15 #include "mozilla/dom/Promise.h"
17 #include "mozilla/ipc/BackgroundChild.h"
18 #include "mozilla/ipc/PBackgroundChild.h"
19 #include "mozilla/ClearOnShutdown.h"
20 #include "mozilla/Preferences.h"
21 #include "mozilla/StaticPrefs_dom.h"
22 #include "mozilla/StaticPtr.h"
24 #include "nsContentUtils.h"
25 #include "nsGlobalWindowInner.h"
26 #include "nsIObserver.h"
27 #include "nsIObserverService.h"
28 #include "nsThreadUtils.h"
29 #include "VRManagerChild.h"
30 #include "mozilla/Services.h"
31 #include "mozilla/Unused.h"
35 using namespace mozilla::ipc
;
37 namespace mozilla::dom
{
41 const nsTArray
<RefPtr
<nsGlobalWindowInner
>>::index_type NoIndex
=
42 nsTArray
<RefPtr
<nsGlobalWindowInner
>>::NoIndex
;
44 bool sShutdown
= false;
46 StaticRefPtr
<GamepadManager
> gGamepadManagerSingleton
;
48 // A threshold value of axis move to determine the first
50 const float AXIS_FIRST_INTENT_THRESHOLD_VALUE
= 0.1f
;
54 NS_IMPL_ISUPPORTS(GamepadManager
, nsIObserver
)
56 GamepadManager::GamepadManager()
58 mNonstandardEventsEnabled(false),
62 nsresult
GamepadManager::Init() {
63 mEnabled
= StaticPrefs::dom_gamepad_enabled();
64 mNonstandardEventsEnabled
=
65 StaticPrefs::dom_gamepad_non_standard_events_enabled();
66 nsCOMPtr
<nsIObserverService
> observerService
=
67 mozilla::services::GetObserverService();
69 if (NS_WARN_IF(!observerService
)) {
70 return NS_ERROR_FAILURE
;
74 rv
= observerService
->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID
,
77 if (NS_WARN_IF(NS_FAILED(rv
))) {
85 GamepadManager::Observe(nsISupports
* aSubject
, const char* aTopic
,
86 const char16_t
* aData
) {
87 nsCOMPtr
<nsIObserverService
> observerService
=
88 mozilla::services::GetObserverService();
89 if (observerService
) {
90 observerService
->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID
);
96 void GamepadManager::StopMonitoring() {
98 PGamepadEventChannelChild::Send__delete__(mChannelChild
);
99 mChannelChild
= nullptr;
101 if (gfx::VRManagerChild::IsCreated()) {
102 gfx::VRManagerChild
* vm
= gfx::VRManagerChild::Get();
103 vm
->SendControllerListenerRemoved();
108 void GamepadManager::BeginShutdown() {
109 mShuttingDown
= true;
111 // Don't let windows call back to unregister during shutdown
112 for (uint32_t i
= 0; i
< mListeners
.Length(); i
++) {
113 mListeners
[i
]->SetHasGamepadEventListener(false);
119 void GamepadManager::AddListener(nsGlobalWindowInner
* aWindow
) {
121 MOZ_ASSERT(NS_IsMainThread());
123 // IPDL child has not been created
124 if (!mChannelChild
) {
125 PBackgroundChild
* actor
= BackgroundChild::GetOrCreateForCurrentThread();
126 if (NS_WARN_IF(!actor
)) {
127 // We are probably shutting down.
131 RefPtr
<GamepadEventChannelChild
> child(GamepadEventChannelChild::Create());
132 if (!actor
->SendPGamepadEventChannelConstructor(child
.get())) {
133 // We are probably shutting down.
137 mChannelChild
= child
;
139 if (gfx::VRManagerChild::IsCreated()) {
140 // Construct VRManagerChannel and ask adding the connected
141 // VR controllers to GamepadManager
142 gfx::VRManagerChild
* vm
= gfx::VRManagerChild::Get();
143 vm
->SendControllerListenerAdded();
147 if (!mEnabled
|| mShuttingDown
||
148 aWindow
->ShouldResistFingerprinting(RFPTarget::Gamepad
)) {
152 if (mListeners
.IndexOf(aWindow
) != NoIndex
) {
153 return; // already exists
156 mListeners
.AppendElement(aWindow
);
159 void GamepadManager::RemoveListener(nsGlobalWindowInner
* aWindow
) {
163 // Doesn't matter at this point. It's possible we're being called
164 // as a result of our own destructor here, so just bail out.
168 if (mListeners
.IndexOf(aWindow
) == NoIndex
) {
169 return; // doesn't exist
172 for (const auto& key
: mGamepads
.Keys()) {
173 aWindow
->RemoveGamepad(key
);
176 mListeners
.RemoveElement(aWindow
);
178 if (mListeners
.IsEmpty()) {
183 already_AddRefed
<Gamepad
> GamepadManager::GetGamepad(
184 GamepadHandle aHandle
) const {
185 RefPtr
<Gamepad
> gamepad
;
186 if (mGamepads
.Get(aHandle
, getter_AddRefs(gamepad
))) {
187 return gamepad
.forget();
193 void GamepadManager::AddGamepad(GamepadHandle aHandle
, const nsAString
& aId
,
194 GamepadMappingType aMapping
, GamepadHand aHand
,
195 uint32_t aDisplayID
, uint32_t aNumButtons
,
196 uint32_t aNumAxes
, uint32_t aNumHaptics
,
197 uint32_t aNumLightIndicator
,
198 uint32_t aNumTouchEvents
) {
199 // TODO: bug 852258: get initial button/axis state
200 RefPtr
<Gamepad
> newGamepad
=
201 new Gamepad(nullptr, aId
,
202 0, // index is set by global window
203 aHandle
, aMapping
, aHand
, aDisplayID
, aNumButtons
, aNumAxes
,
204 aNumHaptics
, aNumLightIndicator
, aNumTouchEvents
);
206 // We store the gamepad related to its index given by the parent process,
207 // and no duplicate index is allowed.
208 MOZ_ASSERT(!mGamepads
.Contains(aHandle
));
209 mGamepads
.InsertOrUpdate(aHandle
, std::move(newGamepad
));
210 NewConnectionEvent(aHandle
, true);
213 void GamepadManager::RemoveGamepad(GamepadHandle aHandle
) {
214 RefPtr
<Gamepad
> gamepad
= GetGamepad(aHandle
);
216 NS_WARNING("Trying to delete gamepad with invalid index");
219 gamepad
->SetConnected(false);
220 NewConnectionEvent(aHandle
, false);
221 mGamepads
.Remove(aHandle
);
224 void GamepadManager::FireButtonEvent(EventTarget
* aTarget
, Gamepad
* aGamepad
,
225 uint32_t aButton
, double aValue
) {
227 aValue
== 1.0L ? u
"gamepadbuttondown"_ns
: u
"gamepadbuttonup"_ns
;
228 GamepadButtonEventInit init
;
229 init
.mBubbles
= false;
230 init
.mCancelable
= false;
231 init
.mGamepad
= aGamepad
;
232 init
.mButton
= aButton
;
233 RefPtr
<GamepadButtonEvent
> event
=
234 GamepadButtonEvent::Constructor(aTarget
, name
, init
);
236 event
->SetTrusted(true);
238 aTarget
->DispatchEvent(*event
);
241 void GamepadManager::FireAxisMoveEvent(EventTarget
* aTarget
, Gamepad
* aGamepad
,
242 uint32_t aAxis
, double aValue
) {
243 GamepadAxisMoveEventInit init
;
244 init
.mBubbles
= false;
245 init
.mCancelable
= false;
246 init
.mGamepad
= aGamepad
;
248 init
.mValue
= aValue
;
249 RefPtr
<GamepadAxisMoveEvent
> event
=
250 GamepadAxisMoveEvent::Constructor(aTarget
, u
"gamepadaxismove"_ns
, init
);
252 event
->SetTrusted(true);
254 aTarget
->DispatchEvent(*event
);
257 void GamepadManager::NewConnectionEvent(GamepadHandle aHandle
,
263 RefPtr
<Gamepad
> gamepad
= GetGamepad(aHandle
);
268 // Hold on to listeners in a separate array because firing events
269 // can mutate the mListeners array.
270 nsTArray
<RefPtr
<nsGlobalWindowInner
>> listeners(mListeners
.Clone());
273 for (uint32_t i
= 0; i
< listeners
.Length(); i
++) {
275 // Don't fire a gamepadconnected event unless it's a secure context
276 if (!listeners
[i
]->IsSecureContext()) {
281 // Do not fire gamepadconnected and gamepaddisconnected events when
282 // privacy.resistFingerprinting is true.
283 if (listeners
[i
]->ShouldResistFingerprinting(RFPTarget::Gamepad
)) {
287 // Only send events to non-background windows
288 if (!listeners
[i
]->IsCurrentInnerWindow() ||
289 listeners
[i
]->GetOuterWindow()->IsBackground()) {
293 // We don't fire a connected event here unless the window
294 // has seen input from at least one device.
295 if (!listeners
[i
]->HasSeenGamepadInput()) {
299 SetWindowHasSeenGamepad(listeners
[i
], aHandle
);
301 RefPtr
<Gamepad
> listenerGamepad
= listeners
[i
]->GetGamepad(aHandle
);
302 if (listenerGamepad
) {
304 FireConnectionEvent(listeners
[i
], listenerGamepad
, aConnected
);
308 // For disconnection events, fire one at every window that has received
309 // data from this gamepad.
310 for (uint32_t i
= 0; i
< listeners
.Length(); i
++) {
311 // Even background windows get these events, so we don't have to
312 // deal with the hassle of syncing the state of removed gamepads.
314 // Do not fire gamepadconnected and gamepaddisconnected events when
315 // privacy.resistFingerprinting is true.
316 if (listeners
[i
]->ShouldResistFingerprinting(RFPTarget::Gamepad
)) {
320 if (WindowHasSeenGamepad(listeners
[i
], aHandle
)) {
321 RefPtr
<Gamepad
> listenerGamepad
= listeners
[i
]->GetGamepad(aHandle
);
322 if (listenerGamepad
) {
323 listenerGamepad
->SetConnected(false);
325 FireConnectionEvent(listeners
[i
], listenerGamepad
, false);
326 listeners
[i
]->RemoveGamepad(aHandle
);
333 void GamepadManager::FireConnectionEvent(EventTarget
* aTarget
,
334 Gamepad
* aGamepad
, bool aConnected
) {
336 aConnected
? u
"gamepadconnected"_ns
: u
"gamepaddisconnected"_ns
;
337 GamepadEventInit init
;
338 init
.mBubbles
= false;
339 init
.mCancelable
= false;
340 init
.mGamepad
= aGamepad
;
341 RefPtr
<GamepadEvent
> event
= GamepadEvent::Constructor(aTarget
, name
, init
);
343 event
->SetTrusted(true);
345 aTarget
->DispatchEvent(*event
);
348 void GamepadManager::SyncGamepadState(GamepadHandle aHandle
,
349 nsGlobalWindowInner
* aWindow
,
351 if (mShuttingDown
|| !mEnabled
||
352 aWindow
->ShouldResistFingerprinting(RFPTarget::Gamepad
)) {
356 RefPtr
<Gamepad
> gamepad
= GetGamepad(aHandle
);
361 aGamepad
->SyncState(gamepad
);
365 bool GamepadManager::IsServiceRunning() { return !!gGamepadManagerSingleton
; }
368 already_AddRefed
<GamepadManager
> GamepadManager::GetService() {
373 if (!gGamepadManagerSingleton
) {
374 RefPtr
<GamepadManager
> manager
= new GamepadManager();
375 nsresult rv
= manager
->Init();
376 if (NS_WARN_IF(NS_FAILED(rv
))) {
379 gGamepadManagerSingleton
= manager
;
380 ClearOnShutdown(&gGamepadManagerSingleton
);
383 RefPtr
<GamepadManager
> service(gGamepadManagerSingleton
);
384 return service
.forget();
387 bool GamepadManager::AxisMoveIsFirstIntent(nsGlobalWindowInner
* aWindow
,
388 GamepadHandle aHandle
,
389 const GamepadChangeEvent
& aEvent
) {
390 const GamepadChangeEventBody
& body
= aEvent
.body();
391 if (!WindowHasSeenGamepad(aWindow
, aHandle
) &&
392 body
.type() == GamepadChangeEventBody::TGamepadAxisInformation
) {
393 // Some controllers would send small axis values even they are just idle.
394 // To avoid controllers be activated without its first intent.
395 const GamepadAxisInformation
& a
= body
.get_GamepadAxisInformation();
396 if (abs(a
.value()) < AXIS_FIRST_INTENT_THRESHOLD_VALUE
) {
403 bool GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindowInner
* aWindow
,
404 GamepadHandle aHandle
) {
405 if (!WindowHasSeenGamepad(aWindow
, aHandle
)) {
406 // This window hasn't seen this gamepad before, so
407 // send a connection event first.
408 SetWindowHasSeenGamepad(aWindow
, aHandle
);
414 bool GamepadManager::WindowHasSeenGamepad(nsGlobalWindowInner
* aWindow
,
415 GamepadHandle aHandle
) const {
416 RefPtr
<Gamepad
> gamepad
= aWindow
->GetGamepad(aHandle
);
417 return gamepad
!= nullptr;
420 void GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindowInner
* aWindow
,
421 GamepadHandle aHandle
,
425 if (mListeners
.IndexOf(aWindow
) == NoIndex
) {
426 // This window isn't even listening for gamepad events.
431 aWindow
->SetHasSeenGamepadInput(true);
432 nsCOMPtr
<nsISupports
> window
= ToSupports(aWindow
);
433 RefPtr
<Gamepad
> gamepad
= GetGamepad(aHandle
);
437 RefPtr
<Gamepad
> clonedGamepad
= gamepad
->Clone(window
);
438 aWindow
->AddGamepad(aHandle
, clonedGamepad
);
440 aWindow
->RemoveGamepad(aHandle
);
444 void GamepadManager::Update(const GamepadChangeEvent
& aEvent
) {
445 if (!mEnabled
|| mShuttingDown
) {
449 const GamepadHandle handle
= aEvent
.handle();
451 GamepadChangeEventBody body
= aEvent
.body();
453 if (body
.type() == GamepadChangeEventBody::TGamepadAdded
) {
454 const GamepadAdded
& a
= body
.get_GamepadAdded();
455 AddGamepad(handle
, a
.id(), static_cast<GamepadMappingType
>(a
.mapping()),
456 static_cast<GamepadHand
>(a
.hand()), a
.display_id(),
457 a
.num_buttons(), a
.num_axes(), a
.num_haptics(), a
.num_lights(),
461 if (body
.type() == GamepadChangeEventBody::TGamepadRemoved
) {
462 RemoveGamepad(handle
);
466 if (!SetGamepadByEvent(aEvent
)) {
470 // Hold on to listeners in a separate array because firing events
471 // can mutate the mListeners array.
472 nsTArray
<RefPtr
<nsGlobalWindowInner
>> listeners(mListeners
.Clone());
474 for (uint32_t i
= 0; i
< listeners
.Length(); i
++) {
475 // Only send events to non-background windows
476 if (!listeners
[i
]->IsCurrentInnerWindow() ||
477 listeners
[i
]->GetOuterWindow()->IsBackground() ||
478 listeners
[i
]->ShouldResistFingerprinting(RFPTarget::Gamepad
)) {
482 SetGamepadByEvent(aEvent
, listeners
[i
]);
483 MaybeConvertToNonstandardGamepadEvent(aEvent
, listeners
[i
]);
487 void GamepadManager::MaybeConvertToNonstandardGamepadEvent(
488 const GamepadChangeEvent
& aEvent
, nsGlobalWindowInner
* aWindow
) {
491 if (!mNonstandardEventsEnabled
) {
495 GamepadHandle handle
= aEvent
.handle();
497 RefPtr
<Gamepad
> gamepad
= aWindow
->GetGamepad(handle
);
498 const GamepadChangeEventBody
& body
= aEvent
.body();
501 switch (body
.type()) {
502 case GamepadChangeEventBody::TGamepadButtonInformation
: {
503 const GamepadButtonInformation
& a
= body
.get_GamepadButtonInformation();
504 FireButtonEvent(aWindow
, gamepad
, a
.button(), a
.value());
507 case GamepadChangeEventBody::TGamepadAxisInformation
: {
508 const GamepadAxisInformation
& a
= body
.get_GamepadAxisInformation();
509 FireAxisMoveEvent(aWindow
, gamepad
, a
.axis(), a
.value());
518 bool GamepadManager::SetGamepadByEvent(const GamepadChangeEvent
& aEvent
,
519 nsGlobalWindowInner
* aWindow
) {
521 bool firstTime
= false;
523 GamepadHandle handle
= aEvent
.handle();
526 if (!AxisMoveIsFirstIntent(aWindow
, handle
, aEvent
)) {
529 firstTime
= !MaybeWindowHasSeenGamepad(aWindow
, handle
);
532 RefPtr
<Gamepad
> gamepad
=
533 aWindow
? aWindow
->GetGamepad(handle
) : GetGamepad(handle
);
534 const GamepadChangeEventBody
& body
= aEvent
.body();
537 switch (body
.type()) {
538 case GamepadChangeEventBody::TGamepadButtonInformation
: {
539 const GamepadButtonInformation
& a
= body
.get_GamepadButtonInformation();
540 gamepad
->SetButton(a
.button(), a
.pressed(), a
.touched(), a
.value());
543 case GamepadChangeEventBody::TGamepadAxisInformation
: {
544 const GamepadAxisInformation
& a
= body
.get_GamepadAxisInformation();
545 gamepad
->SetAxis(a
.axis(), a
.value());
548 case GamepadChangeEventBody::TGamepadPoseInformation
: {
549 const GamepadPoseInformation
& a
= body
.get_GamepadPoseInformation();
550 gamepad
->SetPose(a
.pose_state());
553 case GamepadChangeEventBody::TGamepadLightIndicatorTypeInformation
: {
554 const GamepadLightIndicatorTypeInformation
& a
=
555 body
.get_GamepadLightIndicatorTypeInformation();
556 gamepad
->SetLightIndicatorType(a
.light(), a
.type());
559 case GamepadChangeEventBody::TGamepadTouchInformation
: {
560 // Avoid GamepadTouch's touchId be accessed in cross-origin tracking.
561 for (uint32_t i
= 0; i
< mListeners
.Length(); i
++) {
562 RefPtr
<Gamepad
> listenerGamepad
= mListeners
[i
]->GetGamepad(handle
);
563 if (listenerGamepad
&& mListeners
[i
]->IsCurrentInnerWindow() &&
564 !mListeners
[i
]->GetOuterWindow()->IsBackground()) {
565 const GamepadTouchInformation
& a
=
566 body
.get_GamepadTouchInformation();
567 listenerGamepad
->SetTouchEvent(a
.index(), a
.touch_state());
572 case GamepadChangeEventBody::TGamepadHandInformation
: {
573 const GamepadHandInformation
& a
= body
.get_GamepadHandInformation();
574 gamepad
->SetHand(a
.hand());
584 if (aWindow
&& firstTime
) {
585 FireConnectionEvent(aWindow
, gamepad
, true);
591 already_AddRefed
<Promise
> GamepadManager::VibrateHaptic(
592 GamepadHandle aHandle
, uint32_t aHapticIndex
, double aIntensity
,
593 double aDuration
, nsIGlobalObject
* aGlobal
, ErrorResult
& aRv
) {
594 RefPtr
<Promise
> promise
= Promise::Create(aGlobal
, aRv
);
595 if (NS_WARN_IF(aRv
.Failed())) {
596 aRv
.Throw(NS_ERROR_FAILURE
);
599 if (StaticPrefs::dom_gamepad_haptic_feedback_enabled()) {
600 if (aHandle
.GetKind() == GamepadHandleKind::VR
) {
601 if (gfx::VRManagerChild::IsCreated()) {
602 gfx::VRManagerChild
* vm
= gfx::VRManagerChild::Get();
603 vm
->AddPromise(mPromiseID
, promise
);
604 vm
->SendVibrateHaptic(aHandle
, aHapticIndex
, aIntensity
, aDuration
,
609 mChannelChild
->AddPromise(mPromiseID
, promise
);
610 mChannelChild
->SendVibrateHaptic(aHandle
, aHapticIndex
, aIntensity
,
611 aDuration
, mPromiseID
);
617 return promise
.forget();
620 void GamepadManager::StopHaptics() {
621 if (!StaticPrefs::dom_gamepad_haptic_feedback_enabled()) {
625 for (const auto& entry
: mGamepads
) {
626 const GamepadHandle handle
= entry
.GetWeak()->GetHandle();
627 if (handle
.GetKind() == GamepadHandleKind::VR
) {
628 if (gfx::VRManagerChild::IsCreated()) {
629 gfx::VRManagerChild
* vm
= gfx::VRManagerChild::Get();
630 vm
->SendStopVibrateHaptic(handle
);
634 mChannelChild
->SendStopVibrateHaptic(handle
);
640 already_AddRefed
<Promise
> GamepadManager::SetLightIndicatorColor(
641 GamepadHandle aHandle
, uint32_t aLightColorIndex
, uint8_t aRed
,
642 uint8_t aGreen
, uint8_t aBlue
, nsIGlobalObject
* aGlobal
, ErrorResult
& aRv
) {
643 RefPtr
<Promise
> promise
= Promise::Create(aGlobal
, aRv
);
644 if (NS_WARN_IF(aRv
.Failed())) {
645 aRv
.Throw(NS_ERROR_FAILURE
);
648 if (StaticPrefs::dom_gamepad_extensions_lightindicator()) {
649 MOZ_RELEASE_ASSERT(aHandle
.GetKind() != GamepadHandleKind::VR
,
650 "We don't support light indicator in VR.");
653 mChannelChild
->AddPromise(mPromiseID
, promise
);
654 mChannelChild
->SendLightIndicatorColor(aHandle
, aLightColorIndex
, aRed
,
655 aGreen
, aBlue
, mPromiseID
);
660 return promise
.forget();
662 } // namespace mozilla::dom