|
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/. */ |
|
4 |
|
5 #include "mozilla/Hal.h" |
|
6 #include "mozilla/ClearOnShutdown.h" |
|
7 #include "mozilla/Preferences.h" |
|
8 #include "mozilla/StaticPtr.h" |
|
9 |
|
10 #include "GamepadService.h" |
|
11 #include "Gamepad.h" |
|
12 #include "nsAutoPtr.h" |
|
13 #include "nsIDOMEvent.h" |
|
14 #include "nsIDOMDocument.h" |
|
15 #include "GeneratedEvents.h" |
|
16 #include "nsIDOMWindow.h" |
|
17 #include "nsIObserver.h" |
|
18 #include "nsIObserverService.h" |
|
19 #include "nsIServiceManager.h" |
|
20 #include "nsITimer.h" |
|
21 #include "nsThreadUtils.h" |
|
22 #include "mozilla/Services.h" |
|
23 |
|
24 #include "mozilla/dom/GamepadAxisMoveEvent.h" |
|
25 #include "mozilla/dom/GamepadButtonEvent.h" |
|
26 #include "mozilla/dom/GamepadEvent.h" |
|
27 |
|
28 #include <cstddef> |
|
29 |
|
30 namespace mozilla { |
|
31 namespace dom { |
|
32 |
|
33 namespace { |
|
34 const char* kGamepadEnabledPref = "dom.gamepad.enabled"; |
|
35 const char* kGamepadEventsEnabledPref = |
|
36 "dom.gamepad.non_standard_events.enabled"; |
|
37 // Amount of time to wait before cleaning up gamepad resources |
|
38 // when no pages are listening for events. |
|
39 const int kCleanupDelayMS = 2000; |
|
40 const nsTArray<nsRefPtr<nsGlobalWindow> >::index_type NoIndex = |
|
41 nsTArray<nsRefPtr<nsGlobalWindow> >::NoIndex; |
|
42 |
|
43 StaticRefPtr<GamepadService> gGamepadServiceSingleton; |
|
44 |
|
45 } // namespace |
|
46 |
|
47 bool GamepadService::sShutdown = false; |
|
48 |
|
49 NS_IMPL_ISUPPORTS(GamepadService, nsIObserver) |
|
50 |
|
51 GamepadService::GamepadService() |
|
52 : mStarted(false), |
|
53 mShuttingDown(false) |
|
54 { |
|
55 mEnabled = IsAPIEnabled(); |
|
56 mNonstandardEventsEnabled = |
|
57 Preferences::GetBool(kGamepadEventsEnabledPref, false); |
|
58 nsCOMPtr<nsIObserverService> observerService = |
|
59 mozilla::services::GetObserverService(); |
|
60 observerService->AddObserver(this, |
|
61 NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, |
|
62 false); |
|
63 } |
|
64 |
|
65 NS_IMETHODIMP |
|
66 GamepadService::Observe(nsISupports* aSubject, |
|
67 const char* aTopic, |
|
68 const char16_t* aData) |
|
69 { |
|
70 nsCOMPtr<nsIObserverService> observerService = |
|
71 mozilla::services::GetObserverService(); |
|
72 observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); |
|
73 |
|
74 BeginShutdown(); |
|
75 return NS_OK; |
|
76 } |
|
77 |
|
78 void |
|
79 GamepadService::BeginShutdown() |
|
80 { |
|
81 mShuttingDown = true; |
|
82 if (mTimer) { |
|
83 mTimer->Cancel(); |
|
84 } |
|
85 if (mStarted) { |
|
86 mozilla::hal::StopMonitoringGamepadStatus(); |
|
87 mStarted = false; |
|
88 } |
|
89 // Don't let windows call back to unregister during shutdown |
|
90 for (uint32_t i = 0; i < mListeners.Length(); i++) { |
|
91 mListeners[i]->SetHasGamepadEventListener(false); |
|
92 } |
|
93 mListeners.Clear(); |
|
94 mGamepads.Clear(); |
|
95 sShutdown = true; |
|
96 } |
|
97 |
|
98 void |
|
99 GamepadService::AddListener(nsGlobalWindow* aWindow) |
|
100 { |
|
101 if (mShuttingDown) { |
|
102 return; |
|
103 } |
|
104 |
|
105 if (mListeners.IndexOf(aWindow) != NoIndex) { |
|
106 return; // already exists |
|
107 } |
|
108 |
|
109 if (!mStarted && mEnabled) { |
|
110 mozilla::hal::StartMonitoringGamepadStatus(); |
|
111 mStarted = true; |
|
112 } |
|
113 |
|
114 mListeners.AppendElement(aWindow); |
|
115 } |
|
116 |
|
117 void |
|
118 GamepadService::RemoveListener(nsGlobalWindow* aWindow) |
|
119 { |
|
120 if (mShuttingDown) { |
|
121 // Doesn't matter at this point. It's possible we're being called |
|
122 // as a result of our own destructor here, so just bail out. |
|
123 return; |
|
124 } |
|
125 |
|
126 if (mListeners.IndexOf(aWindow) == NoIndex) { |
|
127 return; // doesn't exist |
|
128 } |
|
129 |
|
130 mListeners.RemoveElement(aWindow); |
|
131 |
|
132 if (mListeners.Length() == 0 && !mShuttingDown && mStarted) { |
|
133 StartCleanupTimer(); |
|
134 } |
|
135 } |
|
136 |
|
137 uint32_t |
|
138 GamepadService::AddGamepad(const char* aId, |
|
139 GamepadMappingType aMapping, |
|
140 uint32_t aNumButtons, |
|
141 uint32_t aNumAxes) |
|
142 { |
|
143 //TODO: bug 852258: get initial button/axis state |
|
144 nsRefPtr<Gamepad> gamepad = |
|
145 new Gamepad(nullptr, |
|
146 NS_ConvertUTF8toUTF16(nsDependentCString(aId)), |
|
147 0, |
|
148 aMapping, |
|
149 aNumButtons, |
|
150 aNumAxes); |
|
151 int index = -1; |
|
152 for (uint32_t i = 0; i < mGamepads.Length(); i++) { |
|
153 if (!mGamepads[i]) { |
|
154 mGamepads[i] = gamepad; |
|
155 index = i; |
|
156 break; |
|
157 } |
|
158 } |
|
159 if (index == -1) { |
|
160 mGamepads.AppendElement(gamepad); |
|
161 index = mGamepads.Length() - 1; |
|
162 } |
|
163 |
|
164 gamepad->SetIndex(index); |
|
165 NewConnectionEvent(index, true); |
|
166 |
|
167 return index; |
|
168 } |
|
169 |
|
170 void |
|
171 GamepadService::RemoveGamepad(uint32_t aIndex) |
|
172 { |
|
173 if (aIndex < mGamepads.Length()) { |
|
174 mGamepads[aIndex]->SetConnected(false); |
|
175 NewConnectionEvent(aIndex, false); |
|
176 // If this is the last entry in the list, just remove it. |
|
177 if (aIndex == mGamepads.Length() - 1) { |
|
178 mGamepads.RemoveElementAt(aIndex); |
|
179 } else { |
|
180 // Otherwise just null it out and leave it, so the |
|
181 // indices of the following entries remain valid. |
|
182 mGamepads[aIndex] = nullptr; |
|
183 } |
|
184 } |
|
185 } |
|
186 |
|
187 void |
|
188 GamepadService::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed) |
|
189 { |
|
190 // Synthesize a value: 1.0 for pressed, 0.0 for unpressed. |
|
191 NewButtonEvent(aIndex, aButton, aPressed, aPressed ? 1.0L : 0.0L); |
|
192 } |
|
193 |
|
194 void |
|
195 GamepadService::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed, |
|
196 double aValue) |
|
197 { |
|
198 if (mShuttingDown || aIndex >= mGamepads.Length()) { |
|
199 return; |
|
200 } |
|
201 |
|
202 mGamepads[aIndex]->SetButton(aButton, aPressed, aValue); |
|
203 |
|
204 // Hold on to listeners in a separate array because firing events |
|
205 // can mutate the mListeners array. |
|
206 nsTArray<nsRefPtr<nsGlobalWindow> > listeners(mListeners); |
|
207 |
|
208 for (uint32_t i = listeners.Length(); i > 0 ; ) { |
|
209 --i; |
|
210 |
|
211 // Only send events to non-background windows |
|
212 if (!listeners[i]->IsCurrentInnerWindow() || |
|
213 listeners[i]->GetOuterWindow()->IsBackground()) { |
|
214 continue; |
|
215 } |
|
216 |
|
217 bool first_time = false; |
|
218 if (!WindowHasSeenGamepad(listeners[i], aIndex)) { |
|
219 // This window hasn't seen this gamepad before, so |
|
220 // send a connection event first. |
|
221 SetWindowHasSeenGamepad(listeners[i], aIndex); |
|
222 first_time = true; |
|
223 } |
|
224 |
|
225 nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); |
|
226 if (gamepad) { |
|
227 gamepad->SetButton(aButton, aPressed, aValue); |
|
228 if (first_time) { |
|
229 FireConnectionEvent(listeners[i], gamepad, true); |
|
230 } |
|
231 if (mNonstandardEventsEnabled) { |
|
232 // Fire event |
|
233 FireButtonEvent(listeners[i], gamepad, aButton, aValue); |
|
234 } |
|
235 } |
|
236 } |
|
237 } |
|
238 |
|
239 void |
|
240 GamepadService::FireButtonEvent(EventTarget* aTarget, |
|
241 Gamepad* aGamepad, |
|
242 uint32_t aButton, |
|
243 double aValue) |
|
244 { |
|
245 nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") : |
|
246 NS_LITERAL_STRING("gamepadbuttonup"); |
|
247 GamepadButtonEventInit init; |
|
248 init.mBubbles = false; |
|
249 init.mCancelable = false; |
|
250 init.mGamepad = aGamepad; |
|
251 init.mButton = aButton; |
|
252 nsRefPtr<GamepadButtonEvent> event = |
|
253 GamepadButtonEvent::Constructor(aTarget, name, init); |
|
254 |
|
255 event->SetTrusted(true); |
|
256 |
|
257 bool defaultActionEnabled = true; |
|
258 aTarget->DispatchEvent(event, &defaultActionEnabled); |
|
259 } |
|
260 |
|
261 void |
|
262 GamepadService::NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue) |
|
263 { |
|
264 if (mShuttingDown || aIndex >= mGamepads.Length()) { |
|
265 return; |
|
266 } |
|
267 mGamepads[aIndex]->SetAxis(aAxis, aValue); |
|
268 |
|
269 // Hold on to listeners in a separate array because firing events |
|
270 // can mutate the mListeners array. |
|
271 nsTArray<nsRefPtr<nsGlobalWindow> > listeners(mListeners); |
|
272 |
|
273 for (uint32_t i = listeners.Length(); i > 0 ; ) { |
|
274 --i; |
|
275 |
|
276 // Only send events to non-background windows |
|
277 if (!listeners[i]->IsCurrentInnerWindow() || |
|
278 listeners[i]->GetOuterWindow()->IsBackground()) { |
|
279 continue; |
|
280 } |
|
281 |
|
282 bool first_time = false; |
|
283 if (!WindowHasSeenGamepad(listeners[i], aIndex)) { |
|
284 // This window hasn't seen this gamepad before, so |
|
285 // send a connection event first. |
|
286 SetWindowHasSeenGamepad(listeners[i], aIndex); |
|
287 first_time = true; |
|
288 } |
|
289 |
|
290 nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); |
|
291 if (gamepad) { |
|
292 gamepad->SetAxis(aAxis, aValue); |
|
293 if (first_time) { |
|
294 FireConnectionEvent(listeners[i], gamepad, true); |
|
295 } |
|
296 if (mNonstandardEventsEnabled) { |
|
297 // Fire event |
|
298 FireAxisMoveEvent(listeners[i], gamepad, aAxis, aValue); |
|
299 } |
|
300 } |
|
301 } |
|
302 } |
|
303 |
|
304 void |
|
305 GamepadService::FireAxisMoveEvent(EventTarget* aTarget, |
|
306 Gamepad* aGamepad, |
|
307 uint32_t aAxis, |
|
308 double aValue) |
|
309 { |
|
310 GamepadAxisMoveEventInit init; |
|
311 init.mBubbles = false; |
|
312 init.mCancelable = false; |
|
313 init.mGamepad = aGamepad; |
|
314 init.mAxis = aAxis; |
|
315 init.mValue = aValue; |
|
316 nsRefPtr<GamepadAxisMoveEvent> event = |
|
317 GamepadAxisMoveEvent::Constructor(aTarget, |
|
318 NS_LITERAL_STRING("gamepadaxismove"), |
|
319 init); |
|
320 |
|
321 event->SetTrusted(true); |
|
322 |
|
323 bool defaultActionEnabled = true; |
|
324 aTarget->DispatchEvent(event, &defaultActionEnabled); |
|
325 } |
|
326 |
|
327 void |
|
328 GamepadService::NewConnectionEvent(uint32_t aIndex, bool aConnected) |
|
329 { |
|
330 if (mShuttingDown || aIndex >= mGamepads.Length()) { |
|
331 return; |
|
332 } |
|
333 |
|
334 // Hold on to listeners in a separate array because firing events |
|
335 // can mutate the mListeners array. |
|
336 nsTArray<nsRefPtr<nsGlobalWindow> > listeners(mListeners); |
|
337 |
|
338 if (aConnected) { |
|
339 for (uint32_t i = listeners.Length(); i > 0 ; ) { |
|
340 --i; |
|
341 |
|
342 // Only send events to non-background windows |
|
343 if (!listeners[i]->IsCurrentInnerWindow() || |
|
344 listeners[i]->GetOuterWindow()->IsBackground()) { |
|
345 continue; |
|
346 } |
|
347 |
|
348 // We don't fire a connected event here unless the window |
|
349 // has seen input from at least one device. |
|
350 if (!listeners[i]->HasSeenGamepadInput()) { |
|
351 continue; |
|
352 } |
|
353 |
|
354 SetWindowHasSeenGamepad(listeners[i], aIndex); |
|
355 |
|
356 nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); |
|
357 if (gamepad) { |
|
358 // Fire event |
|
359 FireConnectionEvent(listeners[i], gamepad, aConnected); |
|
360 } |
|
361 } |
|
362 } else { |
|
363 // For disconnection events, fire one at every window that has received |
|
364 // data from this gamepad. |
|
365 for (uint32_t i = listeners.Length(); i > 0 ; ) { |
|
366 --i; |
|
367 |
|
368 // Even background windows get these events, so we don't have to |
|
369 // deal with the hassle of syncing the state of removed gamepads. |
|
370 |
|
371 if (WindowHasSeenGamepad(listeners[i], aIndex)) { |
|
372 nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); |
|
373 if (gamepad) { |
|
374 gamepad->SetConnected(false); |
|
375 // Fire event |
|
376 FireConnectionEvent(listeners[i], gamepad, false); |
|
377 listeners[i]->RemoveGamepad(aIndex); |
|
378 } |
|
379 } |
|
380 } |
|
381 } |
|
382 } |
|
383 |
|
384 void |
|
385 GamepadService::FireConnectionEvent(EventTarget* aTarget, |
|
386 Gamepad* aGamepad, |
|
387 bool aConnected) |
|
388 { |
|
389 nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") : |
|
390 NS_LITERAL_STRING("gamepaddisconnected"); |
|
391 GamepadEventInit init; |
|
392 init.mBubbles = false; |
|
393 init.mCancelable = false; |
|
394 init.mGamepad = aGamepad; |
|
395 nsRefPtr<GamepadEvent> event = |
|
396 GamepadEvent::Constructor(aTarget, name, init); |
|
397 |
|
398 event->SetTrusted(true); |
|
399 |
|
400 bool defaultActionEnabled = true; |
|
401 aTarget->DispatchEvent(event, &defaultActionEnabled); |
|
402 } |
|
403 |
|
404 void |
|
405 GamepadService::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad) |
|
406 { |
|
407 if (mShuttingDown || !mEnabled || aIndex > mGamepads.Length()) { |
|
408 return; |
|
409 } |
|
410 |
|
411 aGamepad->SyncState(mGamepads[aIndex]); |
|
412 } |
|
413 |
|
414 // static |
|
415 already_AddRefed<GamepadService> |
|
416 GamepadService::GetService() |
|
417 { |
|
418 if (sShutdown) { |
|
419 return nullptr; |
|
420 } |
|
421 |
|
422 if (!gGamepadServiceSingleton) { |
|
423 gGamepadServiceSingleton = new GamepadService(); |
|
424 ClearOnShutdown(&gGamepadServiceSingleton); |
|
425 } |
|
426 nsRefPtr<GamepadService> service(gGamepadServiceSingleton); |
|
427 return service.forget(); |
|
428 } |
|
429 |
|
430 // static |
|
431 bool |
|
432 GamepadService::IsAPIEnabled() { |
|
433 return Preferences::GetBool(kGamepadEnabledPref, false); |
|
434 } |
|
435 |
|
436 bool |
|
437 GamepadService::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) |
|
438 { |
|
439 nsRefPtr<Gamepad> gamepad = aWindow->GetGamepad(aIndex); |
|
440 return gamepad != nullptr; |
|
441 } |
|
442 |
|
443 void |
|
444 GamepadService::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow, |
|
445 uint32_t aIndex, |
|
446 bool aHasSeen) |
|
447 { |
|
448 if (mListeners.IndexOf(aWindow) == NoIndex) { |
|
449 // This window isn't even listening for gamepad events. |
|
450 return; |
|
451 } |
|
452 |
|
453 if (aHasSeen) { |
|
454 aWindow->SetHasSeenGamepadInput(true); |
|
455 nsCOMPtr<nsISupports> window = ToSupports(aWindow); |
|
456 nsRefPtr<Gamepad> gamepad = mGamepads[aIndex]->Clone(window); |
|
457 aWindow->AddGamepad(aIndex, gamepad); |
|
458 } else { |
|
459 aWindow->RemoveGamepad(aIndex); |
|
460 } |
|
461 } |
|
462 |
|
463 // static |
|
464 void |
|
465 GamepadService::TimeoutHandler(nsITimer* aTimer, void* aClosure) |
|
466 { |
|
467 // the reason that we use self, instead of just using nsITimerCallback or nsIObserver |
|
468 // is so that subclasses are free to use timers without worry about the base classes's |
|
469 // usage. |
|
470 GamepadService* self = reinterpret_cast<GamepadService*>(aClosure); |
|
471 if (!self) { |
|
472 NS_ERROR("no self"); |
|
473 return; |
|
474 } |
|
475 |
|
476 if (self->mShuttingDown) { |
|
477 return; |
|
478 } |
|
479 |
|
480 if (self->mListeners.Length() == 0) { |
|
481 mozilla::hal::StopMonitoringGamepadStatus(); |
|
482 self->mStarted = false; |
|
483 if (!self->mGamepads.IsEmpty()) { |
|
484 self->mGamepads.Clear(); |
|
485 } |
|
486 } |
|
487 } |
|
488 |
|
489 void |
|
490 GamepadService::StartCleanupTimer() |
|
491 { |
|
492 if (mTimer) { |
|
493 mTimer->Cancel(); |
|
494 } |
|
495 |
|
496 mTimer = do_CreateInstance("@mozilla.org/timer;1"); |
|
497 if (mTimer) { |
|
498 mTimer->InitWithFuncCallback(TimeoutHandler, |
|
499 this, |
|
500 kCleanupDelayMS, |
|
501 nsITimer::TYPE_ONE_SHOT); |
|
502 } |
|
503 } |
|
504 |
|
505 /* |
|
506 * Implementation of the test service. This is just to provide a simple binding |
|
507 * of the GamepadService to JavaScript via XPCOM so that we can write Mochitests |
|
508 * that add and remove fake gamepads, avoiding the platform-specific backends. |
|
509 */ |
|
510 NS_IMPL_ISUPPORTS(GamepadServiceTest, nsIGamepadServiceTest) |
|
511 |
|
512 GamepadServiceTest* GamepadServiceTest::sSingleton = nullptr; |
|
513 |
|
514 // static |
|
515 already_AddRefed<GamepadServiceTest> |
|
516 GamepadServiceTest::CreateService() |
|
517 { |
|
518 if (sSingleton == nullptr) { |
|
519 sSingleton = new GamepadServiceTest(); |
|
520 } |
|
521 nsRefPtr<GamepadServiceTest> service = sSingleton; |
|
522 return service.forget(); |
|
523 } |
|
524 |
|
525 GamepadServiceTest::GamepadServiceTest() |
|
526 { |
|
527 /* member initializers and constructor code */ |
|
528 nsRefPtr<GamepadService> service = GamepadService::GetService(); |
|
529 } |
|
530 |
|
531 /* uint32_t addGamepad (in string id, in unsigned long mapping, in unsigned long numButtons, in unsigned long numAxes); */ |
|
532 NS_IMETHODIMP GamepadServiceTest::AddGamepad(const char* aID, |
|
533 uint32_t aMapping, |
|
534 uint32_t aNumButtons, |
|
535 uint32_t aNumAxes, |
|
536 uint32_t* aRetval) |
|
537 { |
|
538 *aRetval = gGamepadServiceSingleton->AddGamepad(aID, |
|
539 static_cast<GamepadMappingType>(aMapping), |
|
540 aNumButtons, |
|
541 aNumAxes); |
|
542 return NS_OK; |
|
543 } |
|
544 |
|
545 /* void removeGamepad (in uint32_t index); */ |
|
546 NS_IMETHODIMP GamepadServiceTest::RemoveGamepad(uint32_t aIndex) |
|
547 { |
|
548 gGamepadServiceSingleton->RemoveGamepad(aIndex); |
|
549 return NS_OK; |
|
550 } |
|
551 |
|
552 /* void newButtonEvent (in uint32_t index, in uint32_t button, |
|
553 in boolean pressed); */ |
|
554 NS_IMETHODIMP GamepadServiceTest::NewButtonEvent(uint32_t aIndex, |
|
555 uint32_t aButton, |
|
556 bool aPressed) |
|
557 { |
|
558 gGamepadServiceSingleton->NewButtonEvent(aIndex, aButton, aPressed); |
|
559 return NS_OK; |
|
560 } |
|
561 |
|
562 /* void newAxisMoveEvent (in uint32_t index, in uint32_t axis, |
|
563 in double value); */ |
|
564 NS_IMETHODIMP GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex, |
|
565 uint32_t aAxis, |
|
566 double aValue) |
|
567 { |
|
568 gGamepadServiceSingleton->NewAxisMoveEvent(aIndex, aAxis, aValue); |
|
569 return NS_OK; |
|
570 } |
|
571 |
|
572 } // namespace dom |
|
573 } // namespace mozilla |