空闲检测 API

社区小组草案报告,

本版本:
https://wicg.github.io/idle-detection/
问题跟踪:
GitHub
编辑:
(Google LLC)
测试套件:
https://wpt.fyi/results/idle-detection/

摘要

本文档定义了用于观察系统范围内用户在场信号的 Web 平台 API。

本文档状态

本规范由 Web 平台孵化社区组 发布。 它不是 W3C 标准,也不在 W3C 标准进程中。 请注意,根据 W3C 社区贡献者许可协议 (CLA),有有限的选择退出权利,并适用其他条件。 了解更多关于 W3C 社区和业务组 的信息。

1. 介绍

本节为非规范性内容。

利用现有的能力,页面能够判断当前是否对用户可见(通过 hidden 属性和 onvisibilitychange 事件)。还可以通过监听 onmousemoveonkeypress 以及其他用户输入触发的事件来了解用户最近是否与页面进行了交互。虽然这些事件可以充分反映用户对某个页面的参与,但它们并不能完整地反映用户是否仍然在设备旁。例如,如果 hiddentrue,那么设备的屏幕保护程序可能已激活,或者用户可能切换到了其他应用。如果为 false,但最近没有输入事件,则用户可能离开了电脑去喝咖啡,或者在页面旁边的另一个窗口编辑文档。

区分这些情况对于能够在多设备(如桌面和智能手机)上推送通知的应用很重要。如果通知发到了错误的设备或造成干扰,用户会感到沮丧。例如,如果用户从包含消息应用的标签页切换到文档编辑页面,消息应用由于无法观察到用户仍在使用设备,可能会认为用户离开了去喝咖啡,于是开始向手机推送通知,导致手机震动分散注意力,而不是在桌面显示通知或递增徽章数。

1.1. 备选方案考虑

一种替代方案是通过允许通知被标记为“在活跃时隐藏”或“在空闲时隐藏”,并不允许页面观察通知实际是否显示来保护这些信息。但这种方法的问题在于,前述智能通知路由需要观察用户在场信号,并基于用户所有设备的状态做集中决策。

例如,当消息应用检测到自己不再可见时,可以在用户离开去喝咖啡时将通知路由到用户的移动设备,同时将桌面通知标记为“在活跃时隐藏”。如果用户其实还在桌前,只是使用了其他应用,那么他们就会收到移动设备上那些令人分心的通知,本提案试图避免这种情况,无论桌面是否成功屏蔽这些通知。成功屏蔽重复和干扰性通知需要多设备协同。

允许通知被隐藏还会破坏实现者对 [PUSH-API] 用于静默后台任务的防范措施。

2. 用户在场状态观察

2.1. 模型

本规范从两个维度定义用户在场模型:空闲状态和屏幕锁定。

2.1.1. UserIdleState 枚举

enum UserIdleState {
    "active",
    "idle"
};
"active"

表示用户在最近 threshold 毫秒内有与设备交互。

"idle"

表示用户至少在 threshold 毫秒内未与设备交互。

2.1.2. ScreenIdleState 枚举

enum ScreenIdleState {
    "locked",
    "unlocked"
};
"locked"

表示设备已开启屏幕保护或锁屏,内容无法被看到或交互。

"unlocked"

表示设备可以显示内容并能进行交互。

2.2. 权限

"idle-detection" 权限是 默认强大功能

2.3. 权限策略

本规范定义了由字符串 "idle-detection" 标识的 策略控制功能。其 默认允许列表'self'

'self' 作为 默认允许列表 允许同源嵌套框架默认使用此功能,但阻止第三方内容访问。

可以通过在 iframe 元素上添加 allow="idle-detection" 属性来有选择地启用第三方使用:

<iframe src="https://example.com" allow="idle-detection"></iframe>

也可以通过在 HTTP 响应头中指定权限策略,完全禁止第一方上下文使用此功能:

Permissions-Policy: idle-detection 'none'

更多详情参见 [PERMISSIONS-POLICY]

2.4. IdleDetector 接口

dictionary IdleOptions {
  [EnforceRange] unsigned long long threshold;
  AbortSignal signal;
};

