Backed out changeset 2450366cf7ca (bug 1891629) for causing win msix mochitest failures
[gecko.git] / dom / gamepad / GamepadManager.cpp
blob8d67670746c067c33020063ddda930d9b5c3ea58
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"
33 #include <cstddef>
35 using namespace mozilla::ipc;
37 namespace mozilla::dom {
39 namespace {
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
49 // intent.
50 const float AXIS_FIRST_INTENT_THRESHOLD_VALUE = 0.1f;
52 } // namespace
54 NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver)
56 GamepadManager::GamepadManager()
57 : mEnabled(false),
58 mNonstandardEventsEnabled(false),
59 mShuttingDown(false),
60 mPromiseID(0) {}
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;
73 nsresult rv;
74 rv = observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
75 false);
77 if (NS_WARN_IF(NS_FAILED(rv))) {
78 return rv;
81 return NS_OK;
84 NS_IMETHODIMP
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);
92 BeginShutdown();
93 return NS_OK;
96 void GamepadManager::StopMonitoring() {
97 if (mChannelChild) {
98 PGamepadEventChannelChild::Send__delete__(mChannelChild);
99 mChannelChild = nullptr;
101 if (gfx::VRManagerChild::IsCreated()) {
102 gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
103 vm->SendControllerListenerRemoved();
105 mGamepads.Clear();
108 void GamepadManager::BeginShutdown() {
109 mShuttingDown = true;
110 StopMonitoring();
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);
115 mListeners.Clear();
116 sShutdown = true;
119 void GamepadManager::AddListener(nsGlobalWindowInner* aWindow) {
120 MOZ_ASSERT(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.
128 return;
131 RefPtr<GamepadEventChannelChild> child(GamepadEventChannelChild::Create());
132 if (!actor->SendPGamepadEventChannelConstructor(child.get())) {
133 // We are probably shutting down.
134 return;
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)) {
149 return;
152 if (mListeners.IndexOf(aWindow) != NoIndex) {
153 return; // already exists
156 mListeners.AppendElement(aWindow);
159 void GamepadManager::RemoveListener(nsGlobalWindowInner* aWindow) {
160 MOZ_ASSERT(aWindow);
162 if (mShuttingDown) {
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.
165 return;
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()) {
179 StopMonitoring();
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();
190 return nullptr;
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);
215 if (!gamepad) {
216 NS_WARNING("Trying to delete gamepad with invalid index");
217 return;
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) {
226 nsString name =
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;
247 init.mAxis = aAxis;
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,
258 bool aConnected) {
259 if (mShuttingDown) {
260 return;
263 RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
264 if (!gamepad) {
265 return;
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());
272 if (aConnected) {
273 for (uint32_t i = 0; i < listeners.Length(); i++) {
274 #ifdef NIGHTLY_BUILD
275 // Don't fire a gamepadconnected event unless it's a secure context
276 if (!listeners[i]->IsSecureContext()) {
277 continue;
279 #endif
281 // Do not fire gamepadconnected and gamepaddisconnected events when
282 // privacy.resistFingerprinting is true.
283 if (listeners[i]->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
284 continue;
287 // Only send events to non-background windows
288 if (!listeners[i]->IsCurrentInnerWindow() ||
289 listeners[i]->GetOuterWindow()->IsBackground()) {
290 continue;
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()) {
296 continue;
299 SetWindowHasSeenGamepad(listeners[i], aHandle);
301 RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aHandle);
302 if (listenerGamepad) {
303 // Fire event
304 FireConnectionEvent(listeners[i], listenerGamepad, aConnected);
307 } else {
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)) {
317 continue;
320 if (WindowHasSeenGamepad(listeners[i], aHandle)) {
321 RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aHandle);
322 if (listenerGamepad) {
323 listenerGamepad->SetConnected(false);
324 // Fire event
325 FireConnectionEvent(listeners[i], listenerGamepad, false);
326 listeners[i]->RemoveGamepad(aHandle);
333 void GamepadManager::FireConnectionEvent(EventTarget* aTarget,
334 Gamepad* aGamepad, bool aConnected) {
335 nsString name =
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,
350 Gamepad* aGamepad) {
351 if (mShuttingDown || !mEnabled ||
352 aWindow->ShouldResistFingerprinting(RFPTarget::Gamepad)) {
353 return;
356 RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
357 if (!gamepad) {
358 return;
361 aGamepad->SyncState(gamepad);
364 // static
365 bool GamepadManager::IsServiceRunning() { return !!gGamepadManagerSingleton; }
367 // static
368 already_AddRefed<GamepadManager> GamepadManager::GetService() {
369 if (sShutdown) {
370 return nullptr;
373 if (!gGamepadManagerSingleton) {
374 RefPtr<GamepadManager> manager = new GamepadManager();
375 nsresult rv = manager->Init();
376 if (NS_WARN_IF(NS_FAILED(rv))) {
377 return nullptr;
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) {
397 return false;
400 return true;
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);
409 return false;
411 return true;
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,
422 bool aHasSeen) {
423 MOZ_ASSERT(aWindow);
425 if (mListeners.IndexOf(aWindow) == NoIndex) {
426 // This window isn't even listening for gamepad events.
427 return;
430 if (aHasSeen) {
431 aWindow->SetHasSeenGamepadInput(true);
432 nsCOMPtr<nsISupports> window = ToSupports(aWindow);
433 RefPtr<Gamepad> gamepad = GetGamepad(aHandle);
434 if (!gamepad) {
435 return;
437 RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window);
438 aWindow->AddGamepad(aHandle, clonedGamepad);
439 } else {
440 aWindow->RemoveGamepad(aHandle);
444 void GamepadManager::Update(const GamepadChangeEvent& aEvent) {
445 if (!mEnabled || mShuttingDown) {
446 return;
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(),
458 a.num_touches());
459 return;
461 if (body.type() == GamepadChangeEventBody::TGamepadRemoved) {
462 RemoveGamepad(handle);
463 return;
466 if (!SetGamepadByEvent(aEvent)) {
467 return;
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)) {
479 continue;
482 SetGamepadByEvent(aEvent, listeners[i]);
483 MaybeConvertToNonstandardGamepadEvent(aEvent, listeners[i]);
487 void GamepadManager::MaybeConvertToNonstandardGamepadEvent(
488 const GamepadChangeEvent& aEvent, nsGlobalWindowInner* aWindow) {
489 MOZ_ASSERT(aWindow);
491 if (!mNonstandardEventsEnabled) {
492 return;
495 GamepadHandle handle = aEvent.handle();
497 RefPtr<Gamepad> gamepad = aWindow->GetGamepad(handle);
498 const GamepadChangeEventBody& body = aEvent.body();
500 if (gamepad) {
501 switch (body.type()) {
502 case GamepadChangeEventBody::TGamepadButtonInformation: {
503 const GamepadButtonInformation& a = body.get_GamepadButtonInformation();
504 FireButtonEvent(aWindow, gamepad, a.button(), a.value());
505 break;
507 case GamepadChangeEventBody::TGamepadAxisInformation: {
508 const GamepadAxisInformation& a = body.get_GamepadAxisInformation();
509 FireAxisMoveEvent(aWindow, gamepad, a.axis(), a.value());
510 break;
512 default:
513 break;
518 bool GamepadManager::SetGamepadByEvent(const GamepadChangeEvent& aEvent,
519 nsGlobalWindowInner* aWindow) {
520 bool ret = false;
521 bool firstTime = false;
523 GamepadHandle handle = aEvent.handle();
525 if (aWindow) {
526 if (!AxisMoveIsFirstIntent(aWindow, handle, aEvent)) {
527 return false;
529 firstTime = !MaybeWindowHasSeenGamepad(aWindow, handle);
532 RefPtr<Gamepad> gamepad =
533 aWindow ? aWindow->GetGamepad(handle) : GetGamepad(handle);
534 const GamepadChangeEventBody& body = aEvent.body();
536 if (gamepad) {
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());
541 break;
543 case GamepadChangeEventBody::TGamepadAxisInformation: {
544 const GamepadAxisInformation& a = body.get_GamepadAxisInformation();
545 gamepad->SetAxis(a.axis(), a.value());
546 break;
548 case GamepadChangeEventBody::TGamepadPoseInformation: {
549 const GamepadPoseInformation& a = body.get_GamepadPoseInformation();
550 gamepad->SetPose(a.pose_state());
551 break;
553 case GamepadChangeEventBody::TGamepadLightIndicatorTypeInformation: {
554 const GamepadLightIndicatorTypeInformation& a =
555 body.get_GamepadLightIndicatorTypeInformation();
556 gamepad->SetLightIndicatorType(a.light(), a.type());
557 break;
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());
570 break;
572 case GamepadChangeEventBody::TGamepadHandInformation: {
573 const GamepadHandInformation& a = body.get_GamepadHandInformation();
574 gamepad->SetHand(a.hand());
575 break;
577 default:
578 MOZ_ASSERT(false);
579 break;
581 ret = true;
584 if (aWindow && firstTime) {
585 FireConnectionEvent(aWindow, gamepad, true);
588 return ret;
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);
597 return nullptr;
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,
605 mPromiseID);
607 } else {
608 if (mChannelChild) {
609 mChannelChild->AddPromise(mPromiseID, promise);
610 mChannelChild->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity,
611 aDuration, mPromiseID);
616 ++mPromiseID;
617 return promise.forget();
620 void GamepadManager::StopHaptics() {
621 if (!StaticPrefs::dom_gamepad_haptic_feedback_enabled()) {
622 return;
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);
632 } else {
633 if (mChannelChild) {
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);
646 return nullptr;
648 if (StaticPrefs::dom_gamepad_extensions_lightindicator()) {
649 MOZ_RELEASE_ASSERT(aHandle.GetKind() != GamepadHandleKind::VR,
650 "We don't support light indicator in VR.");
652 if (mChannelChild) {
653 mChannelChild->AddPromise(mPromiseID, promise);
654 mChannelChild->SendLightIndicatorColor(aHandle, aLightColorIndex, aRed,
655 aGreen, aBlue, mPromiseID);
659 ++mPromiseID;
660 return promise.forget();
662 } // namespace mozilla::dom