Mypal/dom/gamepad/GamepadManager.cpp

680 lines
18 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"
#include "nsAutoPtr.h"
#include "nsIDOMEvent.h"
#include "nsIDOMDocument.h"
#include "nsIDOMWindow.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIServiceManager.h"
#include "nsThreadUtils.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"
#include <cstddef>
using namespace mozilla::ipc;
namespace mozilla {
namespace dom {
namespace {
const char* kGamepadEnabledPref = "dom.gamepad.enabled";
const char* kGamepadEventsEnabledPref =
"dom.gamepad.non_standard_events.enabled";
const nsTArray<RefPtr<nsGlobalWindow>>::index_type NoIndex =
nsTArray<RefPtr<nsGlobalWindow>>::NoIndex;
bool sShutdown = false;
StaticRefPtr<GamepadManager> gGamepadManagerSingleton;
const uint32_t VR_GAMEPAD_IDX_OFFSET = 0x01 << 16;
} // namespace
NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver)
GamepadManager::GamepadManager()
: mEnabled(false),
mNonstandardEventsEnabled(false),
mShuttingDown(false)
{}
nsresult
GamepadManager::Init()
{
mEnabled = IsAPIEnabled();
mNonstandardEventsEnabled =
Preferences::GetBool(kGamepadEventsEnabledPref, false);
nsCOMPtr<nsIObserverService> 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<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService) {
observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
}
BeginShutdown();
return NS_OK;
}
void
GamepadManager::StopMonitoring()
{
for (uint32_t i = 0; i < mChannelChildren.Length(); ++i) {
mChannelChildren[i]->SendGamepadListenerRemoved();
}
mChannelChildren.Clear();
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(nsGlobalWindow* aWindow)
{
MOZ_ASSERT(aWindow);
MOZ_ASSERT(aWindow->IsInnerWindow());
MOZ_ASSERT(NS_IsMainThread());
if (!mEnabled || mShuttingDown) {
return;
}
if (mListeners.IndexOf(aWindow) != NoIndex) {
return; // already exists
}
mListeners.AppendElement(aWindow);
// IPDL child has been created
if (!mChannelChildren.IsEmpty()) {
return;
}
PBackgroundChild *actor = BackgroundChild::GetForCurrentThread();
//Try to get the PBackground Child actor
if (actor) {
ActorCreated(actor);
} else {
Unused << BackgroundChild::GetOrCreateForCurrentThread(this);
}
}
void
GamepadManager::RemoveListener(nsGlobalWindow* aWindow)
{
MOZ_ASSERT(aWindow);
MOZ_ASSERT(aWindow->IsInnerWindow());
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 (auto iter = mGamepads.Iter(); !iter.Done(); iter.Next()) {
aWindow->RemoveGamepad(iter.Key());
}
mListeners.RemoveElement(aWindow);
if (mListeners.IsEmpty()) {
StopMonitoring();
}
}
already_AddRefed<Gamepad>
GamepadManager::GetGamepad(uint32_t aIndex) const
{
RefPtr<Gamepad> gamepad;
if (mGamepads.Get(aIndex, getter_AddRefs(gamepad))) {
return gamepad.forget();
}
return nullptr;
}
uint32_t GamepadManager::GetGamepadIndexWithServiceType(uint32_t aIndex,
GamepadServiceType aServiceType)
{
uint32_t newIndex = 0;
switch (aServiceType) {
case GamepadServiceType::Standard:
{
MOZ_ASSERT(aIndex <= VR_GAMEPAD_IDX_OFFSET);
newIndex = aIndex;
break;
}
default:
MOZ_ASSERT(false);
break;
}
return newIndex;
}
void
GamepadManager::AddGamepad(uint32_t aIndex,
const nsAString& aId,
GamepadMappingType aMapping,
GamepadServiceType aServiceType,
uint32_t aNumButtons,
uint32_t aNumAxes)
{
//TODO: bug 852258: get initial button/axis state
RefPtr<Gamepad> gamepad =
new Gamepad(nullptr,
aId,
0, // index is set by global window
aMapping,
aNumButtons,
aNumAxes);
uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
// We store the gamepad related to its index given by the parent process,
// and no duplicate index is allowed.
MOZ_ASSERT(!mGamepads.Get(newIndex, nullptr));
mGamepads.Put(newIndex, gamepad);
NewConnectionEvent(newIndex, true);
}
void
GamepadManager::RemoveGamepad(uint32_t aIndex, GamepadServiceType aServiceType)
{
uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
if (!gamepad) {
NS_WARNING("Trying to delete gamepad with invalid index");
return;
}
gamepad->SetConnected(false);
NewConnectionEvent(newIndex, false);
mGamepads.Remove(newIndex);
}
void
GamepadManager::NewButtonEvent(uint32_t aIndex, GamepadServiceType aServiceType,
uint32_t aButton, bool aPressed, double aValue)
{
if (mShuttingDown) {
return;
}
uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
if (!gamepad) {
return;
}
gamepad->SetButton(aButton, aPressed, aValue);
// Hold on to listeners in a separate array because firing events
// can mutate the mListeners array.
nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
MOZ_ASSERT(!listeners.IsEmpty());
for (uint32_t i = 0; i < listeners.Length(); i++) {
MOZ_ASSERT(listeners[i]->IsInnerWindow());
// Only send events to non-background windows
if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
listeners[i]->GetOuterWindow()->IsBackground()) {
continue;
}
bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
if (listenerGamepad) {
listenerGamepad->SetButton(aButton, aPressed, aValue);
if (firstTime) {
FireConnectionEvent(listeners[i], listenerGamepad, true);
}
if (mNonstandardEventsEnabled) {
// Fire event
FireButtonEvent(listeners[i], listenerGamepad, aButton, aValue);
}
}
}
}
void
GamepadManager::FireButtonEvent(EventTarget* aTarget,
Gamepad* aGamepad,
uint32_t aButton,
double aValue)
{
nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") :
NS_LITERAL_STRING("gamepadbuttonup");
GamepadButtonEventInit init;
init.mBubbles = false;
init.mCancelable = false;
init.mGamepad = aGamepad;
init.mButton = aButton;
RefPtr<GamepadButtonEvent> event =
GamepadButtonEvent::Constructor(aTarget, name, init);
event->SetTrusted(true);
bool defaultActionEnabled = true;
aTarget->DispatchEvent(event, &defaultActionEnabled);
}
void
GamepadManager::NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType,
uint32_t aAxis, double aValue)
{
if (mShuttingDown) {
return;
}
uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
if (!gamepad) {
return;
}
gamepad->SetAxis(aAxis, aValue);
// Hold on to listeners in a separate array because firing events
// can mutate the mListeners array.
nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
MOZ_ASSERT(!listeners.IsEmpty());
for (uint32_t i = 0; i < listeners.Length(); i++) {
MOZ_ASSERT(listeners[i]->IsInnerWindow());
// Only send events to non-background windows
if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
listeners[i]->GetOuterWindow()->IsBackground()) {
continue;
}
bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
if (listenerGamepad) {
listenerGamepad->SetAxis(aAxis, aValue);
if (firstTime) {
FireConnectionEvent(listeners[i], listenerGamepad, true);
}
if (mNonstandardEventsEnabled) {
// Fire event
FireAxisMoveEvent(listeners[i], listenerGamepad, aAxis, aValue);
}
}
}
}
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<GamepadAxisMoveEvent> event =
GamepadAxisMoveEvent::Constructor(aTarget,
NS_LITERAL_STRING("gamepadaxismove"),
init);
event->SetTrusted(true);
bool defaultActionEnabled = true;
aTarget->DispatchEvent(event, &defaultActionEnabled);
}
void
GamepadManager::NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
const GamepadPoseState& aPose)
{
if (mShuttingDown) {
return;
}
uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
if (!gamepad) {
return;
}
gamepad->SetPose(aPose);
// Hold on to listeners in a separate array because firing events
// can mutate the mListeners array.
nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
MOZ_ASSERT(!listeners.IsEmpty());
for (uint32_t i = 0; i < listeners.Length(); i++) {
MOZ_ASSERT(listeners[i]->IsInnerWindow());
// Only send events to non-background windows
if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
listeners[i]->GetOuterWindow()->IsBackground()) {
continue;
}
bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
if (listenerGamepad) {
listenerGamepad->SetPose(aPose);
if (firstTime) {
FireConnectionEvent(listeners[i], listenerGamepad, true);
}
}
}
}
void
GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected)
{
if (mShuttingDown) {
return;
}
RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
if (!gamepad) {
return;
}
// Hold on to listeners in a separate array because firing events
// can mutate the mListeners array.
nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
MOZ_ASSERT(!listeners.IsEmpty());
if (aConnected) {
for (uint32_t i = 0; i < listeners.Length(); i++) {
MOZ_ASSERT(listeners[i]->IsInnerWindow());
// Only send events to non-background windows
if (!listeners[i]->AsInner()->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], aIndex);
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
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.
if (WindowHasSeenGamepad(listeners[i], aIndex)) {
RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
if (listenerGamepad) {
listenerGamepad->SetConnected(false);
// Fire event
FireConnectionEvent(listeners[i], listenerGamepad, false);
listeners[i]->RemoveGamepad(aIndex);
}
}
}
}
}
void
GamepadManager::FireConnectionEvent(EventTarget* aTarget,
Gamepad* aGamepad,
bool aConnected)
{
nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") :
NS_LITERAL_STRING("gamepaddisconnected");
GamepadEventInit init;
init.mBubbles = false;
init.mCancelable = false;
init.mGamepad = aGamepad;
RefPtr<GamepadEvent> event =
GamepadEvent::Constructor(aTarget, name, init);
event->SetTrusted(true);
bool defaultActionEnabled = true;
aTarget->DispatchEvent(event, &defaultActionEnabled);
}
void
GamepadManager::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad)
{
if (mShuttingDown || !mEnabled) {
return;
}
RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
if (!gamepad) {
return;
}
aGamepad->SyncState(gamepad);
}
// static
bool
GamepadManager::IsServiceRunning()
{
return !!gGamepadManagerSingleton;
}
// static
already_AddRefed<GamepadManager>
GamepadManager::GetService()
{
if (sShutdown) {
return nullptr;
}
if (!gGamepadManagerSingleton) {
RefPtr<GamepadManager> manager = new GamepadManager();
nsresult rv = manager->Init();
if(NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
gGamepadManagerSingleton = manager;
ClearOnShutdown(&gGamepadManagerSingleton);
}
RefPtr<GamepadManager> service(gGamepadManagerSingleton);
return service.forget();
}
// static
bool
GamepadManager::IsAPIEnabled() {
return Preferences::GetBool(kGamepadEnabledPref, false);
}
bool
GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex)
{
if (!WindowHasSeenGamepad(aWindow, aIndex)) {
// This window hasn't seen this gamepad before, so
// send a connection event first.
SetWindowHasSeenGamepad(aWindow, aIndex);
return true;
}
return false;
}
bool
GamepadManager::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) const
{
RefPtr<Gamepad> gamepad = aWindow->GetGamepad(aIndex);
return gamepad != nullptr;
}
void
GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow,
uint32_t aIndex,
bool aHasSeen)
{
MOZ_ASSERT(aWindow);
MOZ_ASSERT(aWindow->IsInnerWindow());
if (mListeners.IndexOf(aWindow) == NoIndex) {
// This window isn't even listening for gamepad events.
return;
}
if (aHasSeen) {
aWindow->SetHasSeenGamepadInput(true);
nsCOMPtr<nsISupports> window = ToSupports(aWindow);
RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
if (!gamepad) {
return;
}
RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window);
aWindow->AddGamepad(aIndex, clonedGamepad);
} else {
aWindow->RemoveGamepad(aIndex);
}
}
void
GamepadManager::Update(const GamepadChangeEvent& aEvent)
{
if (aEvent.type() == GamepadChangeEvent::TGamepadAdded) {
const GamepadAdded& a = aEvent.get_GamepadAdded();
AddGamepad(a.index(), a.id(),
static_cast<GamepadMappingType>(a.mapping()),
a.service_type(),
a.num_buttons(), a.num_axes());
return;
}
if (aEvent.type() == GamepadChangeEvent::TGamepadRemoved) {
const GamepadRemoved& a = aEvent.get_GamepadRemoved();
RemoveGamepad(a.index(), a.service_type());
return;
}
if (aEvent.type() == GamepadChangeEvent::TGamepadButtonInformation) {
const GamepadButtonInformation& a = aEvent.get_GamepadButtonInformation();
NewButtonEvent(a.index(), a.service_type(), a.button(),
a.pressed(), a.value());
return;
}
if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) {
const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation();
NewAxisMoveEvent(a.index(), a.service_type(), a.axis(), a.value());
return;
}
if (aEvent.type() == GamepadChangeEvent::TGamepadPoseInformation) {
const GamepadPoseInformation& a = aEvent.get_GamepadPoseInformation();
NewPoseEvent(a.index(), a.service_type(), a.pose_state());
return;
}
MOZ_CRASH("We shouldn't be here!");
}
//Override nsIIPCBackgroundChildCreateCallback
void
GamepadManager::ActorCreated(PBackgroundChild *aActor)
{
MOZ_ASSERT(aActor);
GamepadEventChannelChild *child = new GamepadEventChannelChild();
PGamepadEventChannelChild *initedChild =
aActor->SendPGamepadEventChannelConstructor(child);
if (NS_WARN_IF(!initedChild)) {
ActorFailed();
return;
}
MOZ_ASSERT(initedChild == child);
child->SendGamepadListenerAdded();
mChannelChildren.AppendElement(child);
}
//Override nsIIPCBackgroundChildCreateCallback
void
GamepadManager::ActorFailed()
{
MOZ_CRASH("Gamepad IPC actor create failed!");
}
} // namespace dom
} // namespace mozilla