常用示例
可直接复制的日常任务写法,涵盖 Python、Node.js 和 shell:从全新启动应用、关闭弹窗,到读取列表、重试、一次性验证码和获取新的 IP 地址。
本页为译文。如与英文版不一致,以英文版为准。 English
每个示例解决一个常见任务。选择一次语言,页面上的所有示例都会随之切换。示例假设 d 是一部已连接的手机(Python 中为 d = connect(),Node.js 中为 const d = await connect());在 shell 中,每条 droidline 命令都会自己找到手机。
确认手机已就绪
唤醒屏幕,并检查它没有被 PIN 锁住,因为 Droidline 无法解锁 PIN:
d.wake()
if d.locked():
raise SystemExit("Unlock the phone, or turn off its screen lock, first")
if d.battery()["level"] < 15:
print("Battery is low")await d.wake();
if (await d.locked()) throw new Error("Unlock the phone, or turn off its screen lock, first");
if ((await d.battery()).level < 15) console.log("Battery is low");droidline wake
if [ "$(droidline locked)" = "true" ]; then
echo "Unlock the phone first" >&2; exit 1
fi从干净的状态启动应用
launch 会把应用带到前台,停在它上次离开的地方。要从第一个界面开始,先停止它。要像刚安装一样开始,就清除它的数据:
d.kill("com.example.shop") # 强制停止:需要几秒钟
d.launch("com.example.shop")
d.clear_data("com.example.shop") # 也会清除登录状态和设置
d.launch("com.example.shop")await d.kill("com.example.shop"); // 强制停止:需要几秒钟
await d.launch("com.example.shop");
await d.clearData("com.example.shop"); // 也会清除登录状态和设置
await d.launch("com.example.shop");droidline kill com.example.shop
droidline launch com.example.shopkill 和 clear_data 会替你点击 Android 设置中的按钮,所以需要几秒钟,屏幕上也会短暂显示这些操作。droidline apps 会列出包名。
关闭可能出现也可能不出现的弹窗
广告、评分请求和提示时有时无。用条件逐一检查,条件从不等待,也从不失败:
for text in ["Not now", "Close ad", "Skip", "Later"]:
if d.exists("text", text):
d.touch("text", text)for (const text of ["Not now", "Close ad", "Skip", "Later"]) {
if (await d.exists("text", text)) await d.touch("text", text);
}for text in "Not now" "Close ad" "Skip" "Later"; do
if [ "$(droidline exists text "$text")" = "true" ]; then
droidline touch text "$text"
fi
done如果弹窗在界面打开之后片刻才出现,就像下一个示例那样,用 which 等待最先出现的那一个。
处理结果不确定的步骤
点击之后,应用可能显示下一个界面、一条错误或一个弹窗。which 等待几个候选项中最先出现的那一个,并告诉你出现的是哪一个:
d.touch("text", "Pay")
result = d.which([("text", "Payment complete"),
("textContains", "declined"),
("text", "Confirm")], timeout=30)
if result == 2:
d.touch("text", "Confirm")
elif result != 0:
d.screenshot("payment-problem.png")await d.touch("text", "Pay");
const result = await d.which([["text", "Payment complete"],
["textContains", "declined"],
["text", "Confirm"]], { timeout: 30 });
if (result === 2) await d.touch("text", "Confirm");
else if (result !== 0) await d.screenshot("payment-problem.png");droidline touch text Pay
result=$(droidline which text="Payment complete" textContains=declined text=Confirm --timeout 30)
case "$result" in
2) droidline touch text Confirm ;;
0) ;;
*) droidline screenshot payment-problem.png ;;
esac等待某件事完成
等待加载动画或“Loading”标签消失,或者等待结果出现:
d.wait_gone("id", "progress", 60)
d.wait("textContains", "Upload complete", 120)await d.waitGone("id", "progress", 60);
await d.wait("textContains", "Upload complete", 120);droidline wait_gone id progress 60
droidline wait textContains "Upload complete" 120输入文字和按键
input 填写输入框,替换原有内容;append=True 则改为追加。sendkey 按下一个键,例如按 Enter 提交搜索:
d.input("id", "search", "android automation")
d.sendkey("enter")
d.input("id", "note", " and more", append=True)
d.clear("id", "note")await d.input("id", "search", "android automation");
await d.sendkey("enter");
await d.input("id", "note", " and more", { append: true });
await d.clear("id", "note");droidline input id search "android automation"
droidline sendkey enter任何语言和文字都可以输入,包括韩文、中文和 emoji。键名包括 enter、tab、del、space、escape、back 和 home。
读取列表中的每一项
dump 返回整棵树,所以用几行普通代码就能收集其中的值:
def texts(node):
if node.get("text"):
yield node["text"]
for child in node.get("children", []):
yield from texts(child)
screen = d.dump()
print(list(texts(screen["tree"])))function* texts(node) {
if (node.text) yield node.text;
for (const child of node.children ?? []) yield* texts(child);
}
const screen = await d.dump();
console.log([...texts(screen.tree)]);droidline dump | jq -r '.. | .text? // empty | select(. != "")'对于很长的列表,收集一次,向上滑动,再收集,直到没有新内容出现为止:
seen = []
while True:
new = [t for t in texts(d.dump()["tree"]) if t not in seen]
if not new:
break
seen += new
d.swipe("up")
print(seen)const seen = [];
for (;;) {
const fresh = [...texts((await d.dump()).tree)].filter((t) => !seen.includes(t));
if (fresh.length === 0) break;
seen.push(...fresh);
await d.swipe("up");
}
console.log(seen);失败时保留截图
把一次运行包起来,这样任何失败都会留下一张截图和一份界面转储:
from datetime import datetime
from droidline import DroidlineError
try:
run_my_steps(d)
except DroidlineError:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
d.screenshot(f"fail-{stamp}.png")
d.dump(f"fail-{stamp}.json")
raisetry {
await runMySteps(d);
} catch (e) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
await d.screenshot(`fail-${stamp}.png`);
await d.dump(`fail-${stamp}.json`);
throw e;
}trap 'droidline screenshot "fail-$(date +%s).png"' ERR重试偶尔失败的步骤
错误会说明重试是否可能有用。只重试这类错误:
import time
from droidline import DroidlineError
for attempt in range(3):
try:
d.touch("text", "Refresh")
break
except DroidlineError as e:
if not e.retryable or attempt == 2:
raise
time.sleep(2)import { DroidlineError } from "droidline";
for (let attempt = 0; ; attempt++) {
try {
await d.touch("text", "Refresh");
break;
} catch (e) {
if (!(e instanceof DroidlineError) || !e.retryable || attempt === 2) throw e;
await new Promise((r) => setTimeout(r, 2000));
}
}for attempt in 1 2 3; do
droidline touch text Refresh && break
sleep 2
done打开网页或深层链接
d.chrome.go("droidline.dev") # 在 Chrome 的当前标签页中打开
d.open_url("https://droidline.dev/docs/") # 在默认浏览器中打开
d.open_url("myapp://orders/42") # 直接进入支持深层链接的应用中的某个界面await d.chrome.go("droidline.dev");
await d.openUrl("https://droidline.dev/docs/");
await d.openUrl("myapp://orders/42");droidline chrome.go droidline.dev
droidline open_url https://droidline.dev/docs/对于 launch 无法直接打开的界面,深层链接往往是唯一的到达途径。
等待通知中的验证码
先允许应用的通知发送到电脑(只需一次,用 notify_filter 或在应用的状态标签页中设置)。然后等待消息,并取出其中的数字:
import re
d.notify_filter(["com.google.android.apps.messaging"])
n = d.wait_notification("textContains", "code", 120)
code = re.search(r"\d{4,8}", n["text"]).group()
d.input("id", "otp", code)await d.notifyFilter(["com.google.android.apps.messaging"]);
const n = await d.waitNotification("textContains", "code", 120);
const code = n.text.match(/\d{4,8}/)[0];
await d.input("id", "otp", code);code=$(droidline --json wait_notification textContains code 120 | jq -r '.value.text' | grep -oE '[0-9]{4,8}' | head -1)
droidline input id otp "$code"通过剪贴板复制和粘贴
d.clipboard("text to paste")
d.long_touch("id", "message") # 输入框的菜单中有 Paste
d.touch("text", "Paste")
print(d.clipboard()) # 再读取出来await d.clipboard("text to paste");
await d.longTouch("id", "message");
await d.touch("text", "Paste");
console.log(await d.clipboard());droidline clipboard "text to paste"
droidline clipboard在 Android 10 及以上版本上读取剪贴板,需要把 Droidline 键盘设为当前键盘。
获取新的移动 IP 地址
开关一次飞行模式,运营商就会分配一个新地址。在此期间手机会断开连接,所以要把它作为一个 batch 运行,由手机自行完成:
d.batch([("airplane", True), ("sleep", 3000), ("airplane", False)], wait=True)
print(d.network()) # {'type': 'mobile', 'airplane': False, 'metered': True}await d.batch([["airplane", true], ["sleep", 3000], ["airplane", false]], { wait: true });
console.log(await d.network());droidline batch '[["airplane",true],["sleep",3000],["airplane",false]]' --wait飞行模式关闭后,手机会自行重新连接电脑;概念一页说明了这类命令如何报告结果。
在每部手机上运行相同的步骤
from concurrent.futures import ThreadPoolExecutor
from droidline import Droidline
dl = Droidline()
names = [p["name"] for p in dl.devices() if p["online"]]
def job(name):
d = dl.device(name)
d.launch("com.android.chrome")
return name, d.current()["package"]
with ThreadPoolExecutor(len(names) or 1) as pool:
print(list(pool.map(job, names)))import { Droidline } from "droidline";
const dl = new Droidline();
const online = (await dl.devices()).filter((p) => p.online);
console.log(await Promise.all(online.map(async (p) => {
const d = dl.device(p.name);
await d.launch("com.android.chrome");
return [p.name, (await d.current()).package];
})));for name in $(droidline --json devices | jq -r '.value[] | select(.online) | .name'); do
droidline -d "$name" launch com.android.chrome &
done
wait多部手机一页介绍了更多内容:命名、为每部手机设置代理,以及让一整架手机保持正常运行。