通知与 Webhook

在电脑上接收手机的通知:等待某一条、响应每一条,回复、打开或清除通知,并把它们转发到 Webhook。

本页为译文。如与英文版不一致,以英文版为准。 English

Droidline 可以把手机收到的通知传到电脑。它使用 Android 的通知使用权,能拿到完整的标题和正文(只靠无障碍服务只能看到截短的版本)。

开启

  1. 在手机上打开 Droidline 的设置标签页,允许通知使用权。
  2. 选择哪些应用可以把通知发送到电脑。在你选择之前不会发送任何通知,私人消息因此会留在手机上。可以在应用的状态标签页中的发送到电脑的通知下选择应用,也可以在代码中设置:
d.notify_filter(["com.google.android.gm", "com.google.android.apps.messaging"])
print(d.notify_filter())          # 读取当前列表
await d.notifyFilter(["com.google.android.gm", "com.google.android.apps.messaging"]);
console.log(await d.notifyFilter());   // 读取当前列表
droidline notify_filter com.google.android.gm,com.google.android.apps.messaging
droidline notify_filter

等待一条通知

wait_notification 会一直阻塞,直到匹配的通知到达,然后返回这条通知:

n = d.wait_notification("textContains", "verification code", 60)
print(n["package"], n["title"], n["text"])
const n = await d.waitNotification("textContains", "verification code", 60);
console.log(n.package, n.title, n.text);
droidline wait_notification textContains "verification code" 60

第一个参数指定比较什么:text 或 textContains 同时查看标题和正文,title 只看标题,package 匹配应用。同时传入 package=,就只接受来自某一个应用的通知。

等待由服务器完成,所以在此期间,发给同一部手机的其他命令照常执行。它等待的是调用之后到达的通知。要查看已经显示的通知,请使用 notifications() 或 has_notification(by, value)。

响应每一条通知

stop = d.on_notification(package="com.google.android.gm",
                         callback=lambda n: print(n["title"], n["text"]))
# ... 程序会继续运行;调用 stop() 结束接收
const stop = await d.onNotification({ package: "com.google.android.gm" },
  (n) => console.log(n.title, n.text));
// ... 调用 stop() 结束接收;在此之前进程会一直运行
droidline on_notification --package com.google.android.gm

CLI 每收到一条通知就打印一行 JSON,直到你按下 Ctrl+C,因此很容易通过管道交给其他工具处理。一条通知的格式如下:

{"event":"notification","device":"k7d2q9xa","key":"0|com.google.android.gm|1|null|10123","package":"com.google.android.gm","title":"New message","text":"Lunch at 12?","time":1791360000000,"actions":["reply","mark_read"]}

处理通知

每条通知都有一个 key。用它来打开、回复或清除这条通知:

for n in d.notifications():
    if "Bank" in n["title"]:
        d.notification_click(n["key"])        # 打开通知指向的内容

d.notification_reply(key, "On my way")       # 仅适用于带回复操作的通知
d.notification_dismiss(key)
for (const n of await d.notifications()) {
  if (n.title.includes("Bank")) await d.notificationClick(n.key);
}

await d.notificationReply(key, "On my way");
await d.notificationDismiss(key);
KEY=$(droidline --json notifications | jq -r '.value[0].key')
droidline notification_click "$KEY"

actions 列出这条通知提供的操作。只有其中包含 reply 时,notification_reply 才有效。

手机离线时

手机无法连接电脑时,会保留最多 500 条最近的通知,并在重新连接后按顺序发送。

Webhook

服务器可以把匹配的通知 POST 到任意 URL,这样其他系统不需要 SDK 也能做出响应。在 config.toml 中为每个 Webhook 添加一节,然后重启 droidline serve:

[[webhooks]]
url = "https://example.com/hooks/droidline"
package = "com.google.android.gm"   # 可选
text_contains = "order"             # 可选,匹配标题或正文
device = "shelf-01"                 # 可选

请求体是 JSON 格式的通知,外加 device 和 device_name。每个请求都带有签名,你可以据此确认它来自你的服务器:

X-Droidline-Timestamp: 1791360000
X-Droidline-Signature: sha256=<hex HMAC-SHA256 of timestamp + "." + body>

用 droidline webhook secret 获取签名密钥。在接收端验证签名:

import hashlib, hmac

def valid(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest("sha256=" + mac, signature)
import { createHmac, timingSafeEqual } from "node:crypto";

function valid(secret, timestamp, body, signature) {
  const mac = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.`).update(body).digest("hex");
  return mac.length === signature.length && timingSafeEqual(Buffer.from(mac), Buffer.from(signature));
}

投递失败时会重试两次,分别在 1 秒和 4 秒之后。

Android 15 及以上的验证码

Android 15 会对非系统自带的应用隐藏通知中的一次性验证码。你仍然能收到通知,但验证码被遮盖了。可以把通知当作信号,然后在应用内读取验证码:

d.wait_notification("package", "com.example.bank", 120)
d.launch("com.example.bank")
code = d.get_text("id", "otp_value")
await d.waitNotification("package", "com.example.bank", 120);
await d.launch("com.example.bank");
const code = await d.getText("id", "otp_value");
droidline wait_notification package com.example.bank 120
droidline launch com.example.bank
droidline get_text id otp_value