/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/GamepadManager.h" #include "mozilla/dom/Gamepad.h" #include "mozilla/dom/GamepadAxisMoveEvent.h" #include "mozilla/dom/GamepadButtonEvent.h" #include "mozilla/dom/GamepadEvent.h" #include "mozilla/dom/GamepadEventChannelChild.h" #include "mozilla/dom/GamepadMonitoring.h" #include "mozilla/dom/Promise.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPtr.h" #include "nsContentUtils.h" #include "nsGlobalWindow.h" #include "nsIObserver.h" #include "nsIObserverService.h" #include "nsThreadUtils.h" #include "VRManagerChild.h" #include "mozilla/Services.h" #include "mozilla/Unused.h" #include using namespace mozilla::ipc; namespace mozilla::dom { namespace { const nsTArray>::index_type NoIndex = nsTArray>::NoIndex; bool sShutdown = false; StaticRefPtr gGamepadManagerSingleton; // A threshold value of axis move to determine the first // intent. const float AXIS_FIRST_INTENT_THRESHOLD_VALUE = 0.1f; } // namespace NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver) GamepadManager::GamepadManager() : mEnabled(false), mNonstandardEventsEnabled(false), mShuttingDown(false), mPromiseID(0) {} nsresult GamepadManager::Init() { mEnabled = StaticPrefs::dom_gamepad_enabled(); mNonstandardEventsEnabled = StaticPrefs::dom_gamepad_non_standard_events_enabled(); nsCOMPtr observerService = mozilla::services::GetObserverService(); if (NS_WARN_IF(!observerService)) { return NS_ERROR_FAILURE; } nsresult rv; rv = observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } NS_IMETHODIMP GamepadManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); } BeginShutdown(); return NS_OK; } void GamepadManager::StopMonitoring() { if (mChannelChild) { PGamepadEventChannelChild::Send__delete__(mChannelChild); mChannelChild = nullptr; } if (gfx::VRManagerChild::IsCreated()) { gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); vm->SendControllerListenerRemoved(); } mGamepads.Clear(); } void GamepadManager::BeginShutdown() { mShuttingDown = true; StopMonitoring(); // Don't let windows call back to unregister during shutdown for (uint32_t i = 0; i < mListeners.Length(); i++) { mListeners[i]->SetHasGamepadEventListener(false); } mListeners.Clear(); sShutdown = true; } void GamepadManager::AddListener(nsGlobalWindowInner* aWindow) { MOZ_ASSERT(aWindow); MOZ_ASSERT(NS_IsMainThread()); // IPDL child has not been created if (!mChannelChild) { PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!actor)) { // We are probably shutting down. return; } RefPtr child(GamepadEventChannelChild::Create()); if (!actor->SendPGamepadEventChannelConstructor(child.get())) { // We are probably shutting down. return; } mChannelChild = child; if (gfx::VRManagerChild::IsCreated()) { // Construct VRManagerChannel and ask adding the connected // VR controllers to GamepadManager gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); vm->SendControllerListenerAdded(); } } if (!mEnabled || mShuttingDown || aWindow->ShouldResistFingerprinting()) { return; } if (mListeners.IndexOf(aWindow) != NoIndex) { return; // already exists } mListeners.AppendElement(aWindow); } void GamepadManager::RemoveListener(nsGlobalWindowInner* aWindow) { MOZ_ASSERT(aWindow); if (mShuttingDown) { // Doesn't matter at this point. It's possible we're being called // as a result of our own destructor here, so just bail out. return; } if (mListeners.IndexOf(aWindow) == NoIndex) { return; // doesn't exist } for (const auto& key : mGamepads.Keys()) { aWindow->RemoveGamepad(key); } mListeners.RemoveElement(aWindow); if (mListeners.IsEmpty()) { StopMonitoring(); } } already_AddRefed GamepadManager::GetGamepad( GamepadHandle aHandle) const { RefPtr gamepad; if (mGamepads.Get(aHandle, getter_AddRefs(gamepad))) { return gamepad.forget(); } return nullptr; } void GamepadManager::AddGamepad(GamepadHandle aHandle, const nsAString& aId, GamepadMappingType aMapping, GamepadHand aHand, uint32_t aDisplayID, uint32_t aNumButtons, uint32_t aNumAxes, uint32_t aNumHaptics, uint32_t aNumLightIndicator, uint32_t aNumTouchEvents) { // TODO: bug 852258: get initial button/axis state RefPtr newGamepad = new Gamepad(nullptr, aId, 0, // index is set by global window aHandle, aMapping, aHand, aDisplayID, aNumButtons, aNumAxes, aNumHaptics, aNumLightIndicator, aNumTouchEvents); // We store the gamepad related to its index given by the parent process, // and no duplicate index is allowed. MOZ_ASSERT(!mGamepads.Contains(aHandle)); mGamepads.InsertOrUpdate(aHandle, std::move(newGamepad)); NewConnectionEvent(aHandle, true); } void GamepadManager::RemoveGamepad(GamepadHandle aHandle) { RefPtr gamepad = GetGamepad(aHandle); if (!gamepad) { NS_WARNING("Trying to delete gamepad with invalid index"); return; } gamepad->SetConnected(false); NewConnectionEvent(aHandle, false); mGamepads.Remove(aHandle); } void GamepadManager::FireButtonEvent(EventTarget* aTarget, Gamepad* aGamepad, uint32_t aButton, double aValue) { nsString name = aValue == 1.0L ? u"gamepadbuttondown"_ns : u"gamepadbuttonup"_ns; GamepadButtonEventInit init; init.mBubbles = false; init.mCancelable = false; init.mGamepad = aGamepad; init.mButton = aButton; RefPtr event = GamepadButtonEvent::Constructor(aTarget, name, init); event->SetTrusted(true); aTarget->DispatchEvent(*event); } void GamepadManager::FireAxisMoveEvent(EventTarget* aTarget, Gamepad* aGamepad, uint32_t aAxis, double aValue) { GamepadAxisMoveEventInit init; init.mBubbles = false; init.mCancelable = false; init.mGamepad = aGamepad; init.mAxis = aAxis; init.mValue = aValue; RefPtr event = GamepadAxisMoveEvent::Constructor(aTarget, u"gamepadaxismove"_ns, init); event->SetTrusted(true); aTarget->DispatchEvent(*event); } void GamepadManager::NewConnectionEvent(GamepadHandle aHandle, bool aConnected) { if (mShuttingDown) { return; } RefPtr gamepad = GetGamepad(aHandle); if (!gamepad) { return; } // Hold on to listeners in a separate array because firing events // can mutate the mListeners array. nsTArray> listeners(mListeners.Clone()); if (aConnected) { for (uint32_t i = 0; i < listeners.Length(); i++) { #ifdef NIGHTLY_BUILD // Don't fire a gamepadconnected event unless it's a secure context if (!listeners[i]->IsSecureContext()) { continue; } #endif // Do not fire gamepadconnected and gamepaddisconnected events when // privacy.resistFingerprinting is true. if (listeners[i]->ShouldResistFingerprinting()) { continue; } // Only send events to non-background windows if (!listeners[i]->IsCurrentInnerWindow() || listeners[i]->GetOuterWindow()->IsBackground()) { continue; } // We don't fire a connected event here unless the window // has seen input from at least one device. if (!listeners[i]->HasSeenGamepadInput()) { continue; } SetWindowHasSeenGamepad(listeners[i], aHandle); RefPtr listenerGamepad = listeners[i]->GetGamepad(aHandle); if (listenerGamepad) { // Fire event FireConnectionEvent(listeners[i], listenerGamepad, aConnected); } } } else { // For disconnection events, fire one at every window that has received // data from this gamepad. for (uint32_t i = 0; i < listeners.Length(); i++) { // Even background windows get these events, so we don't have to // deal with the hassle of syncing the state of removed gamepads. // Do not fire gamepadconnected and gamepaddisconnected events when // privacy.resistFingerprinting is true. if (listeners[i]->ShouldResistFingerprinting()) { continue; } if (WindowHasSeenGamepad(listeners[i], aHandle)) { RefPtr listenerGamepad = listeners[i]->GetGamepad(aHandle); if (listenerGamepad) { listenerGamepad->SetConnected(false); // Fire event FireConnectionEvent(listeners[i], listenerGamepad, false); listeners[i]->RemoveGamepad(aHandle); } } } } } void GamepadManager::FireConnectionEvent(EventTarget* aTarget, Gamepad* aGamepad, bool aConnected) { nsString name = aConnected ? u"gamepadconnected"_ns : u"gamepaddisconnected"_ns; GamepadEventInit init; init.mBubbles = false; init.mCancelable = false; init.mGamepad = aGamepad; RefPtr event = GamepadEvent::Constructor(aTarget, name, init); event->SetTrusted(true); aTarget->DispatchEvent(*event); } void GamepadManager::SyncGamepadState(GamepadHandle aHandle, nsGlobalWindowInner* aWindow, Gamepad* aGamepad) { if (mShuttingDown || !mEnabled || aWindow->ShouldResistFingerprinting()) { return; } RefPtr gamepad = GetGamepad(aHandle); if (!gamepad) { return; } aGamepad->SyncState(gamepad); } // static bool GamepadManager::IsServiceRunning() { return !!gGamepadManagerSingleton; } // static already_AddRefed GamepadManager::GetService() { if (sShutdown) { return nullptr; } if (!gGamepadManagerSingleton) { RefPtr manager = new GamepadManager(); nsresult rv = manager->Init(); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } gGamepadManagerSingleton = manager; ClearOnShutdown(&gGamepadManagerSingleton); } RefPtr service(gGamepadManagerSingleton); return service.forget(); } bool GamepadManager::AxisMoveIsFirstIntent(nsGlobalWindowInner* aWindow, GamepadHandle aHandle, const GamepadChangeEvent& aEvent) { const GamepadChangeEventBody& body = aEvent.body(); if (!WindowHasSeenGamepad(aWindow, aHandle) && body.type() == GamepadChangeEventBody::TGamepadAxisInformation) { // Some controllers would send small axis values even they are just idle. // To avoid controllers be activated without its first intent. const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); if (abs(a.value()) < AXIS_FIRST_INTENT_THRESHOLD_VALUE) { return false; } } return true; } bool GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindowInner* aWindow, GamepadHandle aHandle) { if (!WindowHasSeenGamepad(aWindow, aHandle)) { // This window hasn't seen this gamepad before, so // send a connection event first. SetWindowHasSeenGamepad(aWindow, aHandle); return false; } return true; } bool GamepadManager::WindowHasSeenGamepad(nsGlobalWindowInner* aWindow, GamepadHandle aHandle) const { RefPtr gamepad = aWindow->GetGamepad(aHandle); return gamepad != nullptr; } void GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindowInner* aWindow, GamepadHandle aHandle, bool aHasSeen) { MOZ_ASSERT(aWindow); if (mListeners.IndexOf(aWindow) == NoIndex) { // This window isn't even listening for gamepad events. return; } if (aHasSeen) { aWindow->SetHasSeenGamepadInput(true); nsCOMPtr window = ToSupports(aWindow); RefPtr gamepad = GetGamepad(aHandle); if (!gamepad) { return; } RefPtr clonedGamepad = gamepad->Clone(window); aWindow->AddGamepad(aHandle, clonedGamepad); } else { aWindow->RemoveGamepad(aHandle); } } void GamepadManager::Update(const GamepadChangeEvent& aEvent) { if (!mEnabled || mShuttingDown || nsContentUtils::ShouldResistFingerprinting()) { return; } const GamepadHandle handle = aEvent.handle(); GamepadChangeEventBody body = aEvent.body(); if (body.type() == GamepadChangeEventBody::TGamepadAdded) { const GamepadAdded& a = body.get_GamepadAdded(); AddGamepad(handle, a.id(), static_cast(a.mapping()), static_cast(a.hand()), a.display_id(), a.num_buttons(), a.num_axes(), a.num_haptics(), a.num_lights(), a.num_touches()); return; } if (body.type() == GamepadChangeEventBody::TGamepadRemoved) { RemoveGamepad(handle); return; } if (!SetGamepadByEvent(aEvent)) { return; } // Hold on to listeners in a separate array because firing events // can mutate the mListeners array. nsTArray> listeners(mListeners.Clone()); for (uint32_t i = 0; i < listeners.Length(); i++) { // Only send events to non-background windows if (!listeners[i]->IsCurrentInnerWindow() || listeners[i]->GetOuterWindow()->IsBackground()) { continue; } SetGamepadByEvent(aEvent, listeners[i]); MaybeConvertToNonstandardGamepadEvent(aEvent, listeners[i]); } } void GamepadManager::MaybeConvertToNonstandardGamepadEvent( const GamepadChangeEvent& aEvent, nsGlobalWindowInner* aWindow) { MOZ_ASSERT(aWindow); if (!mNonstandardEventsEnabled) { return; } GamepadHandle handle = aEvent.handle(); RefPtr gamepad = aWindow->GetGamepad(handle); const GamepadChangeEventBody& body = aEvent.body(); if (gamepad) { switch (body.type()) { case GamepadChangeEventBody::TGamepadButtonInformation: { const GamepadButtonInformation& a = body.get_GamepadButtonInformation(); FireButtonEvent(aWindow, gamepad, a.button(), a.value()); break; } case GamepadChangeEventBody::TGamepadAxisInformation: { const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); FireAxisMoveEvent(aWindow, gamepad, a.axis(), a.value()); break; } default: break; } } } bool GamepadManager::SetGamepadByEvent(const GamepadChangeEvent& aEvent, nsGlobalWindowInner* aWindow) { bool ret = false; bool firstTime = false; GamepadHandle handle = aEvent.handle(); if (aWindow) { if (!AxisMoveIsFirstIntent(aWindow, handle, aEvent)) { return false; } firstTime = !MaybeWindowHasSeenGamepad(aWindow, handle); } RefPtr gamepad = aWindow ? aWindow->GetGamepad(handle) : GetGamepad(handle); const GamepadChangeEventBody& body = aEvent.body(); if (gamepad) { switch (body.type()) { case GamepadChangeEventBody::TGamepadButtonInformation: { const GamepadButtonInformation& a = body.get_GamepadButtonInformation(); gamepad->SetButton(a.button(), a.pressed(), a.touched(), a.value()); break; } case GamepadChangeEventBody::TGamepadAxisInformation: { const GamepadAxisInformation& a = body.get_GamepadAxisInformation(); gamepad->SetAxis(a.axis(), a.value()); break; } case GamepadChangeEventBody::TGamepadPoseInformation: { const GamepadPoseInformation& a = body.get_GamepadPoseInformation(); gamepad->SetPose(a.pose_state()); break; } case GamepadChangeEventBody::TGamepadLightIndicatorTypeInformation: { const GamepadLightIndicatorTypeInformation& a = body.get_GamepadLightIndicatorTypeInformation(); gamepad->SetLightIndicatorType(a.light(), a.type()); break; } case GamepadChangeEventBody::TGamepadTouchInformation: { // Avoid GamepadTouch's touchId be accessed in cross-origin tracking. for (uint32_t i = 0; i < mListeners.Length(); i++) { RefPtr listenerGamepad = mListeners[i]->GetGamepad(handle); if (listenerGamepad && mListeners[i]->IsCurrentInnerWindow() && !mListeners[i]->GetOuterWindow()->IsBackground()) { const GamepadTouchInformation& a = body.get_GamepadTouchInformation(); listenerGamepad->SetTouchEvent(a.index(), a.touch_state()); } } break; } case GamepadChangeEventBody::TGamepadHandInformation: { const GamepadHandInformation& a = body.get_GamepadHandInformation(); gamepad->SetHand(a.hand()); break; } default: MOZ_ASSERT(false); break; } ret = true; } if (aWindow && firstTime) { FireConnectionEvent(aWindow, gamepad, true); } return ret; } already_AddRefed GamepadManager::VibrateHaptic( GamepadHandle aHandle, uint32_t aHapticIndex, double aIntensity, double aDuration, nsIGlobalObject* aGlobal, ErrorResult& aRv) { RefPtr promise = Promise::Create(aGlobal, aRv); if (NS_WARN_IF(aRv.Failed())) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } if (StaticPrefs::dom_gamepad_haptic_feedback_enabled()) { if (aHandle.GetKind() == GamepadHandleKind::VR) { if (gfx::VRManagerChild::IsCreated()) { gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); vm->AddPromise(mPromiseID, promise); vm->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity, aDuration, mPromiseID); } } else { if (mChannelChild) { mChannelChild->AddPromise(mPromiseID, promise); mChannelChild->SendVibrateHaptic(aHandle, aHapticIndex, aIntensity, aDuration, mPromiseID); } } } ++mPromiseID; return promise.forget(); } void GamepadManager::StopHaptics() { if (!StaticPrefs::dom_gamepad_haptic_feedback_enabled()) { return; } for (const auto& entry : mGamepads) { const GamepadHandle handle = entry.GetWeak()->GetHandle(); if (handle.GetKind() == GamepadHandleKind::VR) { if (gfx::VRManagerChild::IsCreated()) { gfx::VRManagerChild* vm = gfx::VRManagerChild::Get(); vm->SendStopVibrateHaptic(handle); } } else { if (mChannelChild) { mChannelChild->SendStopVibrateHaptic(handle); } } } } already_AddRefed GamepadManager::SetLightIndicatorColor( GamepadHandle aHandle, uint32_t aLightColorIndex, uint8_t aRed, uint8_t aGreen, uint8_t aBlue, nsIGlobalObject* aGlobal, ErrorResult& aRv) { RefPtr promise = Promise::Create(aGlobal, aRv); if (NS_WARN_IF(aRv.Failed())) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } if (StaticPrefs::dom_gamepad_extensions_lightindicator()) { MOZ_RELEASE_ASSERT(aHandle.GetKind() != GamepadHandleKind::VR, "We don't support light indicator in VR."); if (mChannelChild) { mChannelChild->AddPromise(mPromiseID, promise); mChannelChild->SendLightIndicatorColor(aHandle, aLightColorIndex, aRed, aGreen, aBlue, mPromiseID); } } ++mPromiseID; return promise.forget(); } } // namespace mozilla::dom