| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/idle/idle_detector.h" |
| |
| #include <utility> |
| |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h" |
| #include "third_party/blink/public/mojom/idle/idle_manager.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_idle_options.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_screen_idle_state.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_user_idle_state.h" |
| #include "third_party/blink/renderer/core/dom/abort_signal.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/execution_context/security_context.h" |
| #include "third_party/blink/renderer/modules/idle/idle_manager.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/bindings/script_state.h" |
| #include "third_party/blink/renderer/platform/heap/garbage_collected.h" |
| #include "third_party/blink/renderer/platform/heap/persistent.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| using mojom::blink::IdleManagerError; |
| |
| const char kFeaturePolicyBlocked[] = |
| "Access to the feature \"idle-detection\" is disallowed by permissions " |
| "policy."; |
| |
| constexpr base::TimeDelta kMinimumThreshold = base::Seconds(60); |
| constexpr base::TimeDelta kUserInputThreshold = |
| base::Milliseconds(mojom::blink::IdleManager::kUserInputThresholdMs); |
| |
| static_assert( |
| kMinimumThreshold >= kUserInputThreshold, |
| "Browser threshold can't be less than the minimum allowed by the API"); |
| |
| } // namespace |
| |
| class IdleDetector::StartAbortAlgorithm final : public AbortSignal::Algorithm { |
| public: |
| explicit StartAbortAlgorithm(IdleDetector* idle_detector) |
| : idle_detector_(idle_detector) {} |
| ~StartAbortAlgorithm() override = default; |
| |
| void Run() override { idle_detector_->Abort(); } |
| |
| void Trace(Visitor* visitor) const override { |
| visitor->Trace(idle_detector_); |
| Algorithm::Trace(visitor); |
| } |
| |
| private: |
| Member<IdleDetector> idle_detector_; |
| }; |
| |
| IdleDetector* IdleDetector::Create(ScriptState* script_state) { |
| return MakeGarbageCollected<IdleDetector>( |
| ExecutionContext::From(script_state)); |
| } |
| |
| IdleDetector::IdleDetector(ExecutionContext* context) |
| : ActiveScriptWrappable<IdleDetector>({}), |
| ExecutionContextLifecycleObserver(context), |
| task_runner_(context->GetTaskRunner(TaskType::kMiscPlatformAPI)), |
| timer_(task_runner_, this, &IdleDetector::DispatchUserIdleEvent), |
| receiver_(this, context) {} |
| |
| IdleDetector::~IdleDetector() = default; |
| |
| const AtomicString& IdleDetector::InterfaceName() const { |
| return event_target_names::kIdleDetector; |
| } |
| |
| ExecutionContext* IdleDetector::GetExecutionContext() const { |
| return ExecutionContextLifecycleObserver::GetExecutionContext(); |
| } |
| |
| bool IdleDetector::HasPendingActivity() const { |
| // This object should be considered active as long as there are registered |
| // event listeners. |
| return GetExecutionContext() && HasEventListeners(); |
| } |
| |
| std::optional<V8UserIdleState> IdleDetector::userState() const { |
| if (!has_state_) { |
| return std::nullopt; |
| } |
| |
| return user_idle_ ? V8UserIdleState(V8UserIdleState::Enum::kIdle) |
| : V8UserIdleState(V8UserIdleState::Enum::kActive); |
| } |
| |
| std::optional<V8ScreenIdleState> IdleDetector::screenState() const { |
| if (!has_state_) { |
| return std::nullopt; |
| } |
| |
| return screen_locked_ ? V8ScreenIdleState(V8ScreenIdleState::Enum::kLocked) |
| : V8ScreenIdleState(V8ScreenIdleState::Enum::kUnlocked); |
| } |
| |
| // static |
| ScriptPromise<V8PermissionState> IdleDetector::requestPermission( |
| ScriptState* script_state, |
| ExceptionState& exception_state) { |
| if (!script_state->ContextIsValid()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Execution context is detached."); |
| return EmptyPromise(); |
| } |
| |
| auto* context = ExecutionContext::From(script_state); |
| return IdleManager::From(context)->RequestPermission(script_state, |
| exception_state); |
| } |
| |
| ScriptPromise<IDLUndefined> IdleDetector::start( |
| ScriptState* script_state, |
| const IdleOptions* options, |
| ExceptionState& exception_state) { |
| if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Execution context is detached."); |
| return EmptyPromise(); |
| } |
| |
| ExecutionContext* context = ExecutionContext::From(script_state); |
| DCHECK(context->IsContextThread()); |
| |
| if (!context->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kIdleDetection, |
| ReportOptions::kReportOnFailure)) { |
| exception_state.ThrowSecurityError(kFeaturePolicyBlocked); |
| return EmptyPromise(); |
| } |
| |
| if (receiver_.is_bound()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Idle detector is already started."); |
| return EmptyPromise(); |
| } |
| |
| if (options->hasThreshold()) { |
| auto threshold = base::Milliseconds(options->threshold()); |
| if (threshold < kMinimumThreshold) { |
| exception_state.ThrowTypeError("Minimum threshold is 1 minute."); |
| return EmptyPromise(); |
| } |
| threshold_ = threshold; |
| } |
| |
| signal_ = options->getSignalOr(nullptr); |
| if (signal_) { |
| if (signal_->aborted()) { |
| return ScriptPromise<IDLUndefined>::Reject(script_state, |
| signal_->reason(script_state)); |
| } |
| // If there was a previous algorithm, it should have been removed when we |
| // reached the "stopped" state. |
| DCHECK(!abort_handle_); |
| abort_handle_ = |
| signal_->AddAlgorithm(MakeGarbageCollected<StartAbortAlgorithm>(this)); |
| } |
| |
| mojo::PendingRemote<mojom::blink::IdleMonitor> remote; |
| receiver_.Bind(remote.InitWithNewPipeAndPassReceiver(), task_runner_); |
| receiver_.set_disconnect_handler( |
| BindOnce(&IdleDetector::OnMonitorDisconnected, WrapWeakPersistent(this))); |
| |
| resolver_ = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>( |
| script_state, exception_state.GetContext()); |
| auto promise = resolver_->Promise(); |
| IdleManager::From(context)->AddMonitor( |
| std::move(remote), |
| BindOnce(&IdleDetector::OnAddMonitor, WrapWeakPersistent(this), |
| WrapPersistent(resolver_.Get()))); |
| return promise; |
| } |
| |
| void IdleDetector::SetTaskRunnerForTesting( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner, |
| const base::TickClock* tick_clock) { |
| task_runner_ = std::move(task_runner); |
| timer_.SetTaskRunnerForTesting(task_runner_, tick_clock); |
| } |
| |
| void IdleDetector::Abort() { |
| if (resolver_) { |
| ScriptState* script_state = resolver_->GetScriptState(); |
| if (IsInParallelAlgorithmRunnable(resolver_->GetExecutionContext(), |
| script_state)) { |
| ScriptState::Scope script_state_scope(script_state); |
| resolver_->Reject(signal_->reason(script_state)); |
| } |
| } |
| Clear(); |
| } |
| |
| void IdleDetector::OnMonitorDisconnected() { |
| ScriptState* resolver_script_state(nullptr); |
| |
| if (resolver_ && (resolver_script_state = resolver_->GetScriptState()) && |
| IsInParallelAlgorithmRunnable(resolver_->GetExecutionContext(), |
| resolver_script_state)) { |
| ScriptState::Scope script_state_scope(resolver_->GetScriptState()); |
| resolver_->Reject(V8ThrowDOMException::CreateOrDie( |
| resolver_->GetScriptState()->GetIsolate(), |
| DOMExceptionCode::kNotSupportedError, "Idle detection not available.")); |
| } |
| Clear(); |
| } |
| |
| void IdleDetector::OnAddMonitor(ScriptPromiseResolver<IDLUndefined>* resolver, |
| IdleManagerError error, |
| mojom::blink::IdleStatePtr state) { |
| if (resolver_ != resolver) { |
| // Starting the detector was aborted so `resolver_` has already been used |
| // and `receiver_` has already been reset. |
| return; |
| } |
| |
| ScriptState* resolver_script_state = resolver_->GetScriptState(); |
| if (!IsInParallelAlgorithmRunnable(resolver_->GetExecutionContext(), |
| resolver_script_state)) { |
| resolver_ = nullptr; |
| return; |
| } |
| ScriptState::Scope script_state_scope(resolver_script_state); |
| |
| switch (error) { |
| case IdleManagerError::kPermissionDisabled: |
| resolver_->Reject( |
| V8ThrowDOMException::CreateOrDie(resolver_script_state->GetIsolate(), |
| DOMExceptionCode::kNotAllowedError, |
| "Idle detection permission denied")); |
| resolver_ = nullptr; |
| break; |
| case IdleManagerError::kSuccess: |
| DCHECK(state); |
| resolver_->Resolve(); |
| resolver_ = nullptr; |
| |
| // This call may execute script if it dispatches an event. |
| Update(std::move(state), /*is_overridden_by_devtools=*/false); |
| break; |
| } |
| } |
| |
| void IdleDetector::Update(mojom::blink::IdleStatePtr state, |
| bool is_overridden_by_devtools) { |
| DCHECK(receiver_.is_bound()); |
| if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) |
| return; |
| |
| bool fire_event = false; |
| if (!has_state_) { |
| has_state_ = true; |
| fire_event = true; |
| } |
| |
| if (state->screen_locked != screen_locked_) { |
| screen_locked_ = state->screen_locked; |
| fire_event = true; |
| } |
| |
| if (state->idle_time.has_value()) { |
| DCHECK_GE(threshold_, kUserInputThreshold); |
| if (!is_overridden_by_devtools && |
| threshold_ > kUserInputThreshold + *state->idle_time) { |
| base::TimeDelta delay = |
| threshold_ - kUserInputThreshold - *state->idle_time; |
| timer_.StartOneShot(delay, FROM_HERE); |
| |
| // Normally this condition is unsatisfiable because state->idle_time |
| // cannot move backwards but it can if the state was previously overridden |
| // by DevTools. |
| if (user_idle_) { |
| user_idle_ = false; |
| fire_event = true; |
| } |
| } else if (!user_idle_) { |
| user_idle_ = true; |
| fire_event = true; |
| } |
| } else { |
| // The user is now active, so cancel any scheduled task to notify script |
| // that the user is idle. |
| timer_.Stop(); |
| |
| if (user_idle_) { |
| user_idle_ = false; |
| fire_event = true; |
| } |
| } |
| |
| if (fire_event) { |
| DispatchEvent(*Event::Create(event_type_names::kChange)); |
| } |
| } |
| |
| void IdleDetector::DispatchUserIdleEvent(TimerBase*) { |
| user_idle_ = true; |
| DispatchEvent(*Event::Create(event_type_names::kChange)); |
| } |
| |
| void IdleDetector::Trace(Visitor* visitor) const { |
| visitor->Trace(timer_); |
| visitor->Trace(signal_); |
| visitor->Trace(abort_handle_); |
| visitor->Trace(resolver_); |
| visitor->Trace(receiver_); |
| EventTarget::Trace(visitor); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| ActiveScriptWrappable::Trace(visitor); |
| } |
| |
| void IdleDetector::ContextDestroyed() { |
| Clear(); |
| } |
| |
| void IdleDetector::Clear() { |
| if (abort_handle_) { |
| CHECK(signal_); |
| signal_->RemoveAlgorithm(abort_handle_); |
| } |
| resolver_ = nullptr; |
| abort_handle_ = nullptr; |
| has_state_ = false; |
| receiver_.reset(); |
| } |
| |
| } // namespace blink |