blob: f60a23a7b8350aad8de831e042d77951d21ccb04 [file] [log] [blame]
/*
* Copyright (C) 2011, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
#include "third_party/blink/renderer/modules/gamepad/navigator_gamepad.h"
#include "device/gamepad/public/cpp/gamepad.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/navigator.h"
#include "third_party/blink/renderer/core/origin_trials/origin_trials.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/modules/gamepad/gamepad_dispatcher.h"
#include "third_party/blink/renderer/modules/gamepad/gamepad_event.h"
#include "third_party/blink/renderer/modules/gamepad/gamepad_list.h"
#include "third_party/blink/renderer/modules/vr/navigator_vr.h"
namespace {
void HasGamepadConnectionChanged(const String& old_id,
const String& new_id,
bool old_connected,
bool new_connected,
bool* gamepad_found,
bool* gamepad_lost) {
// If the gamepad ID changes, treat it as a disconnection and connection.
bool id_changed = old_connected && new_connected && old_id != new_id;
if (gamepad_found)
*gamepad_found = id_changed || (!old_connected && new_connected);
if (gamepad_lost)
*gamepad_lost = id_changed || (old_connected && !new_connected);
}
} // namespace
namespace blink {
template <typename T>
static void SampleGamepad(unsigned index,
T& gamepad,
const device::Gamepad& device_gamepad) {
String old_id = gamepad.id();
bool old_was_connected = gamepad.connected();
gamepad.SetId(device_gamepad.id);
gamepad.SetConnected(device_gamepad.connected);
gamepad.SetTimestamp(device_gamepad.timestamp);
gamepad.SetAxes(device_gamepad.axes_length, device_gamepad.axes);
gamepad.SetButtons(device_gamepad.buttons_length, device_gamepad.buttons);
gamepad.SetPose(device_gamepad.pose);
gamepad.SetHand(device_gamepad.hand);
bool newly_connected;
HasGamepadConnectionChanged(old_id, gamepad.id(), old_was_connected,
gamepad.connected(), &newly_connected, nullptr);
// These fields are not expected to change and will only be written when the
// gamepad is newly connected.
if (newly_connected) {
gamepad.SetIndex(index);
gamepad.SetMapping(device_gamepad.mapping);
gamepad.SetVibrationActuator(device_gamepad.vibration_actuator);
gamepad.SetDisplayId(device_gamepad.display_id);
} else if (!gamepad.vibrationActuator() &&
device_gamepad.vibration_actuator.not_null) {
// Some gamepads require additional steps to determine haptics capability.
// These gamepads may initially set |vibration_actuator| to null and then
// update it some time later. Make sure such devices can correctly propagate
// the changed capabilities.
gamepad.SetVibrationActuator(device_gamepad.vibration_actuator);
}
}
template <typename GamepadType, typename ListType>
static void SampleGamepads(ListType* into, const ExecutionContext* context) {
device::Gamepads gamepads;
GamepadDispatcher::Instance().SampleGamepads(gamepads);
for (unsigned i = 0; i < device::Gamepads::kItemsLengthCap; ++i) {
device::Gamepad& web_gamepad = gamepads.items[i];
bool hide_xr_gamepad = false;
if (web_gamepad.is_xr) {
bool webxr_enabled =
(context && OriginTrials::webXRGamepadSupportEnabled(context) &&
OriginTrials::webXREnabled(context));
bool webvr_enabled = (context && OriginTrials::webVREnabled(context));
if (!webxr_enabled && !webvr_enabled) {
// If neither WebXR nor WebVR are enabled, we should not expose XR-
// backed gamepads.
hide_xr_gamepad = true;
}
}
if (hide_xr_gamepad) {
into->Set(i, nullptr);
} else if (web_gamepad.connected) {
GamepadType* gamepad = into->item(i);
if (!gamepad)
gamepad = GamepadType::Create();
SampleGamepad(i, *gamepad, web_gamepad);
into->Set(i, gamepad);
} else {
into->Set(i, nullptr);
}
}
}
// static
const char NavigatorGamepad::kSupplementName[] = "NavigatorGamepad";
NavigatorGamepad* NavigatorGamepad::From(Document& document) {
if (!document.GetFrame() || !document.GetFrame()->DomWindow())
return nullptr;
Navigator& navigator = *document.GetFrame()->DomWindow()->navigator();
return &From(navigator);
}
NavigatorGamepad& NavigatorGamepad::From(Navigator& navigator) {
NavigatorGamepad* supplement =
Supplement<Navigator>::From<NavigatorGamepad>(navigator);
if (!supplement) {
supplement = new NavigatorGamepad(navigator);
ProvideTo(navigator, supplement);
}
return *supplement;
}
GamepadList* NavigatorGamepad::getGamepads(Navigator& navigator) {
return NavigatorGamepad::From(navigator).Gamepads();
}
GamepadList* NavigatorGamepad::Gamepads() {
// Tell VR that gamepad is in use.
Document* document = GetFrame() ? GetFrame()->GetDocument() : nullptr;
if (document) {
NavigatorVR* navigator_vr = NavigatorVR::From(*document);
if (navigator_vr) {
navigator_vr->SetDidUseGamepad();
}
}
SampleAndCheckConnectedGamepads();
return gamepads_.Get();
}
void NavigatorGamepad::Trace(blink::Visitor* visitor) {
visitor->Trace(gamepads_);
visitor->Trace(gamepads_back_);
visitor->Trace(pending_events_);
visitor->Trace(dispatch_one_event_runner_);
Supplement<Navigator>::Trace(visitor);
DOMWindowClient::Trace(visitor);
PlatformEventController::Trace(visitor);
}
bool NavigatorGamepad::StartUpdatingIfAttached() {
// The frame must be attached to start updating.
if (GetFrame()) {
StartUpdating();
return true;
}
return false;
}
void NavigatorGamepad::DidUpdateData() {
// We should stop listening once we detached.
DCHECK(GetFrame());
DCHECK(DomWindow());
// We register to the dispatcher before sampling gamepads so we need to check
// if we actually have an event listener.
if (!has_event_listener_)
return;
SampleAndCheckConnectedGamepads();
}
void NavigatorGamepad::DispatchOneEvent() {
DCHECK(DomWindow());
DCHECK(!pending_events_.IsEmpty());
Gamepad* gamepad = pending_events_.TakeFirst();
const AtomicString& event_name = gamepad->connected()
? EventTypeNames::gamepadconnected
: EventTypeNames::gamepaddisconnected;
DomWindow()->DispatchEvent(GamepadEvent::Create(
event_name, Event::Bubbles::kNo, Event::Cancelable::kYes, gamepad));
if (!pending_events_.IsEmpty()) {
DCHECK(dispatch_one_event_runner_);
dispatch_one_event_runner_->RunAsync();
}
}
NavigatorGamepad::NavigatorGamepad(Navigator& navigator)
: Supplement<Navigator>(navigator),
DOMWindowClient(navigator.DomWindow()),
PlatformEventController(
navigator.GetFrame() ? navigator.GetFrame()->GetDocument() : nullptr),
dispatch_one_event_runner_(
navigator.GetFrame() ? AsyncMethodRunner<NavigatorGamepad>::Create(
this,
&NavigatorGamepad::DispatchOneEvent,
navigator.GetFrame()->GetTaskRunner(
TaskType::kMiscPlatformAPI))
: nullptr) {
if (navigator.DomWindow())
navigator.DomWindow()->RegisterEventListenerObserver(this);
}
NavigatorGamepad::~NavigatorGamepad() = default;
void NavigatorGamepad::RegisterWithDispatcher() {
GamepadDispatcher::Instance().AddController(this);
if (dispatch_one_event_runner_)
dispatch_one_event_runner_->Unpause();
}
void NavigatorGamepad::UnregisterWithDispatcher() {
if (dispatch_one_event_runner_)
dispatch_one_event_runner_->Pause();
GamepadDispatcher::Instance().RemoveController(this);
}
bool NavigatorGamepad::HasLastData() {
// Gamepad data is polled instead of pushed.
return false;
}
static bool IsGamepadEvent(const AtomicString& event_type) {
return event_type == EventTypeNames::gamepadconnected ||
event_type == EventTypeNames::gamepaddisconnected;
}
void NavigatorGamepad::DidAddEventListener(LocalDOMWindow*,
const AtomicString& event_type) {
if (!IsGamepadEvent(event_type))
return;
bool first_event_listener = !has_event_listener_;
has_event_listener_ = true;
if (GetPage() && GetPage()->IsPageVisible()) {
StartUpdatingIfAttached();
if (first_event_listener)
SampleAndCheckConnectedGamepads();
}
}
void NavigatorGamepad::DidRemoveEventListener(LocalDOMWindow* window,
const AtomicString& event_type) {
if (IsGamepadEvent(event_type) &&
!window->HasEventListeners(EventTypeNames::gamepadconnected) &&
!window->HasEventListeners(EventTypeNames::gamepaddisconnected)) {
DidRemoveGamepadEventListeners();
}
}
void NavigatorGamepad::DidRemoveAllEventListeners(LocalDOMWindow*) {
DidRemoveGamepadEventListeners();
}
void NavigatorGamepad::DidRemoveGamepadEventListeners() {
has_event_listener_ = false;
if (dispatch_one_event_runner_)
dispatch_one_event_runner_->Stop();
pending_events_.clear();
StopUpdating();
}
void NavigatorGamepad::SampleAndCheckConnectedGamepads() {
ExecutionContext* execution_context =
DomWindow() ? DomWindow()->GetExecutionContext() : nullptr;
if (StartUpdatingIfAttached()) {
if (!gamepads_)
gamepads_ = GamepadList::Create();
if (GetPage()->IsPageVisible() && has_event_listener_) {
if (!gamepads_back_)
gamepads_back_ = GamepadList::Create();
// Compare the current sample with the old data and enqueue connection
// events for any differences.
SampleGamepads<Gamepad>(gamepads_back_.Get(), execution_context);
if (CheckConnectedGamepads(gamepads_.Get(), gamepads_back_.Get())) {
// If we had any disconnected gamepads, we can't overwrite gamepads_
// because the Gamepad object from the old buffer is reused as the
// disconnection event and will be overwritten with new data. Instead,
// recreate the buffer.
gamepads_ = GamepadList::Create();
}
if (!pending_events_.IsEmpty()) {
DCHECK(dispatch_one_event_runner_);
dispatch_one_event_runner_->RunAsync();
}
}
SampleGamepads<Gamepad>(gamepads_.Get(), execution_context);
}
}
bool NavigatorGamepad::CheckConnectedGamepads(GamepadList* old_gamepads,
GamepadList* new_gamepads) {
int disconnection_count = 0;
for (unsigned i = 0; i < device::Gamepads::kItemsLengthCap; ++i) {
Gamepad* old_gamepad = old_gamepads ? old_gamepads->item(i) : nullptr;
Gamepad* new_gamepad = new_gamepads->item(i);
bool connected, disconnected;
CheckConnectedGamepad(old_gamepad, new_gamepad, &connected, &disconnected);
if (disconnected) {
old_gamepad->SetConnected(false);
pending_events_.push_back(old_gamepad);
disconnection_count++;
}
if (connected) {
pending_events_.push_back(new_gamepad);
}
}
return disconnection_count > 0;
}
void NavigatorGamepad::CheckConnectedGamepad(Gamepad* old_gamepad,
Gamepad* new_gamepad,
bool* gamepad_found,
bool* gamepad_lost) {
bool old_connected = old_gamepad && old_gamepad->connected();
bool new_connected = new_gamepad && new_gamepad->connected();
if (old_gamepad && new_gamepad) {
HasGamepadConnectionChanged(old_gamepad->id(), new_gamepad->id(),
old_connected, new_connected, gamepad_found,
gamepad_lost);
return;
}
if (gamepad_found)
*gamepad_found = new_connected;
if (gamepad_lost)
*gamepad_lost = old_connected;
}
void NavigatorGamepad::PageVisibilityChanged() {
// Inform the embedder whether it needs to provide gamepad data for us.
bool visible = GetPage()->IsPageVisible();
if (visible && (has_event_listener_ || gamepads_)) {
StartUpdatingIfAttached();
} else {
StopUpdating();
}
if (visible && has_event_listener_)
SampleAndCheckConnectedGamepads();
}
} // namespace blink