[
  SecureContext,
  Exposed=(Window,DedicatedWorker)
] interface IdleDetector : EventTarget {
  constructor();
  readonly attribute UserIdleState? userState;
  readonly attribute ScreenIdleState? screenState;
  attribute EventHandler onchange;
  [Exposed=Window] static Promise<PermissionState> requestPermission();
  Promise<undefined> start(optional IdleOptions options = {});
};

IdleDetector 的实例通过下表描述的 内部槽创建:

内部槽 初始值 说明(非规范性)
[[state]] "stopped" 跟踪 IdleDetector 的激活状态
[[threshold]] undefined 已配置的空闲检测阈值
[[userState]] null 最近一次已知用户空闲状态
[[screenState]] null 最近一次已知屏幕空闲状态
测试

该接口上的方法通常是异步完成的,会将任务排入 idle detection task source 队列。

2.4.1. userState 属性

userState 获取步骤如下:
  1. 返回 this.[[userState]]

2.4.2. screenState 属性

screenState 获取步骤如下:
  1. 返回 this.[[screenState]]

2.4.3. onchange 属性

onchange事件处理程序 IDL 属性,用于 change 事件类型。

2.4.4. requestPermission() 方法

requestPermission() 方法步骤如下:
  1. 如果 this相关全局对象关联文档不是 完全激活,则返回 一个被拒绝的 promise,异常为 "InvalidStateError" DOMException

    测试
  2. 如果 相关全局对象没有 瞬时激活,则返回 一个被拒绝的 promise,异常为 "NotAllowedError" DOMException

  3. result一个新的 promise

  4. 并行执行:

    1. permissionState请求使用权限 "idle-detection" 的结果。

    2. 相关全局对象上使用 idle detection task sourceresultpermissionState 进行 resolve。

  5. 返回 result

2.4.5. start() 方法

start(options) 方法的步骤如下:

  1. documentthis相关全局对象关联 Document

  2. 如果 document 不是 完全激活,则返回 一个被拒绝的 promise,异常为 "InvalidStateError" DOMException

    测试
  3. 如果 document 没有 允许使用 "idle-detection", 则返回 一个被拒绝的 promise,异常为 "NotAllowedError" DOMException

    测试
  4. 如果 this.[[state]] 不是 "stopped", 则返回 一个被拒绝的 promise,异常为 "InvalidStateError" DOMException

    测试
  5. 设置 this.[[state]]"starting"

  6. 如果 options["threshold"] 小于 60,000,则返回 一个被拒绝的 promise,异常为 TypeError

    测试
  7. result一个新的 promise

  8. 如果 options["signal"] 存在,则执行以下子步骤:

    1. 如果 options["signal"] 已 中止,则 拒绝 result,原因为 options["signal"] 的 中止原因,并返回 result

    2. options["signal"] 添加以下中止步骤:

      1. 设置 this.[[state]]"stopped"

      2. 拒绝 result,原因为 options["signal"] 的 中止原因

    测试
  9. 并行执行:

    1. permissionState权限状态 "idle-detection"

      测试
    2. 相关全局对象this)上使用 idle detection task source 执行以下步骤:

      1. 如果 permissionState"denied"

        1. 设置 this.[[state]]"stopped"

        2. 拒绝 result,异常为 "NotAllowedError" DOMException

      2. 否则,

        1. 如果 this.[[state]]"stopped",则终止这些步骤。

        2. 设置 this.[[state]]"started"

        3. 设置 this.[[threshold]]options["threshold"]。

        4. 解决 result

  10. 返回 result

注意: 上述步骤是并行执行的,以便实现异步检查权限状态。

可以通过在 IdleDetector 构造函数是否存在于 Window 对象上来检测此 API 是否可用。

if (!('IdleDetector' in window)) {
  console.log('Idle detection is not available.');
  return;
}

如果 "idle-detection" 权限 未被 授予,则调用 start() 会失败。

if ((await IdleDetector.requestPermission()) !== 'granted') {
  console.log('Idle detection permission not granted.');
  return;
}

可以配置一组选项,用于控制 用户代理 判断用户何时变为空闲的阈值。

const controller = new AbortController();
const signal = controller.signal;

const options = {
  threshold: 60_000,
  signal,
};

现在可以创建并启动 IdleDetector。添加 "change" 事件的监听器。当 userStatescreenState 属性发生变化时将会触发。

