blob: 2d58ec5e7cd36fd702b910b394089783a5219ec6 [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 "base/auto_reset.h"
#include "device/gamepad/public/cpp/gamepads.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/loader/document_loader.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.h"
#include "third_party/blink/renderer/modules/gamepad/gamepad_comparisons.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/platform/wtf/text/atomic_string.h"
namespace blink {
namespace {
bool IsGamepadConnectionEvent(const AtomicString& event_type) {
return event_type == event_type_names::kGamepadconnected ||
event_type == event_type_names::kGamepaddisconnected;
}
bool HasConnectionEventListeners(LocalDOMWindow* window) {
return window->HasEventListeners(event_type_names::kGamepadconnected) ||
window->HasEventListeners(event_type_names::kGamepaddisconnected);
}
} // namespace
// 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 = MakeGarbageCollected<NavigatorGamepad>(navigator);
ProvideTo(navigator, supplement);
}
return *supplement;
}
// static
GamepadList* NavigatorGamepad::getGamepads(Navigator& navigator) {
return NavigatorGamepad::From(navigator).Gamepads();
}
GamepadList* NavigatorGamepad::Gamepads() {
SampleAndCompareGamepadState();
// Ensure |gamepads_| is not null.
if (!gamepads_)
gamepads_ = MakeGarbageCollected<GamepadList>();
// Allow gamepad button presses to qualify as user activations if the page is
// visible.
if (RuntimeEnabledFeatures::UserActivationV2Enabled() && GetFrame() &&
GetPage() && GetPage()->IsPageVisible() &&
GamepadComparisons::HasUserActivation(gamepads_)) {
LocalFrame::NotifyUserActivation(GetFrame(), UserGestureToken::kNewGesture);
}
is_gamepads_exposed_ = true;
ExecutionContext* context =
DomWindow() ? DomWindow()->GetExecutionContext() : nullptr;
if (GetFrame() && GetFrame()->IsCrossOriginSubframe()) {
UseCounter::Count(context, WebFeature::kGetGamepadsFromCrossOriginSubframe);
}
if (context && !context->IsSecureContext()) {
UseCounter::Count(context, WebFeature::kGetGamepadsFromInsecureContext);
}
return gamepads_.Get();
}
void NavigatorGamepad::SampleGamepads() {
device::Gamepads gamepads;
gamepad_dispatcher_->SampleGamepads(gamepads);
for (uint32_t i = 0; i < device::Gamepads::kItemsLengthCap; ++i) {
device::Gamepad& device_gamepad = gamepads.items[i];
// All WebXR gamepads should be hidden
if (device_gamepad.is_xr) {
gamepads_back_->Set(i, nullptr);
} else if (device_gamepad.connected) {
Gamepad* gamepad = gamepads_back_->item(i);
if (!gamepad) {
gamepad = MakeGarbageCollected<Gamepad>(this, i, navigation_start_,
gamepads_start_);
}
gamepad->UpdateFromDeviceState(device_gamepad);
gamepads_back_->Set(i, gamepad);
} else {
gamepads_back_->Set(i, nullptr);
}
}
}
GamepadHapticActuator* NavigatorGamepad::GetVibrationActuatorForGamepad(
const Gamepad& gamepad) {
if (!gamepad.connected()) {
return nullptr;
}
if (!gamepad.HasVibrationActuator()) {
return nullptr;
}
int pad_index = gamepad.index();
DCHECK_GE(pad_index, 0);
if (!vibration_actuators_[pad_index]) {
ExecutionContext* context =
DomWindow() ? DomWindow()->GetExecutionContext() : nullptr;
auto* actuator = GamepadHapticActuator::Create(context, pad_index);
actuator->SetType(gamepad.GetVibrationActuatorType());
vibration_actuators_[pad_index] = actuator;
}
return vibration_actuators_[pad_index].Get();
}
void NavigatorGamepad::Trace(blink::Visitor* visitor) {
visitor->Trace(gamepads_);
visitor->Trace(gamepads_back_);
visitor->Trace(vibration_actuators_);
visitor->Trace(gamepad_dispatcher_);
Supplement<Navigator>::Trace(visitor);
DOMWindowClient::Trace(visitor);
PlatformEventController::Trace(visitor);
Gamepad::Client::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());
// Record when gamepad data was first made available to the page.
if (gamepads_start_.is_null())
gamepads_start_ = base::TimeTicks::Now();
// Fetch the new gamepad state and dispatch gamepad events.
if (has_event_listener_)
SampleAndCompareGamepadState();
}
NavigatorGamepad::NavigatorGamepad(Navigator& navigator)
: Supplement<Navigator>(navigator),
DOMWindowClient(navigator.DomWindow()),
PlatformEventController(
navigator.GetFrame() ? navigator.GetFrame()->GetDocument() : nullptr),
// See https://bit.ly/2S0zRAS for task types
gamepad_dispatcher_(MakeGarbageCollected<GamepadDispatcher>(
navigator.GetFrame() ? navigator.GetFrame()->GetTaskRunner(
blink::TaskType::kMiscPlatformAPI)
: nullptr)) {
if (navigator.DomWindow())
navigator.DomWindow()->RegisterEventListenerObserver(this);
// Fetch |window.performance.timing.navigationStart|. Gamepad timestamps are
// reported relative to this value.
if (GetFrame()) {
DocumentLoader* loader = GetFrame()->Loader().GetDocumentLoader();
if (loader)
navigation_start_ = loader->GetTiming().NavigationStart();
}
vibration_actuators_.resize(device::Gamepads::kItemsLengthCap);
}
NavigatorGamepad::~NavigatorGamepad() = default;
void NavigatorGamepad::RegisterWithDispatcher() {
gamepad_dispatcher_->AddController(this, GetFrame());
}
void NavigatorGamepad::UnregisterWithDispatcher() {
gamepad_dispatcher_->RemoveController(this);
}
bool NavigatorGamepad::HasLastData() {
// Gamepad data is polled instead of pushed.
return false;
}
void NavigatorGamepad::DidAddEventListener(LocalDOMWindow*,
const AtomicString& event_type) {
if (IsGamepadConnectionEvent(event_type)) {
has_connection_event_listener_ = true;
bool first_event_listener = !has_event_listener_;
has_event_listener_ = true;
if (GetPage() && GetPage()->IsPageVisible()) {
StartUpdatingIfAttached();
if (first_event_listener)
SampleAndCompareGamepadState();
}
}
}
void NavigatorGamepad::DidRemoveEventListener(LocalDOMWindow* window,
const AtomicString& event_type) {
if (IsGamepadConnectionEvent(event_type)) {
has_connection_event_listener_ = HasConnectionEventListeners(window);
if (!has_connection_event_listener_)
DidRemoveGamepadEventListeners();
}
}
void NavigatorGamepad::DidRemoveAllEventListeners(LocalDOMWindow*) {
DidRemoveGamepadEventListeners();
}
void NavigatorGamepad::DidRemoveGamepadEventListeners() {
has_event_listener_ = false;
StopUpdating();
}
void NavigatorGamepad::SampleAndCompareGamepadState() {
// Avoid re-entry. Do not fetch a new sample until we are finished dispatching
// events from the previous sample.
if (processing_events_)
return;
base::AutoReset<bool> processing_events_reset(&processing_events_, true);
if (StartUpdatingIfAttached()) {
if (GetPage()->IsPageVisible()) {
// Allocate a buffer to hold the new gamepad state, if needed.
if (!gamepads_back_)
gamepads_back_ = MakeGarbageCollected<GamepadList>();
SampleGamepads();
// Compare the new sample with the previous sample and record which
// gamepad events should be dispatched. Swap buffers if the gamepad
// state changed. We must swap buffers before dispatching events to
// ensure |gamepads_| holds the correct data when getGamepads is called
// from inside a gamepad event listener.
auto compare_result = GamepadComparisons::Compare(
gamepads_.Get(), gamepads_back_.Get(), false, false);
if (compare_result.IsDifferent()) {
gamepads_.Swap(gamepads_back_);
bool is_gamepads_back_exposed = is_gamepads_exposed_;
is_gamepads_exposed_ = false;
// Dispatch gamepad events. Dispatching an event calls the event
// listeners synchronously.
//
// Note: In some instances the gamepad connection state may change while
// inside an event listener. This is most common when using test APIs
// that allow the gamepad state to be changed from javascript. The set
// of event listeners may also change if listeners are added or removed
// by another listener.
for (uint32_t i = 0; i < device::Gamepads::kItemsLengthCap; ++i) {
bool is_connected = compare_result.IsGamepadConnected(i);
bool is_disconnected = compare_result.IsGamepadDisconnected(i);
// When a gamepad is disconnected and connected in the same update,
// dispatch the gamepaddisconnected event first.
if (has_connection_event_listener_ && is_disconnected) {
// Reset the vibration state associated with the disconnected
// gamepad to prevent it from being associated with a
// newly-connected gamepad at the same index.
vibration_actuators_[i] = nullptr;
Gamepad* pad = gamepads_back_->item(i);
DCHECK(pad);
pad->SetConnected(false);
is_gamepads_back_exposed = true;
DispatchGamepadEvent(event_type_names::kGamepaddisconnected, pad);
}
if (has_connection_event_listener_ && is_connected) {
Gamepad* pad = gamepads_->item(i);
DCHECK(pad);
is_gamepads_exposed_ = true;
DispatchGamepadEvent(event_type_names::kGamepadconnected, pad);
}
}
// Clear |gamepads_back_| if it was ever exposed to the page so it can
// be garbage collected when no active references remain. If it was
// never exposed, retain the buffer so it can be reused.
if (is_gamepads_back_exposed)
gamepads_back_.Clear();
}
}
}
}
void NavigatorGamepad::DispatchGamepadEvent(const AtomicString& event_name,
Gamepad* gamepad) {
// Ensure that we're blocking re-entrancy.
DCHECK(processing_events_);
DCHECK(has_connection_event_listener_);
DCHECK(gamepad);
DomWindow()->DispatchEvent(*GamepadEvent::Create(
event_name, Event::Bubbles::kNo, Event::Cancelable::kYes, gamepad));
}
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_)
SampleAndCompareGamepadState();
}
} // namespace blink