| /* |
| * 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/gamepad_features.h" |
| #include "device/gamepad/public/cpp/gamepads.h" |
| #include "third_party/blink/public/common/privacy_budget/identifiability_metric_builder.h" |
| #include "third_party/blink/public/common/privacy_budget/identifiability_study_settings.h" |
| #include "third_party/blink/public/platform/task_type.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/inspector/console_message.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/privacy_budget/identifiability_digest_helpers.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"; |
| const char kSecureContextBlocked[] = |
| "Access to the feature \"gamepad\" requires a secure context"; |
| const char kFeaturePolicyBlocked[] = |
| "Access to the feature \"gamepad\" is disallowed by permissions policy."; |
| |
| NavigatorGamepad& NavigatorGamepad::From(Navigator& navigator) { |
| NavigatorGamepad* supplement = |
| Supplement<Navigator>::From<NavigatorGamepad>(navigator); |
| if (!supplement) { |
| supplement = MakeGarbageCollected<NavigatorGamepad>(navigator); |
| ProvideTo(navigator, supplement); |
| } |
| return *supplement; |
| } |
| |
| namespace { |
| |
| void RecordGamepadsForIdentifiabilityStudy(ExecutionContext* context, |
| GamepadList* gamepads) { |
| if (!context || !IdentifiabilityStudySettings::Get()->ShouldSample( |
| IdentifiableSurface::FromTypeAndToken( |
| IdentifiableSurface::Type::kWebFeature, |
| WebFeature::kGetGamepads))) |
| return; |
| IdentifiableTokenBuilder builder; |
| if (gamepads) { |
| for (unsigned i = 0; i < gamepads->length(); i++) { |
| if (auto* gp = gamepads->item(i)) { |
| builder.AddValue(gp->axes().size()) |
| .AddValue(gp->buttons().size()) |
| .AddValue(gp->connected()) |
| .AddToken(IdentifiabilityBenignStringToken(gp->id())) |
| .AddToken(IdentifiabilityBenignStringToken(gp->mapping())) |
| .AddValue(gp->timestamp()); |
| if (auto* vb = gp->vibrationActuator()) { |
| builder.AddToken(IdentifiabilityBenignStringToken(vb->type())); |
| } |
| } |
| } |
| } |
| IdentifiabilityMetricBuilder(context->UkmSourceID()) |
| .SetWebfeature(WebFeature::kGetGamepads, builder.GetToken()) |
| .Record(context->UkmRecorder()); |
| } |
| |
| } // namespace |
| |
| // static |
| GamepadList* NavigatorGamepad::getGamepads(Navigator& navigator, |
| ExceptionState& exception_state) { |
| if (!navigator.DomWindow()) { |
| // Using an existing NavigatorGamepad if one exists, but don't create one |
| // for a detached window, as its subclasses depend on a non-null window. |
| auto* gamepad = Supplement<Navigator>::From<NavigatorGamepad>(navigator); |
| if (gamepad) { |
| auto* result = gamepad->Gamepads(); |
| RecordGamepadsForIdentifiabilityStudy(gamepad->GetExecutionContext(), |
| result); |
| return result; |
| } |
| return nullptr; |
| } |
| |
| auto* navigator_gamepad = &NavigatorGamepad::From(navigator); |
| |
| ExecutionContext* context = navigator_gamepad->GetExecutionContext(); |
| if (!context || !context->IsSecureContext()) { |
| if (base::FeatureList::IsEnabled(features::kRestrictGamepadAccess)) { |
| exception_state.ThrowSecurityError(kSecureContextBlocked); |
| return nullptr; |
| } else { |
| context->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, |
| mojom::blink::ConsoleMessageLevel::kWarning, |
| "getGamepad will now require Secure Context. " |
| "Please update your application accordingly. " |
| "For more information see " |
| "https://github.com/w3c/gamepad/pull/120"), |
| /*discard_duplicates=*/true); |
| } |
| } |
| |
| if (!context->IsFeatureEnabled( |
| mojom::blink::FeaturePolicyFeature::kGamepad)) { |
| if (base::FeatureList::IsEnabled(features::kRestrictGamepadAccess)) { |
| exception_state.ThrowSecurityError(kFeaturePolicyBlocked); |
| return nullptr; |
| } else { |
| context->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, |
| mojom::blink::ConsoleMessageLevel::kWarning, |
| "getGamepad will now require a Permission Policy. " |
| "Please update your application accordingly. " |
| "For more information see " |
| "https://github.com/w3c/gamepad/pull/112"), |
| /*discard_duplicates=*/true); |
| } |
| } |
| |
| auto* result = NavigatorGamepad::From(navigator).Gamepads(); |
| RecordGamepadsForIdentifiabilityStudy(context, result); |
| return result; |
| } |
| |
| 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 (DomWindow() && DomWindow()->GetFrame()->GetPage()->IsPageVisible() && |
| GamepadComparisons::HasUserActivation(gamepads_)) { |
| LocalFrame::NotifyUserActivation( |
| DomWindow()->GetFrame(), |
| mojom::blink::UserActivationNotificationType::kInteraction); |
| } |
| is_gamepads_exposed_ = true; |
| |
| ExecutionContext* context = DomWindow(); |
| |
| if (DomWindow() && DomWindow()->GetFrame()->IsCrossOriginToMainFrame()) { |
| 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]) { |
| auto* actuator = MakeGarbageCollected<GamepadHapticActuator>( |
| *DomWindow(), pad_index, gamepad.GetVibrationActuatorType()); |
| vibration_actuators_[pad_index] = actuator; |
| } |
| return vibration_actuators_[pad_index].Get(); |
| } |
| |
| void NavigatorGamepad::Trace(Visitor* visitor) const { |
| visitor->Trace(gamepads_); |
| visitor->Trace(gamepads_back_); |
| visitor->Trace(vibration_actuators_); |
| visitor->Trace(gamepad_dispatcher_); |
| Supplement<Navigator>::Trace(visitor); |
| ExecutionContextClient::Trace(visitor); |
| PlatformEventController::Trace(visitor); |
| Gamepad::Client::Trace(visitor); |
| } |
| |
| bool NavigatorGamepad::StartUpdatingIfAttached() { |
| // The frame must be attached to start updating. |
| if (DomWindow()) { |
| StartUpdating(); |
| return true; |
| } |
| return false; |
| } |
| |
| void NavigatorGamepad::DidUpdateData() { |
| // We should stop listening once we detached. |
| 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), |
| ExecutionContextClient(navigator.DomWindow()), |
| PlatformEventController(*navigator.DomWindow()), |
| gamepad_dispatcher_( |
| MakeGarbageCollected<GamepadDispatcher>(*navigator.DomWindow())) { |
| navigator.DomWindow()->RegisterEventListenerObserver(this); |
| |
| // Fetch |window.performance.timing.navigationStart|. Gamepad timestamps are |
| // reported relative to this value. |
| auto& timing = DomWindow()->document()->Loader()->GetTiming(); |
| navigation_start_ = timing.NavigationStart(); |
| |
| vibration_actuators_.resize(device::Gamepads::kItemsLengthCap); |
| } |
| |
| NavigatorGamepad::~NavigatorGamepad() = default; |
| |
| void NavigatorGamepad::RegisterWithDispatcher() { |
| gamepad_dispatcher_->AddController(this, DomWindow()); |
| } |
| |
| 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 |