michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/Hal.h" michael@0: #include "mozilla/ClearOnShutdown.h" michael@0: #include "mozilla/Preferences.h" michael@0: #include "mozilla/StaticPtr.h" michael@0: michael@0: #include "GamepadService.h" michael@0: #include "Gamepad.h" michael@0: #include "nsAutoPtr.h" michael@0: #include "nsIDOMEvent.h" michael@0: #include "nsIDOMDocument.h" michael@0: #include "GeneratedEvents.h" michael@0: #include "nsIDOMWindow.h" michael@0: #include "nsIObserver.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsITimer.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "mozilla/Services.h" michael@0: michael@0: #include "mozilla/dom/GamepadAxisMoveEvent.h" michael@0: #include "mozilla/dom/GamepadButtonEvent.h" michael@0: #include "mozilla/dom/GamepadEvent.h" michael@0: michael@0: #include michael@0: michael@0: namespace mozilla { michael@0: namespace dom { michael@0: michael@0: namespace { michael@0: const char* kGamepadEnabledPref = "dom.gamepad.enabled"; michael@0: const char* kGamepadEventsEnabledPref = michael@0: "dom.gamepad.non_standard_events.enabled"; michael@0: // Amount of time to wait before cleaning up gamepad resources michael@0: // when no pages are listening for events. michael@0: const int kCleanupDelayMS = 2000; michael@0: const nsTArray >::index_type NoIndex = michael@0: nsTArray >::NoIndex; michael@0: michael@0: StaticRefPtr gGamepadServiceSingleton; michael@0: michael@0: } // namespace michael@0: michael@0: bool GamepadService::sShutdown = false; michael@0: michael@0: NS_IMPL_ISUPPORTS(GamepadService, nsIObserver) michael@0: michael@0: GamepadService::GamepadService() michael@0: : mStarted(false), michael@0: mShuttingDown(false) michael@0: { michael@0: mEnabled = IsAPIEnabled(); michael@0: mNonstandardEventsEnabled = michael@0: Preferences::GetBool(kGamepadEventsEnabledPref, false); michael@0: nsCOMPtr observerService = michael@0: mozilla::services::GetObserverService(); michael@0: observerService->AddObserver(this, michael@0: NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, michael@0: false); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: GamepadService::Observe(nsISupports* aSubject, michael@0: const char* aTopic, michael@0: const char16_t* aData) michael@0: { michael@0: nsCOMPtr observerService = michael@0: mozilla::services::GetObserverService(); michael@0: observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); michael@0: michael@0: BeginShutdown(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: GamepadService::BeginShutdown() michael@0: { michael@0: mShuttingDown = true; michael@0: if (mTimer) { michael@0: mTimer->Cancel(); michael@0: } michael@0: if (mStarted) { michael@0: mozilla::hal::StopMonitoringGamepadStatus(); michael@0: mStarted = false; michael@0: } michael@0: // Don't let windows call back to unregister during shutdown michael@0: for (uint32_t i = 0; i < mListeners.Length(); i++) { michael@0: mListeners[i]->SetHasGamepadEventListener(false); michael@0: } michael@0: mListeners.Clear(); michael@0: mGamepads.Clear(); michael@0: sShutdown = true; michael@0: } michael@0: michael@0: void michael@0: GamepadService::AddListener(nsGlobalWindow* aWindow) michael@0: { michael@0: if (mShuttingDown) { michael@0: return; michael@0: } michael@0: michael@0: if (mListeners.IndexOf(aWindow) != NoIndex) { michael@0: return; // already exists michael@0: } michael@0: michael@0: if (!mStarted && mEnabled) { michael@0: mozilla::hal::StartMonitoringGamepadStatus(); michael@0: mStarted = true; michael@0: } michael@0: michael@0: mListeners.AppendElement(aWindow); michael@0: } michael@0: michael@0: void michael@0: GamepadService::RemoveListener(nsGlobalWindow* aWindow) michael@0: { michael@0: if (mShuttingDown) { michael@0: // Doesn't matter at this point. It's possible we're being called michael@0: // as a result of our own destructor here, so just bail out. michael@0: return; michael@0: } michael@0: michael@0: if (mListeners.IndexOf(aWindow) == NoIndex) { michael@0: return; // doesn't exist michael@0: } michael@0: michael@0: mListeners.RemoveElement(aWindow); michael@0: michael@0: if (mListeners.Length() == 0 && !mShuttingDown && mStarted) { michael@0: StartCleanupTimer(); michael@0: } michael@0: } michael@0: michael@0: uint32_t michael@0: GamepadService::AddGamepad(const char* aId, michael@0: GamepadMappingType aMapping, michael@0: uint32_t aNumButtons, michael@0: uint32_t aNumAxes) michael@0: { michael@0: //TODO: bug 852258: get initial button/axis state michael@0: nsRefPtr gamepad = michael@0: new Gamepad(nullptr, michael@0: NS_ConvertUTF8toUTF16(nsDependentCString(aId)), michael@0: 0, michael@0: aMapping, michael@0: aNumButtons, michael@0: aNumAxes); michael@0: int index = -1; michael@0: for (uint32_t i = 0; i < mGamepads.Length(); i++) { michael@0: if (!mGamepads[i]) { michael@0: mGamepads[i] = gamepad; michael@0: index = i; michael@0: break; michael@0: } michael@0: } michael@0: if (index == -1) { michael@0: mGamepads.AppendElement(gamepad); michael@0: index = mGamepads.Length() - 1; michael@0: } michael@0: michael@0: gamepad->SetIndex(index); michael@0: NewConnectionEvent(index, true); michael@0: michael@0: return index; michael@0: } michael@0: michael@0: void michael@0: GamepadService::RemoveGamepad(uint32_t aIndex) michael@0: { michael@0: if (aIndex < mGamepads.Length()) { michael@0: mGamepads[aIndex]->SetConnected(false); michael@0: NewConnectionEvent(aIndex, false); michael@0: // If this is the last entry in the list, just remove it. michael@0: if (aIndex == mGamepads.Length() - 1) { michael@0: mGamepads.RemoveElementAt(aIndex); michael@0: } else { michael@0: // Otherwise just null it out and leave it, so the michael@0: // indices of the following entries remain valid. michael@0: mGamepads[aIndex] = nullptr; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: GamepadService::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed) michael@0: { michael@0: // Synthesize a value: 1.0 for pressed, 0.0 for unpressed. michael@0: NewButtonEvent(aIndex, aButton, aPressed, aPressed ? 1.0L : 0.0L); michael@0: } michael@0: michael@0: void michael@0: GamepadService::NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed, michael@0: double aValue) michael@0: { michael@0: if (mShuttingDown || aIndex >= mGamepads.Length()) { michael@0: return; michael@0: } michael@0: michael@0: mGamepads[aIndex]->SetButton(aButton, aPressed, aValue); michael@0: michael@0: // Hold on to listeners in a separate array because firing events michael@0: // can mutate the mListeners array. michael@0: nsTArray > listeners(mListeners); michael@0: michael@0: for (uint32_t i = listeners.Length(); i > 0 ; ) { michael@0: --i; michael@0: michael@0: // Only send events to non-background windows michael@0: if (!listeners[i]->IsCurrentInnerWindow() || michael@0: listeners[i]->GetOuterWindow()->IsBackground()) { michael@0: continue; michael@0: } michael@0: michael@0: bool first_time = false; michael@0: if (!WindowHasSeenGamepad(listeners[i], aIndex)) { michael@0: // This window hasn't seen this gamepad before, so michael@0: // send a connection event first. michael@0: SetWindowHasSeenGamepad(listeners[i], aIndex); michael@0: first_time = true; michael@0: } michael@0: michael@0: nsRefPtr gamepad = listeners[i]->GetGamepad(aIndex); michael@0: if (gamepad) { michael@0: gamepad->SetButton(aButton, aPressed, aValue); michael@0: if (first_time) { michael@0: FireConnectionEvent(listeners[i], gamepad, true); michael@0: } michael@0: if (mNonstandardEventsEnabled) { michael@0: // Fire event michael@0: FireButtonEvent(listeners[i], gamepad, aButton, aValue); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: GamepadService::FireButtonEvent(EventTarget* aTarget, michael@0: Gamepad* aGamepad, michael@0: uint32_t aButton, michael@0: double aValue) michael@0: { michael@0: nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") : michael@0: NS_LITERAL_STRING("gamepadbuttonup"); michael@0: GamepadButtonEventInit init; michael@0: init.mBubbles = false; michael@0: init.mCancelable = false; michael@0: init.mGamepad = aGamepad; michael@0: init.mButton = aButton; michael@0: nsRefPtr event = michael@0: GamepadButtonEvent::Constructor(aTarget, name, init); michael@0: michael@0: event->SetTrusted(true); michael@0: michael@0: bool defaultActionEnabled = true; michael@0: aTarget->DispatchEvent(event, &defaultActionEnabled); michael@0: } michael@0: michael@0: void michael@0: GamepadService::NewAxisMoveEvent(uint32_t aIndex, uint32_t aAxis, double aValue) michael@0: { michael@0: if (mShuttingDown || aIndex >= mGamepads.Length()) { michael@0: return; michael@0: } michael@0: mGamepads[aIndex]->SetAxis(aAxis, aValue); michael@0: michael@0: // Hold on to listeners in a separate array because firing events michael@0: // can mutate the mListeners array. michael@0: nsTArray > listeners(mListeners); michael@0: michael@0: for (uint32_t i = listeners.Length(); i > 0 ; ) { michael@0: --i; michael@0: michael@0: // Only send events to non-background windows michael@0: if (!listeners[i]->IsCurrentInnerWindow() || michael@0: listeners[i]->GetOuterWindow()->IsBackground()) { michael@0: continue; michael@0: } michael@0: michael@0: bool first_time = false; michael@0: if (!WindowHasSeenGamepad(listeners[i], aIndex)) { michael@0: // This window hasn't seen this gamepad before, so michael@0: // send a connection event first. michael@0: SetWindowHasSeenGamepad(listeners[i], aIndex); michael@0: first_time = true; michael@0: } michael@0: michael@0: nsRefPtr gamepad = listeners[i]->GetGamepad(aIndex); michael@0: if (gamepad) { michael@0: gamepad->SetAxis(aAxis, aValue); michael@0: if (first_time) { michael@0: FireConnectionEvent(listeners[i], gamepad, true); michael@0: } michael@0: if (mNonstandardEventsEnabled) { michael@0: // Fire event michael@0: FireAxisMoveEvent(listeners[i], gamepad, aAxis, aValue); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: GamepadService::FireAxisMoveEvent(EventTarget* aTarget, michael@0: Gamepad* aGamepad, michael@0: uint32_t aAxis, michael@0: double aValue) michael@0: { michael@0: GamepadAxisMoveEventInit init; michael@0: init.mBubbles = false; michael@0: init.mCancelable = false; michael@0: init.mGamepad = aGamepad; michael@0: init.mAxis = aAxis; michael@0: init.mValue = aValue; michael@0: nsRefPtr event = michael@0: GamepadAxisMoveEvent::Constructor(aTarget, michael@0: NS_LITERAL_STRING("gamepadaxismove"), michael@0: init); michael@0: michael@0: event->SetTrusted(true); michael@0: michael@0: bool defaultActionEnabled = true; michael@0: aTarget->DispatchEvent(event, &defaultActionEnabled); michael@0: } michael@0: michael@0: void michael@0: GamepadService::NewConnectionEvent(uint32_t aIndex, bool aConnected) michael@0: { michael@0: if (mShuttingDown || aIndex >= mGamepads.Length()) { michael@0: return; michael@0: } michael@0: michael@0: // Hold on to listeners in a separate array because firing events michael@0: // can mutate the mListeners array. michael@0: nsTArray > listeners(mListeners); michael@0: michael@0: if (aConnected) { michael@0: for (uint32_t i = listeners.Length(); i > 0 ; ) { michael@0: --i; michael@0: michael@0: // Only send events to non-background windows michael@0: if (!listeners[i]->IsCurrentInnerWindow() || michael@0: listeners[i]->GetOuterWindow()->IsBackground()) { michael@0: continue; michael@0: } michael@0: michael@0: // We don't fire a connected event here unless the window michael@0: // has seen input from at least one device. michael@0: if (!listeners[i]->HasSeenGamepadInput()) { michael@0: continue; michael@0: } michael@0: michael@0: SetWindowHasSeenGamepad(listeners[i], aIndex); michael@0: michael@0: nsRefPtr gamepad = listeners[i]->GetGamepad(aIndex); michael@0: if (gamepad) { michael@0: // Fire event michael@0: FireConnectionEvent(listeners[i], gamepad, aConnected); michael@0: } michael@0: } michael@0: } else { michael@0: // For disconnection events, fire one at every window that has received michael@0: // data from this gamepad. michael@0: for (uint32_t i = listeners.Length(); i > 0 ; ) { michael@0: --i; michael@0: michael@0: // Even background windows get these events, so we don't have to michael@0: // deal with the hassle of syncing the state of removed gamepads. michael@0: michael@0: if (WindowHasSeenGamepad(listeners[i], aIndex)) { michael@0: nsRefPtr gamepad = listeners[i]->GetGamepad(aIndex); michael@0: if (gamepad) { michael@0: gamepad->SetConnected(false); michael@0: // Fire event michael@0: FireConnectionEvent(listeners[i], gamepad, false); michael@0: listeners[i]->RemoveGamepad(aIndex); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: GamepadService::FireConnectionEvent(EventTarget* aTarget, michael@0: Gamepad* aGamepad, michael@0: bool aConnected) michael@0: { michael@0: nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") : michael@0: NS_LITERAL_STRING("gamepaddisconnected"); michael@0: GamepadEventInit init; michael@0: init.mBubbles = false; michael@0: init.mCancelable = false; michael@0: init.mGamepad = aGamepad; michael@0: nsRefPtr event = michael@0: GamepadEvent::Constructor(aTarget, name, init); michael@0: michael@0: event->SetTrusted(true); michael@0: michael@0: bool defaultActionEnabled = true; michael@0: aTarget->DispatchEvent(event, &defaultActionEnabled); michael@0: } michael@0: michael@0: void michael@0: GamepadService::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad) michael@0: { michael@0: if (mShuttingDown || !mEnabled || aIndex > mGamepads.Length()) { michael@0: return; michael@0: } michael@0: michael@0: aGamepad->SyncState(mGamepads[aIndex]); michael@0: } michael@0: michael@0: // static michael@0: already_AddRefed michael@0: GamepadService::GetService() michael@0: { michael@0: if (sShutdown) { michael@0: return nullptr; michael@0: } michael@0: michael@0: if (!gGamepadServiceSingleton) { michael@0: gGamepadServiceSingleton = new GamepadService(); michael@0: ClearOnShutdown(&gGamepadServiceSingleton); michael@0: } michael@0: nsRefPtr service(gGamepadServiceSingleton); michael@0: return service.forget(); michael@0: } michael@0: michael@0: // static michael@0: bool michael@0: GamepadService::IsAPIEnabled() { michael@0: return Preferences::GetBool(kGamepadEnabledPref, false); michael@0: } michael@0: michael@0: bool michael@0: GamepadService::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) michael@0: { michael@0: nsRefPtr gamepad = aWindow->GetGamepad(aIndex); michael@0: return gamepad != nullptr; michael@0: } michael@0: michael@0: void michael@0: GamepadService::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow, michael@0: uint32_t aIndex, michael@0: bool aHasSeen) michael@0: { michael@0: if (mListeners.IndexOf(aWindow) == NoIndex) { michael@0: // This window isn't even listening for gamepad events. michael@0: return; michael@0: } michael@0: michael@0: if (aHasSeen) { michael@0: aWindow->SetHasSeenGamepadInput(true); michael@0: nsCOMPtr window = ToSupports(aWindow); michael@0: nsRefPtr gamepad = mGamepads[aIndex]->Clone(window); michael@0: aWindow->AddGamepad(aIndex, gamepad); michael@0: } else { michael@0: aWindow->RemoveGamepad(aIndex); michael@0: } michael@0: } michael@0: michael@0: // static michael@0: void michael@0: GamepadService::TimeoutHandler(nsITimer* aTimer, void* aClosure) michael@0: { michael@0: // the reason that we use self, instead of just using nsITimerCallback or nsIObserver michael@0: // is so that subclasses are free to use timers without worry about the base classes's michael@0: // usage. michael@0: GamepadService* self = reinterpret_cast(aClosure); michael@0: if (!self) { michael@0: NS_ERROR("no self"); michael@0: return; michael@0: } michael@0: michael@0: if (self->mShuttingDown) { michael@0: return; michael@0: } michael@0: michael@0: if (self->mListeners.Length() == 0) { michael@0: mozilla::hal::StopMonitoringGamepadStatus(); michael@0: self->mStarted = false; michael@0: if (!self->mGamepads.IsEmpty()) { michael@0: self->mGamepads.Clear(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: GamepadService::StartCleanupTimer() michael@0: { michael@0: if (mTimer) { michael@0: mTimer->Cancel(); michael@0: } michael@0: michael@0: mTimer = do_CreateInstance("@mozilla.org/timer;1"); michael@0: if (mTimer) { michael@0: mTimer->InitWithFuncCallback(TimeoutHandler, michael@0: this, michael@0: kCleanupDelayMS, michael@0: nsITimer::TYPE_ONE_SHOT); michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * Implementation of the test service. This is just to provide a simple binding michael@0: * of the GamepadService to JavaScript via XPCOM so that we can write Mochitests michael@0: * that add and remove fake gamepads, avoiding the platform-specific backends. michael@0: */ michael@0: NS_IMPL_ISUPPORTS(GamepadServiceTest, nsIGamepadServiceTest) michael@0: michael@0: GamepadServiceTest* GamepadServiceTest::sSingleton = nullptr; michael@0: michael@0: // static michael@0: already_AddRefed michael@0: GamepadServiceTest::CreateService() michael@0: { michael@0: if (sSingleton == nullptr) { michael@0: sSingleton = new GamepadServiceTest(); michael@0: } michael@0: nsRefPtr service = sSingleton; michael@0: return service.forget(); michael@0: } michael@0: michael@0: GamepadServiceTest::GamepadServiceTest() michael@0: { michael@0: /* member initializers and constructor code */ michael@0: nsRefPtr service = GamepadService::GetService(); michael@0: } michael@0: michael@0: /* uint32_t addGamepad (in string id, in unsigned long mapping, in unsigned long numButtons, in unsigned long numAxes); */ michael@0: NS_IMETHODIMP GamepadServiceTest::AddGamepad(const char* aID, michael@0: uint32_t aMapping, michael@0: uint32_t aNumButtons, michael@0: uint32_t aNumAxes, michael@0: uint32_t* aRetval) michael@0: { michael@0: *aRetval = gGamepadServiceSingleton->AddGamepad(aID, michael@0: static_cast(aMapping), michael@0: aNumButtons, michael@0: aNumAxes); michael@0: return NS_OK; michael@0: } michael@0: michael@0: /* void removeGamepad (in uint32_t index); */ michael@0: NS_IMETHODIMP GamepadServiceTest::RemoveGamepad(uint32_t aIndex) michael@0: { michael@0: gGamepadServiceSingleton->RemoveGamepad(aIndex); michael@0: return NS_OK; michael@0: } michael@0: michael@0: /* void newButtonEvent (in uint32_t index, in uint32_t button, michael@0: in boolean pressed); */ michael@0: NS_IMETHODIMP GamepadServiceTest::NewButtonEvent(uint32_t aIndex, michael@0: uint32_t aButton, michael@0: bool aPressed) michael@0: { michael@0: gGamepadServiceSingleton->NewButtonEvent(aIndex, aButton, aPressed); michael@0: return NS_OK; michael@0: } michael@0: michael@0: /* void newAxisMoveEvent (in uint32_t index, in uint32_t axis, michael@0: in double value); */ michael@0: NS_IMETHODIMP GamepadServiceTest::NewAxisMoveEvent(uint32_t aIndex, michael@0: uint32_t aAxis, michael@0: double aValue) michael@0: { michael@0: gGamepadServiceSingleton->NewAxisMoveEvent(aIndex, aAxis, aValue); michael@0: return NS_OK; michael@0: } michael@0: michael@0: } // namespace dom michael@0: } // namespace mozilla