blob: d2807edd831016e2a840482807ac078ac5803c0c [file] [log] [blame]
// 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