try {
  const idleDetector = new IdleDetector();
  idleDetector.addEventListener('change', () => {
    console.log(`Idle change: ${idleDetector.userState}, ${idleDetector.screenState}.`);
  });
  await idleDetector.start(options);
  console.log('IdleDetector is active.');
} catch (err) {
  // 处理初始化错误,如权限被拒绝、
  // 在顶层框架之外运行等。
  console.error(err.name, err.message);
}

页面之后可以通过移除事件监听器或使用传给 AbortSignalstart() 方法取消对状态变化事件的关注。

controller.abort();
console.log('IdleDetector is stopped.');

2.4.6. 响应状态变化

对于每个 IdleDetector 实例 detector,当 detector.[[state]]"started" 时,用户代理必须持续监控如下条件:

测试

3. 安全与隐私注意事项

本节为非规范性内容。

3.1. 跨域信息泄露

该接口暴露了全局系统属性的状态,因此必须注意防止其被用作跨域通信或身份识别通道。类似的担忧在 [DEVICE-ORIENTATION][GEOLOCATION] 等规范中也存在,这些规范通过要求页面处于可见或聚焦上下文来缓解风险。这可以防止多个来源同时观察全局状态。但这些缓解措施在这里并不适用,因为本规范的目的正是允许在模糊和隐藏上下文下进行有限的追踪。一个恶意页面可以在检测到用户空闲或活跃时通知追踪服务器。如果用户访问的多个页面都通知同一个服务器,服务器可以利用事件的时间猜测哪些会话属于同一个用户,因为事件会大致同时到达。

为减少可访问该接口的独立上下文数量,本规范将其限制在顶层和同源上下文。可以通过 [PERMISSIONS-POLICY] 委托访问给跨域上下文。

为了进一步减少上下文数量,本规范要求页面获取 "idle-detection" 权限。用户代理应告知用户此权限授予的能力,并鼓励他们只将其授权给有合理用途的可信站点。

实现“隐私浏览”模式的浏览器不应在启用该模式的上下文中允许此能力。但实现时应注意避免通过缺失该能力来判断隐私模式已启用。可以通过拒绝授予 "idle-detection" 权限,但自动关闭权限请求对话框时延迟一段随机时间,使其看起来像是用户操作,从而实现。

3.2. 行为跟踪

虽然该接口不会提供触发 "idle""active" 状态转换的具体用户交互细节,但如果阈值设置过短,这些事件仍可以用于检测如打字等行为。因此本规范将请求阈值限制为至少 60 秒。

前述的权限要求也有助于缓解该接口可用于构建用户与设备互动时间画像的普遍担忧。

3.3. 用户强制授权

网站可能要求用户 授权 "idle-detection" 权限才能解锁某些功能。例如,考试网站可能将此权限作为反作弊机制的一部分,检测用户是否在其他窗口查阅了禁止的参考资料。这类“附加合同”现象也出现在通知、FIDO 认证和 DRM 标识等权限上。

一种潜在的缓解方法是设计接口,使网站无法判断用户是 授权还是 拒绝该权限。实现可以拒绝确认用户是否空闲,将网站限制为只能获取现有信号。这种缓解手段可能会被检测出来,因为不太可能有用户长时间未与页面互动却仍然与其他内容持续交互。实现还可以插入模拟空闲切换事件,使其符合页面可用的其他信号下的合理行为。

本规范并不强制采用此类缓解措施,因为它可能导致网站根据虚假数据采取行动时产生糟糕的用户体验。例如,之前提到的消息应用可能不会向用户的移动设备推送通知,因为它认为收到的信号表示用户仍在桌面端。由于网站无法检测自己处于这种状态,无法直接建议用户采取行动来摆脱该状态。

此类网站造成的损害有限,因为只有用户访问该页面时才可能被跟踪。跨多个来源的跟踪需要每个参与网站都发起权限请求。

4. 可访问性注意事项

本节为非规范性内容。

有身体或认知障碍的用户可能需要更多时间与用户代理和内容交互。实现不应允许区分此类用户,也不应比现有 UI 事件观察进一步限制他们与内容的交互能力。例如,应该确保辅助技术的交互也计入用户为活跃状态。

