1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/gamepad/GamepadService.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,573 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +#include "mozilla/Hal.h" 1.9 +#include "mozilla/ClearOnShutdown.h" 1.10 +#include "mozilla/Preferences.h" 1.11 +#include "mozilla/StaticPtr.h" 1.12 + 1.13 +#include "GamepadService.h" 1.14 +#include "Gamepad.h" 1.15 +#include "nsAutoPtr.h" 1.16 +#include "nsIDOMEvent.h" 1.17 +#include "nsIDOMDocument.h" 1.18 +#include "GeneratedEvents.h" 1.19 +#include "nsIDOMWindow.h" 1.20 +#include "nsIObserver.h" 1.21 +#include "nsIObserverService.h" 1.22 +#include "nsIServiceManager.h" 1.23 +#include "nsITimer.h" 1.24 +#include "nsThreadUtils.h" 1.25 +#include "mozilla/Services.h" 1.26 + 1.27 +#include "mozilla/dom/GamepadAxisMoveEvent.h" 1.28 +#include "mozilla/dom/GamepadButtonEvent.h" 1.29 +#include "mozilla/dom/GamepadEvent.h" 1.30 + 1.31 +#include <cstddef> 1.32 + 1.33 +namespace mozilla { 1.34 +namespace dom { 1.35 + 1.36 +namespace { 1.37 +const char* kGamepadEnabledPref = "dom.gamepad.enabled"; 1.38 +const char* kGamepadEventsEnabledPref = 1.39 + "dom.gamepad.non_standard_events.enabled"; 1.40 +// Amount of time to wait before cleaning up gamepad resources 1.41 +// when no pages are listening for events. 1.42 +const int kCleanupDelayMS = 2000; 1.43 +const nsTArray<nsRefPtr<nsGlobalWindow> >::index_type NoIndex = 1.44 + nsTArray<nsRefPtr<nsGlobalWindow> >::NoIndex; 1.45 + 1.46 +StaticRefPtr<GamepadService> gGamepadServiceSingleton; 1.47 + 1.48 +} // namespace 1.49 + 1.50 +bool GamepadService::sShutdown = false; 1.51 + 1.52 +NS_IMPL_ISUPPORTS(GamepadService, nsIObserver) 1.53 + 1.54 +GamepadService::GamepadService() 1.55 + : mStarted(false), 1.56 + mShuttingDown(false) 1.57 +{ 1.58 + mEnabled = IsAPIEnabled(); 1.59 + mNonstandardEventsEnabled = 1.60 + Preferences::GetBool(kGamepadEventsEnabledPref, false); 1.61 + nsCOMPtr<nsIObserverService> observerService = 1.62 + mozilla::services::GetObserverService(); 1.63 + observerService->AddObserver(this, 1.64 + NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, 1.65 + false); 1.66 +} 1.67 + 1.68 +NS_IMETHODIMP 1.69 +GamepadService::Observe(nsISupports* aSubject, 1.70 + const char* aTopic, 1.71 + const char16_t* aData) 1.72 +{ 1.73 + nsCOMPtr<nsIObserverService> observerService = 1.74 + mozilla::services::GetObserverService(); 1.75 + observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); 1.76 + 1.77 + BeginShutdown(); 1.78 + return NS_OK; 1.79 +} 1.80 + 1.81 +void 1.82 +GamepadService::BeginShutdown() 1.83 +{ 1.84 + mShuttingDown = true; 1.85 + if (mTimer) { 1.86 + mTimer->Cancel(); 1.87 + } 1.88 + if (mStarted) { 1.89 + mozilla::hal::StopMonitoringGamepadStatus(); 1.90 + mStarted = false; 1.91 + } 1.92 + // Don't let windows call back to unregister during shutdown 1.93 + for (uint32_t i = 0; i < mListeners.Length(); i++) { 1.94 + mListeners[i]->SetHasGamepadEventListener(false); 1.95 + } 1.96 + mListeners.Clear(); 1.97 + mGamepads.Clear(); 1.98 + sShutdown = true; 1.99 +} 1.100 + 1.101 +void 1.102 +GamepadService::AddListener(nsGlobalWindow* aWindow) 1.103 +{ 1.104 + if (mShuttingDown) { 1.105 + return; 1.106 + } 1.107 + 1.108 + if (mListeners.IndexOf(aWindow) != NoIndex) { 1.109 + return; // already exists 1.110 + } 1.111 + 1.112 + if (!mStarted && mEnabled) { 1.113 + mozilla::hal::StartMonitoringGamepadStatus(); 1.114 + mStarted = true; 1.115 + } 1.116 + 1.117 + mListeners.AppendElement(aWindow); 1.118 +} 1.119 + 1.120 +void 1.121 +GamepadService::RemoveListener(nsGlobalWindow* aWindow) 1.122 +{ 1.123 + if (mShuttingDown) { 1.124 + // Doesn't matter at this point. It's possible we're being called 1.125 + // as a result of our own destructor here, so just bail out. 1.126 + return; 1.127 + } 1.128 + 1.129 + if (mListeners.IndexOf(aWindow) == NoIndex) { 1.130 + return; // doesn't exist 1.131 + } 1.132 + 1.133 + mListeners.RemoveElement(aWindow); 1.134 + 1.135 + if (mListeners.Length() == 0 && !mShuttingDown && mStarted) { 1.136 + StartCleanupTimer(); 1.137 + } 1.138 +} 1.139 + 1.140 +uint32_t 1.141 +GamepadService::AddGamepad(const char* aId, 1.142 + GamepadMappingType aMapping, 1.143 + uint32_t aNumButtons, 1.144 + uint32_t aNumAxes) 1.145 +{ 1.146 + //TODO: bug 852258: get initial button/axis state 1.147 + nsRefPtr<Gamepad> gamepad = 1.148 + new Gamepad(nullptr, 1.149 + NS_ConvertUTF8toUTF16(nsDependentCString(aId)), 1.150 + 0, 1.151 + aMapping, 1.152 + aNumButtons, 1.153 + aNumAxes); 1.154 + int index = -1; 1.155 + for (uint32_t i = 0; i < mGamepads.Length(); i++) { 1.156 + if (!mGamepads[i]) { 1.157 + mGamepads[i] = gamepad; 1.158 + index = i; 1.159 + break; 1.160 + } 1.161 + } 1.162 + if (index == -1) { 1.163 + mGamepads.AppendElement(gamepad); 1.164 + index = mGamepads.Length() - 1; 1.165 + } 1.166 + 1.167 + gamepad->SetIndex(index); 1.168 + NewConnectionEvent(index, true); 1.169 + 1.170 + return index; 1.171 +} 1.172 + 1.173 +void 1.174 +GamepadService::RemoveGamepad(uint32_t aIndex) 1.175 +{ 1.176 + if (aIndex < mGamepads.Length()) { 1.177 + mGamepads[aIndex]->SetConnected(false); 1.178 + NewConnectionEvent(aIndex, false); 1.179 + // If this is the last entry in the list, just remove it. 1.180 + if (aIndex == mGamepads.Length() - 1) { 1.181 + mGamepads.RemoveElementAt(aIndex); 1.182 + } else { 1.183 + // Otherwise just null it out and leave it, so the 1.184 + // indices of the following entries remain valid. 1.185 + mGamepads[aIndex] = nullptr; 1.186 + } 1.187 + } 1.188 +} 1.189 + 1.190 +void 1.191 +GamepadService::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed) 1.192 +{ 1.193 + // Synthesize a value: 1.0 for pressed, 0.0 for unpressed. 1.194 + NewButtonEvent(aIndex, aButton, aPressed, aPressed ? 1.0L : 0.0L); 1.195 +} 1.196 + 1.197 +void 1.198 +GamepadService::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed, 1.199 + double aValue) 1.200 +{ 1.201 + if (mShuttingDown || aIndex >= mGamepads.Length()) { 1.202 + return; 1.203 + } 1.204 + 1.205 + mGamepads[aIndex]->SetButton(aButton, aPressed, aValue); 1.206 + 1.207 + // Hold on to listeners in a separate array because firing events 1.208 + // can mutate the mListeners array. 1.209 + nsTArray<nsRefPtr<nsGlobalWindow> > listeners(mListeners); 1.210 + 1.211 + for (uint32_t i = listeners.Length(); i > 0 ; ) { 1.212 + --i; 1.213 + 1.214 + // Only send events to non-background windows 1.215 + if (!listeners[i]->IsCurrentInnerWindow() || 1.216 + listeners[i]->GetOuterWindow()->IsBackground()) { 1.217 + continue; 1.218 + } 1.219 + 1.220 + bool first_time = false; 1.221 + if (!WindowHasSeenGamepad(listeners[i], aIndex)) { 1.222 + // This window hasn't seen this gamepad before, so 1.223 + // send a connection event first. 1.224 + SetWindowHasSeenGamepad(listeners[i], aIndex); 1.225 + first_time = true; 1.226 + } 1.227 + 1.228 + nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); 1.229 + if (gamepad) { 1.230 + gamepad->SetButton(aButton, aPressed, aValue); 1.231 + if (first_time) { 1.232 + FireConnectionEvent(listeners[i], gamepad, true); 1.233 + } 1.234 + if (mNonstandardEventsEnabled) { 1.235 + // Fire event 1.236 + FireButtonEvent(listeners[i], gamepad, aButton, aValue); 1.237 + } 1.238 + } 1.239 + } 1.240 +} 1.241 + 1.242 +void 1.243 +GamepadService::FireButtonEvent(EventTarget* aTarget, 1.244 + Gamepad* aGamepad, 1.245 + uint32_t aButton, 1.246 + double aValue) 1.247 +{ 1.248 + nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") : 1.249 + NS_LITERAL_STRING("gamepadbuttonup"); 1.250 + GamepadButtonEventInit init; 1.251 + init.mBubbles = false; 1.252 + init.mCancelable = false; 1.253 + init.mGamepad = aGamepad; 1.254 + init.mButton = aButton; 1.255 + nsRefPtr<GamepadButtonEvent> event = 1.256 + GamepadButtonEvent::Constructor(aTarget, name, init); 1.257 + 1.258 + event->SetTrusted(true); 1.259 + 1.260 + bool defaultActionEnabled = true; 1.261 + aTarget->DispatchEvent(event, &defaultActionEnabled); 1.262 +} 1.263 + 1.264 +void 1.265 +GamepadService::NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue) 1.266 +{ 1.267 + if (mShuttingDown || aIndex >= mGamepads.Length()) { 1.268 + return; 1.269 + } 1.270 + mGamepads[aIndex]->SetAxis(aAxis, aValue); 1.271 + 1.272 + // Hold on to listeners in a separate array because firing events 1.273 + // can mutate the mListeners array. 1.274 + nsTArray<nsRefPtr<nsGlobalWindow> > listeners(mListeners); 1.275 + 1.276 + for (uint32_t i = listeners.Length(); i > 0 ; ) { 1.277 + --i; 1.278 + 1.279 + // Only send events to non-background windows 1.280 + if (!listeners[i]->IsCurrentInnerWindow() || 1.281 + listeners[i]->GetOuterWindow()->IsBackground()) { 1.282 + continue; 1.283 + } 1.284 + 1.285 + bool first_time = false; 1.286 + if (!WindowHasSeenGamepad(listeners[i], aIndex)) { 1.287 + // This window hasn't seen this gamepad before, so 1.288 + // send a connection event first. 1.289 + SetWindowHasSeenGamepad(listeners[i], aIndex); 1.290 + first_time = true; 1.291 + } 1.292 + 1.293 + nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); 1.294 + if (gamepad) { 1.295 + gamepad->SetAxis(aAxis, aValue); 1.296 + if (first_time) { 1.297 + FireConnectionEvent(listeners[i], gamepad, true); 1.298 + } 1.299 + if (mNonstandardEventsEnabled) { 1.300 + // Fire event 1.301 + FireAxisMoveEvent(listeners[i], gamepad, aAxis, aValue); 1.302 + } 1.303 + } 1.304 + } 1.305 +} 1.306 + 1.307 +void 1.308 +GamepadService::FireAxisMoveEvent(EventTarget* aTarget, 1.309 + Gamepad* aGamepad, 1.310 + uint32_t aAxis, 1.311 + double aValue) 1.312 +{ 1.313 + GamepadAxisMoveEventInit init; 1.314 + init.mBubbles = false; 1.315 + init.mCancelable = false; 1.316 + init.mGamepad = aGamepad; 1.317 + init.mAxis = aAxis; 1.318 + init.mValue = aValue; 1.319 + nsRefPtr<GamepadAxisMoveEvent> event = 1.320 + GamepadAxisMoveEvent::Constructor(aTarget, 1.321 + NS_LITERAL_STRING("gamepadaxismove"), 1.322 + init); 1.323 + 1.324 + event->SetTrusted(true); 1.325 + 1.326 + bool defaultActionEnabled = true; 1.327 + aTarget->DispatchEvent(event, &defaultActionEnabled); 1.328 +} 1.329 + 1.330 +void 1.331 +GamepadService::NewConnectionEvent(uint32_t aIndex, bool aConnected) 1.332 +{ 1.333 + if (mShuttingDown || aIndex >= mGamepads.Length()) { 1.334 + return; 1.335 + } 1.336 + 1.337 + // Hold on to listeners in a separate array because firing events 1.338 + // can mutate the mListeners array. 1.339 + nsTArray<nsRefPtr<nsGlobalWindow> > listeners(mListeners); 1.340 + 1.341 + if (aConnected) { 1.342 + for (uint32_t i = listeners.Length(); i > 0 ; ) { 1.343 + --i; 1.344 + 1.345 + // Only send events to non-background windows 1.346 + if (!listeners[i]->IsCurrentInnerWindow() || 1.347 + listeners[i]->GetOuterWindow()->IsBackground()) { 1.348 + continue; 1.349 + } 1.350 + 1.351 + // We don't fire a connected event here unless the window 1.352 + // has seen input from at least one device. 1.353 + if (!listeners[i]->HasSeenGamepadInput()) { 1.354 + continue; 1.355 + } 1.356 + 1.357 + SetWindowHasSeenGamepad(listeners[i], aIndex); 1.358 + 1.359 + nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); 1.360 + if (gamepad) { 1.361 + // Fire event 1.362 + FireConnectionEvent(listeners[i], gamepad, aConnected); 1.363 + } 1.364 + } 1.365 + } else { 1.366 + // For disconnection events, fire one at every window that has received 1.367 + // data from this gamepad. 1.368 + for (uint32_t i = listeners.Length(); i > 0 ; ) { 1.369 + --i; 1.370 + 1.371 + // Even background windows get these events, so we don't have to 1.372 + // deal with the hassle of syncing the state of removed gamepads. 1.373 + 1.374 + if (WindowHasSeenGamepad(listeners[i], aIndex)) { 1.375 + nsRefPtr<Gamepad> gamepad = listeners[i]->GetGamepad(aIndex); 1.376 + if (gamepad) { 1.377 + gamepad->SetConnected(false); 1.378 + // Fire event 1.379 + FireConnectionEvent(listeners[i], gamepad, false); 1.380 + listeners[i]->RemoveGamepad(aIndex); 1.381 + } 1.382 + } 1.383 + } 1.384 + } 1.385 +} 1.386 + 1.387 +void 1.388 +GamepadService::FireConnectionEvent(EventTarget* aTarget, 1.389 + Gamepad* aGamepad, 1.390 + bool aConnected) 1.391 +{ 1.392 + nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") : 1.393 + NS_LITERAL_STRING("gamepaddisconnected"); 1.394 + GamepadEventInit init; 1.395 + init.mBubbles = false; 1.396 + init.mCancelable = false; 1.397 + init.mGamepad = aGamepad; 1.398 + nsRefPtr<GamepadEvent> event = 1.399 + GamepadEvent::Constructor(aTarget, name, init); 1.400 + 1.401 + event->SetTrusted(true); 1.402 + 1.403 + bool defaultActionEnabled = true; 1.404 + aTarget->DispatchEvent(event, &defaultActionEnabled); 1.405 +} 1.406 + 1.407 +void 1.408 +GamepadService::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad) 1.409 +{ 1.410 + if (mShuttingDown || !mEnabled || aIndex > mGamepads.Length()) { 1.411 + return; 1.412 + } 1.413 + 1.414 + aGamepad->SyncState(mGamepads[aIndex]); 1.415 +} 1.416 + 1.417 +// static 1.418 +already_AddRefed<GamepadService> 1.419 +GamepadService::GetService() 1.420 +{ 1.421 + if (sShutdown) { 1.422 + return nullptr; 1.423 + } 1.424 + 1.425 + if (!gGamepadServiceSingleton) { 1.426 + gGamepadServiceSingleton = new GamepadService(); 1.427 + ClearOnShutdown(&gGamepadServiceSingleton); 1.428 + } 1.429 + nsRefPtr<GamepadService> service(gGamepadServiceSingleton); 1.430 + return service.forget(); 1.431 +} 1.432 + 1.433 +// static 1.434 +bool 1.435 +GamepadService::IsAPIEnabled() { 1.436 + return Preferences::GetBool(kGamepadEnabledPref, false); 1.437 +} 1.438 + 1.439 +bool 1.440 +GamepadService::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) 1.441 +{ 1.442 + nsRefPtr<Gamepad> gamepad = aWindow->GetGamepad(aIndex); 1.443 + return gamepad != nullptr; 1.444 +} 1.445 + 1.446 +void 1.447 +GamepadService::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow, 1.448 + uint32_t aIndex, 1.449 + bool aHasSeen) 1.450 +{ 1.451 + if (mListeners.IndexOf(aWindow) == NoIndex) { 1.452 + // This window isn't even listening for gamepad events. 1.453 + return; 1.454 + } 1.455 + 1.456 + if (aHasSeen) { 1.457 + aWindow->SetHasSeenGamepadInput(true); 1.458 + nsCOMPtr<nsISupports> window = ToSupports(aWindow); 1.459 + nsRefPtr<Gamepad> gamepad = mGamepads[aIndex]->Clone(window); 1.460 + aWindow->AddGamepad(aIndex, gamepad); 1.461 + } else { 1.462 + aWindow->RemoveGamepad(aIndex); 1.463 + } 1.464 +} 1.465 + 1.466 +// static 1.467 +void 1.468 +GamepadService::TimeoutHandler(nsITimer* aTimer, void* aClosure) 1.469 +{ 1.470 + // the reason that we use self, instead of just using nsITimerCallback or nsIObserver 1.471 + // is so that subclasses are free to use timers without worry about the base classes's 1.472 + // usage. 1.473 + GamepadService* self = reinterpret_cast<GamepadService*>(aClosure); 1.474 + if (!self) { 1.475 + NS_ERROR("no self"); 1.476 + return; 1.477 + } 1.478 + 1.479 + if (self->mShuttingDown) { 1.480 + return; 1.481 + } 1.482 + 1.483 + if (self->mListeners.Length() == 0) { 1.484 + mozilla::hal::StopMonitoringGamepadStatus(); 1.485 + self->mStarted = false; 1.486 + if (!self->mGamepads.IsEmpty()) { 1.487 + self->mGamepads.Clear(); 1.488 + } 1.489 + } 1.490 +} 1.491 + 1.492 +void 1.493 +GamepadService::StartCleanupTimer() 1.494 +{ 1.495 + if (mTimer) { 1.496 + mTimer->Cancel(); 1.497 + } 1.498 + 1.499 + mTimer = do_CreateInstance("@mozilla.org/timer;1"); 1.500 + if (mTimer) { 1.501 + mTimer->InitWithFuncCallback(TimeoutHandler, 1.502 + this, 1.503 + kCleanupDelayMS, 1.504 + nsITimer::TYPE_ONE_SHOT); 1.505 + } 1.506 +} 1.507 + 1.508 +/* 1.509 + * Implementation of the test service. This is just to provide a simple binding 1.510 + * of the GamepadService to JavaScript via XPCOM so that we can write Mochitests 1.511 + * that add and remove fake gamepads, avoiding the platform-specific backends. 1.512 + */ 1.513 +NS_IMPL_ISUPPORTS(GamepadServiceTest, nsIGamepadServiceTest) 1.514 + 1.515 +GamepadServiceTest* GamepadServiceTest::sSingleton = nullptr; 1.516 + 1.517 +// static 1.518 +already_AddRefed<GamepadServiceTest> 1.519 +GamepadServiceTest::CreateService() 1.520 +{ 1.521 + if (sSingleton == nullptr) { 1.522 + sSingleton = new GamepadServiceTest(); 1.523 + } 1.524 + nsRefPtr<GamepadServiceTest> service = sSingleton; 1.525 + return service.forget(); 1.526 +} 1.527 + 1.528 +GamepadServiceTest::GamepadServiceTest() 1.529 +{ 1.530 + /* member initializers and constructor code */ 1.531 + nsRefPtr<GamepadService> service = GamepadService::GetService(); 1.532 +} 1.533 + 1.534 +/* uint32_t addGamepad (in string id, in unsigned long mapping, in unsigned long numButtons, in unsigned long numAxes); */ 1.535 +NS_IMETHODIMP GamepadServiceTest::AddGamepad(const char* aID, 1.536 + uint32_t aMapping, 1.537 + uint32_t aNumButtons, 1.538 + uint32_t aNumAxes, 1.539 + uint32_t* aRetval) 1.540 +{ 1.541 + *aRetval = gGamepadServiceSingleton->AddGamepad(aID, 1.542 + static_cast<GamepadMappingType>(aMapping), 1.543 + aNumButtons, 1.544 + aNumAxes); 1.545 + return NS_OK; 1.546 +} 1.547 + 1.548 +/* void removeGamepad (in uint32_t index); */ 1.549 +NS_IMETHODIMP GamepadServiceTest::RemoveGamepad(uint32_t aIndex) 1.550 +{ 1.551 + gGamepadServiceSingleton->RemoveGamepad(aIndex); 1.552 + return NS_OK; 1.553 +} 1.554 + 1.555 +/* void newButtonEvent (in uint32_t index, in uint32_t button, 1.556 + in boolean pressed); */ 1.557 +NS_IMETHODIMP GamepadServiceTest::NewButtonEvent(uint32_t aIndex, 1.558 + uint32_t aButton, 1.559 + bool aPressed) 1.560 +{ 1.561 + gGamepadServiceSingleton->NewButtonEvent(aIndex, aButton, aPressed); 1.562 + return NS_OK; 1.563 +} 1.564 + 1.565 +/* void newAxisMoveEvent (in uint32_t index, in uint32_t axis, 1.566 + in double value); */ 1.567 +NS_IMETHODIMP GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex, 1.568 + uint32_t aAxis, 1.569 + double aValue) 1.570 +{ 1.571 + gGamepadServiceSingleton->NewAxisMoveEvent(aIndex, aAxis, aValue); 1.572 + return NS_OK; 1.573 +} 1.574 + 1.575 +} // namespace dom 1.576 +} // namespace mozilla