1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "mozilla/Hal.h"
6 #include "mozilla/ClearOnShutdown.h"
7 #include "mozilla/Preferences.h"
8 #include "mozilla/StaticPtr.h"
10 #include "GamepadService.h"
12 #include "nsAutoPtr.h"
13 #include "nsIDOMEvent.h"
14 #include "nsIDOMDocument.h"
15 #include "nsIDOMWindow.h"
16 #include "nsIObserver.h"
17 #include "nsIObserverService.h"
18 #include "nsIServiceManager.h"
20 #include "nsThreadUtils.h"
21 #include "mozilla/Services.h"
23 #include "mozilla/dom/GamepadAxisMoveEvent.h"
24 #include "mozilla/dom/GamepadButtonEvent.h"
25 #include "mozilla/dom/GamepadEvent.h"
33 const char* kGamepadEnabledPref
= "dom.gamepad.enabled";
34 const char* kGamepadEventsEnabledPref
=
35 "dom.gamepad.non_standard_events.enabled";
36 // Amount of time to wait before cleaning up gamepad resources
37 // when no pages are listening for events.
38 const int kCleanupDelayMS
= 2000;
39 const nsTArray
<nsRefPtr
<nsGlobalWindow
> >::index_type NoIndex
=
40 nsTArray
<nsRefPtr
<nsGlobalWindow
> >::NoIndex
;
42 StaticRefPtr
<GamepadService
> gGamepadServiceSingleton
;
46 bool GamepadService::sShutdown
= false;
48 NS_IMPL_ISUPPORTS(GamepadService
, nsIObserver
)
50 GamepadService::GamepadService()
54 mEnabled
= IsAPIEnabled();
55 mNonstandardEventsEnabled
=
56 Preferences::GetBool(kGamepadEventsEnabledPref
, false);
57 nsCOMPtr
<nsIObserverService
> observerService
=
58 mozilla::services::GetObserverService();
59 observerService
->AddObserver(this,
60 NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID
,
65 GamepadService::Observe(nsISupports
* aSubject
,
67 const char16_t
* aData
)
69 nsCOMPtr
<nsIObserverService
> observerService
=
70 mozilla::services::GetObserverService();
71 observerService
->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID
);
78 GamepadService::BeginShutdown()
85 mozilla::hal::StopMonitoringGamepadStatus();
88 // Don't let windows call back to unregister during shutdown
89 for (uint32_t i
= 0; i
< mListeners
.Length(); i
++) {
90 mListeners
[i
]->SetHasGamepadEventListener(false);
98 GamepadService::AddListener(nsGlobalWindow
* aWindow
)
101 MOZ_ASSERT(aWindow
->IsInnerWindow());
107 if (mListeners
.IndexOf(aWindow
) != NoIndex
) {
108 return; // already exists
111 if (!mStarted
&& mEnabled
) {
112 mozilla::hal::StartMonitoringGamepadStatus();
116 mListeners
.AppendElement(aWindow
);
120 GamepadService::RemoveListener(nsGlobalWindow
* aWindow
)
123 MOZ_ASSERT(aWindow
->IsInnerWindow());
126 // Doesn't matter at this point. It's possible we're being called
127 // as a result of our own destructor here, so just bail out.
131 if (mListeners
.IndexOf(aWindow
) == NoIndex
) {
132 return; // doesn't exist
135 mListeners
.RemoveElement(aWindow
);
137 if (mListeners
.Length() == 0 && !mShuttingDown
&& mStarted
) {
143 GamepadService::AddGamepad(const char* aId
,
144 GamepadMappingType aMapping
,
145 uint32_t aNumButtons
,
148 //TODO: bug 852258: get initial button/axis state
149 nsRefPtr
<Gamepad
> gamepad
=
151 NS_ConvertUTF8toUTF16(nsDependentCString(aId
)),
157 for (uint32_t i
= 0; i
< mGamepads
.Length(); i
++) {
159 mGamepads
[i
] = gamepad
;
165 mGamepads
.AppendElement(gamepad
);
166 index
= mGamepads
.Length() - 1;
169 gamepad
->SetIndex(index
);
170 NewConnectionEvent(index
, true);
176 GamepadService::RemoveGamepad(uint32_t aIndex
)
178 if (aIndex
< mGamepads
.Length()) {
179 mGamepads
[aIndex
]->SetConnected(false);
180 NewConnectionEvent(aIndex
, false);
181 // If this is the last entry in the list, just remove it.
182 if (aIndex
== mGamepads
.Length() - 1) {
183 mGamepads
.RemoveElementAt(aIndex
);
185 // Otherwise just null it out and leave it, so the
186 // indices of the following entries remain valid.
187 mGamepads
[aIndex
] = nullptr;
193 GamepadService::NewButtonEvent(uint32_t aIndex
, uint32_t aButton
, bool aPressed
)
195 // Synthesize a value: 1.0 for pressed, 0.0 for unpressed.
196 NewButtonEvent(aIndex
, aButton
, aPressed
, aPressed
? 1.0L : 0.0L);
200 GamepadService::NewButtonEvent(uint32_t aIndex
, uint32_t aButton
, bool aPressed
,
203 if (mShuttingDown
|| aIndex
>= mGamepads
.Length()) {
207 mGamepads
[aIndex
]->SetButton(aButton
, aPressed
, aValue
);
209 // Hold on to listeners in a separate array because firing events
210 // can mutate the mListeners array.
211 nsTArray
<nsRefPtr
<nsGlobalWindow
> > listeners(mListeners
);
213 for (uint32_t i
= listeners
.Length(); i
> 0 ; ) {
216 // Only send events to non-background windows
217 if (!listeners
[i
]->IsCurrentInnerWindow() ||
218 listeners
[i
]->GetOuterWindow()->IsBackground()) {
222 bool first_time
= false;
223 if (!WindowHasSeenGamepad(listeners
[i
], aIndex
)) {
224 // This window hasn't seen this gamepad before, so
225 // send a connection event first.
226 SetWindowHasSeenGamepad(listeners
[i
], aIndex
);
230 nsRefPtr
<Gamepad
> gamepad
= listeners
[i
]->GetGamepad(aIndex
);
232 gamepad
->SetButton(aButton
, aPressed
, aValue
);
234 FireConnectionEvent(listeners
[i
], gamepad
, true);
236 if (mNonstandardEventsEnabled
) {
238 FireButtonEvent(listeners
[i
], gamepad
, aButton
, aValue
);
245 GamepadService::FireButtonEvent(EventTarget
* aTarget
,
250 nsString name
= aValue
== 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") :
251 NS_LITERAL_STRING("gamepadbuttonup");
252 GamepadButtonEventInit init
;
253 init
.mBubbles
= false;
254 init
.mCancelable
= false;
255 init
.mGamepad
= aGamepad
;
256 init
.mButton
= aButton
;
257 nsRefPtr
<GamepadButtonEvent
> event
=
258 GamepadButtonEvent::Constructor(aTarget
, name
, init
);
260 event
->SetTrusted(true);
262 bool defaultActionEnabled
= true;
263 aTarget
->DispatchEvent(event
, &defaultActionEnabled
);
267 GamepadService::NewAxisMoveEvent(uint32_t aIndex
, uint32_t aAxis
, double aValue
)
269 if (mShuttingDown
|| aIndex
>= mGamepads
.Length()) {
272 mGamepads
[aIndex
]->SetAxis(aAxis
, aValue
);
274 // Hold on to listeners in a separate array because firing events
275 // can mutate the mListeners array.
276 nsTArray
<nsRefPtr
<nsGlobalWindow
> > listeners(mListeners
);
278 for (uint32_t i
= listeners
.Length(); i
> 0 ; ) {
281 // Only send events to non-background windows
282 if (!listeners
[i
]->IsCurrentInnerWindow() ||
283 listeners
[i
]->GetOuterWindow()->IsBackground()) {
287 bool first_time
= false;
288 if (!WindowHasSeenGamepad(listeners
[i
], aIndex
)) {
289 // This window hasn't seen this gamepad before, so
290 // send a connection event first.
291 SetWindowHasSeenGamepad(listeners
[i
], aIndex
);
295 nsRefPtr
<Gamepad
> gamepad
= listeners
[i
]->GetGamepad(aIndex
);
297 gamepad
->SetAxis(aAxis
, aValue
);
299 FireConnectionEvent(listeners
[i
], gamepad
, true);
301 if (mNonstandardEventsEnabled
) {
303 FireAxisMoveEvent(listeners
[i
], gamepad
, aAxis
, aValue
);
310 GamepadService::FireAxisMoveEvent(EventTarget
* aTarget
,
315 GamepadAxisMoveEventInit init
;
316 init
.mBubbles
= false;
317 init
.mCancelable
= false;
318 init
.mGamepad
= aGamepad
;
320 init
.mValue
= aValue
;
321 nsRefPtr
<GamepadAxisMoveEvent
> event
=
322 GamepadAxisMoveEvent::Constructor(aTarget
,
323 NS_LITERAL_STRING("gamepadaxismove"),
326 event
->SetTrusted(true);
328 bool defaultActionEnabled
= true;
329 aTarget
->DispatchEvent(event
, &defaultActionEnabled
);
333 GamepadService::NewConnectionEvent(uint32_t aIndex
, bool aConnected
)
335 if (mShuttingDown
|| aIndex
>= mGamepads
.Length()) {
339 // Hold on to listeners in a separate array because firing events
340 // can mutate the mListeners array.
341 nsTArray
<nsRefPtr
<nsGlobalWindow
> > listeners(mListeners
);
344 for (uint32_t i
= listeners
.Length(); i
> 0 ; ) {
347 // Only send events to non-background windows
348 if (!listeners
[i
]->IsCurrentInnerWindow() ||
349 listeners
[i
]->GetOuterWindow()->IsBackground()) {
353 // We don't fire a connected event here unless the window
354 // has seen input from at least one device.
355 if (!listeners
[i
]->HasSeenGamepadInput()) {
359 SetWindowHasSeenGamepad(listeners
[i
], aIndex
);
361 nsRefPtr
<Gamepad
> gamepad
= listeners
[i
]->GetGamepad(aIndex
);
364 FireConnectionEvent(listeners
[i
], gamepad
, aConnected
);
368 // For disconnection events, fire one at every window that has received
369 // data from this gamepad.
370 for (uint32_t i
= listeners
.Length(); i
> 0 ; ) {
373 // Even background windows get these events, so we don't have to
374 // deal with the hassle of syncing the state of removed gamepads.
376 if (WindowHasSeenGamepad(listeners
[i
], aIndex
)) {
377 nsRefPtr
<Gamepad
> gamepad
= listeners
[i
]->GetGamepad(aIndex
);
379 gamepad
->SetConnected(false);
381 FireConnectionEvent(listeners
[i
], gamepad
, false);
382 listeners
[i
]->RemoveGamepad(aIndex
);
390 GamepadService::FireConnectionEvent(EventTarget
* aTarget
,
394 nsString name
= aConnected
? NS_LITERAL_STRING("gamepadconnected") :
395 NS_LITERAL_STRING("gamepaddisconnected");
396 GamepadEventInit init
;
397 init
.mBubbles
= false;
398 init
.mCancelable
= false;
399 init
.mGamepad
= aGamepad
;
400 nsRefPtr
<GamepadEvent
> event
=
401 GamepadEvent::Constructor(aTarget
, name
, init
);
403 event
->SetTrusted(true);
405 bool defaultActionEnabled
= true;
406 aTarget
->DispatchEvent(event
, &defaultActionEnabled
);
410 GamepadService::SyncGamepadState(uint32_t aIndex
, Gamepad
* aGamepad
)
412 if (mShuttingDown
|| !mEnabled
|| aIndex
> mGamepads
.Length()) {
416 aGamepad
->SyncState(mGamepads
[aIndex
]);
420 already_AddRefed
<GamepadService
>
421 GamepadService::GetService()
427 if (!gGamepadServiceSingleton
) {
428 gGamepadServiceSingleton
= new GamepadService();
429 ClearOnShutdown(&gGamepadServiceSingleton
);
431 nsRefPtr
<GamepadService
> service(gGamepadServiceSingleton
);
432 return service
.forget();
437 GamepadService::IsAPIEnabled() {
438 return Preferences::GetBool(kGamepadEnabledPref
, false);
442 GamepadService::WindowHasSeenGamepad(nsGlobalWindow
* aWindow
, uint32_t aIndex
)
444 nsRefPtr
<Gamepad
> gamepad
= aWindow
->GetGamepad(aIndex
);
445 return gamepad
!= nullptr;
449 GamepadService::SetWindowHasSeenGamepad(nsGlobalWindow
* aWindow
,
454 MOZ_ASSERT(aWindow
->IsInnerWindow());
456 if (mListeners
.IndexOf(aWindow
) == NoIndex
) {
457 // This window isn't even listening for gamepad events.
462 aWindow
->SetHasSeenGamepadInput(true);
463 nsCOMPtr
<nsISupports
> window
= ToSupports(aWindow
);
464 nsRefPtr
<Gamepad
> gamepad
= mGamepads
[aIndex
]->Clone(window
);
465 aWindow
->AddGamepad(aIndex
, gamepad
);
467 aWindow
->RemoveGamepad(aIndex
);
473 GamepadService::TimeoutHandler(nsITimer
* aTimer
, void* aClosure
)
475 // the reason that we use self, instead of just using nsITimerCallback or nsIObserver
476 // is so that subclasses are free to use timers without worry about the base classes's
478 GamepadService
* self
= reinterpret_cast<GamepadService
*>(aClosure
);
484 if (self
->mShuttingDown
) {
488 if (self
->mListeners
.Length() == 0) {
489 mozilla::hal::StopMonitoringGamepadStatus();
490 self
->mStarted
= false;
491 if (!self
->mGamepads
.IsEmpty()) {
492 self
->mGamepads
.Clear();
498 GamepadService::StartCleanupTimer()
504 mTimer
= do_CreateInstance("@mozilla.org/timer;1");
506 mTimer
->InitWithFuncCallback(TimeoutHandler
,
509 nsITimer::TYPE_ONE_SHOT
);
514 * Implementation of the test service. This is just to provide a simple binding
515 * of the GamepadService to JavaScript via XPCOM so that we can write Mochitests
516 * that add and remove fake gamepads, avoiding the platform-specific backends.
518 NS_IMPL_ISUPPORTS(GamepadServiceTest
, nsIGamepadServiceTest
)
520 GamepadServiceTest
* GamepadServiceTest::sSingleton
= nullptr;
523 already_AddRefed
<GamepadServiceTest
>
524 GamepadServiceTest::CreateService()
526 if (sSingleton
== nullptr) {
527 sSingleton
= new GamepadServiceTest();
529 nsRefPtr
<GamepadServiceTest
> service
= sSingleton
;
530 return service
.forget();
533 GamepadServiceTest::GamepadServiceTest()
535 /* member initializers and constructor code */
536 nsRefPtr
<GamepadService
> service
= GamepadService::GetService();
539 /* uint32_t addGamepad (in string id, in unsigned long mapping, in unsigned long numButtons, in unsigned long numAxes); */
540 NS_IMETHODIMP
GamepadServiceTest::AddGamepad(const char* aID
,
542 uint32_t aNumButtons
,
546 *aRetval
= gGamepadServiceSingleton
->AddGamepad(aID
,
547 static_cast<GamepadMappingType
>(aMapping
),
553 /* void removeGamepad (in uint32_t index); */
554 NS_IMETHODIMP
GamepadServiceTest::RemoveGamepad(uint32_t aIndex
)
556 gGamepadServiceSingleton
->RemoveGamepad(aIndex
);
560 /* void newButtonEvent (in uint32_t index, in uint32_t button,
561 in boolean pressed); */
562 NS_IMETHODIMP
GamepadServiceTest::NewButtonEvent(uint32_t aIndex
,
566 gGamepadServiceSingleton
->NewButtonEvent(aIndex
, aButton
, aPressed
);
570 /* void newAxisMoveEvent (in uint32_t index, in uint32_t axis,
572 NS_IMETHODIMP
GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex
,
576 gGamepadServiceSingleton
->NewAxisMoveEvent(aIndex
, aAxis
, aValue
);
581 } // namespace mozilla