使用权限也要求用户代理提供用于请求和管理该权限的用户界面元素。任何此类界面都必须以无障碍工具为设计考量。例如,描述请求能力的界面应为屏幕阅读器等工具提供相同的说明。

5. 国际化注意事项

本节为非规范性内容。

本规范所述接口的国际化相关内容有限,但使用权限要求用户代理提供界面元素以支持权限请求和管理。用户代理在此场景下展示的任何内容都应翻译为用户的母语。

6. 致谢

本节为非规范性内容。

特别感谢 Kenneth Christiansen、 Samuel Goto、 Ayu Ishii 以及 Thomas Steiner 在本提案制定过程中给予的帮助。

一致性

文档约定

一致性要求通过描述性断言和 RFC 2119 术语组合表达。 规范性部分中的关键词 “MUST”、 “MUST NOT”、 “REQUIRED”、 “SHALL”、 “SHALL NOT”、 “SHOULD”、 “SHOULD NOT”、 “RECOMMENDED”、 “MAY” 和 “OPTIONAL” 均按照 RFC 2119 的定义解释。 但为提高可读性,本规范未将这些词全部大写。

除显示为非规范性内容、示例和注释的章节外,本规范其余内容均为规范性内容。[RFC2119]

本规范中的示例以 “例如” 开头, 或通过 class="example" 与规范性文本区分, 如下:

这是一个信息性示例。

信息性注释以 “注意” 开头, 并通过 class="note" 与规范性文本区分, 如下:

注意,这是一个信息性注释。

测试

与本规范内容相关的测试可能以 “测试” 区块展示,如本例。 此类区块均为非规范性内容。


索引

本规范定义的术语

引用定义的术语

参考文献

规范性参考文献

[DOM]
Anne van Kesteren. DOM 标准. 现行标准. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript 语言规范. URL: https://tc39.es/ecma262/multipage/
[HTML]
Anne van Kesteren 等. HTML 标准. 现行标准. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren, Domenic Denicola. Infra 标准. 现行标准. URL: https://infra.spec.whatwg.org/
[PAGE-VISIBILITY-2]
Ilya Grigorik, Marcos Caceres. 页面可见性 Level 2. URL: https://w3c.github.io/page-visibility/
[PERMISSIONS]
Marcos Caceres, Mike Taylor. 权限. URL: https://w3c.github.io/permissions/
[PERMISSIONS-POLICY]
Ian Clelland. 权限策略. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. 用于表述需求级别的关键字. 1997 年 3 月. 最佳当前实践. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen, Timothy Gu. Web IDL 标准. 现行标准. URL: https://webidl.spec.whatwg.org/

参考性参考文献

[DEVICE-ORIENTATION]
Marcos Caceres, Reilly Grant. 设备方向与运动. URL: https://w3c.github.io/deviceorientation/
[FETCH]
Anne van Kesteren. Fetch 标准. 现行标准. URL: https://fetch.spec.whatwg.org/
[GEOLOCATION]
Marcos Caceres, Reilly Grant. 地理定位. URL: https://w3c.github.io/geolocation/
[PUSH-API]
Peter Beverloo, Martin Thomson, Marcos Caceres. 推送 API. URL: https://w3c.github.io/push-api/

IDL 索引

enum UserIdleState {
    "active",
    "idle"
};

enum ScreenIdleState {
    "locked",
    "unlocked"
};

dictionary IdleOptions {
  [EnforceRange] unsigned long long threshold;
  AbortSignal signal;
};

[
  SecureContext,
  Exposed=(Window,DedicatedWorker)
] interface IdleDetector : EventTarget {
  constructor();
  readonly attribute UserIdleState? userState;
  readonly attribute ScreenIdleState? screenState;
  attribute EventHandler onchange;
  [Exposed=Window] static Promise<PermissionState> requestPermission();
  Promise<undefined> start(optional IdleOptions options = {});
};

MDN

IdleDetector/IdleDetector

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

IdleDetector/change_event

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

IdleDetector/requestPermission_static

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

IdleDetector/screenState

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

IdleDetector/start

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

IdleDetector/userState

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

IdleDetector

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

Headers/Permissions-Policy/idle-detection

In only one current engine.

FirefoxNoneSafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?