Recipes
Ready-to-copy patterns for everyday tasks in Python, Node.js and the shell, from starting an app fresh and dismissing pop-ups to reading lists, retrying, one-time codes and new IP addresses.
Each recipe solves one common task. Pick your language once and every example on the page follows. The examples assume a connected phone in d (d = connect() in Python, const d = await connect() in Node.js); in the shell, every droidline command finds the phone by itself.
Make sure the phone is ready
Wake the screen and check that it is not locked behind a PIN, which Droidline cannot unlock:
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
fiStart an app from a clean state
launch brings an app to the front where it left off. To start from its first screen, stop it first. To start as if freshly installed, clear its data:
d.kill("com.example.shop") # force stop: takes a few seconds
d.launch("com.example.shop")
d.clear_data("com.example.shop") # wipes logins and settings too
d.launch("com.example.shop")await d.kill("com.example.shop"); // force stop: takes a few seconds
await d.launch("com.example.shop");
await d.clearData("com.example.shop"); // wipes logins and settings too
await d.launch("com.example.shop");droidline kill com.example.shop
droidline launch com.example.shopkill and clear_data press the buttons in Android's settings for you, so they take a few seconds and show briefly on the screen. droidline apps lists package names.
Dismiss pop-ups that may or may not appear
Ads, rating requests and tips come and go. Check for each with a condition, which never waits and never fails:
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
doneIf a pop-up appears a moment after a screen opens, wait for whichever comes first with which, as in the next recipe.
Handle a step that can end in different places
After a tap, the app might show the next screen, an error, or a pop-up. which waits for the first of several candidates and tells you which one appeared:
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 ;;
esacWait for something to finish
Wait for a spinner or a "Loading" label to disappear, or for a result to appear:
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" 120Type text and press keys
input fills a field, replacing what was there; append=True adds to it instead. sendkey presses a key, such as Enter to submit a search:
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 enterAny language and any script works, including Korean, Chinese and emoji. Key names include enter, tab, del, space, escape, back and home.
Read every item in a list
dump returns the whole tree, so you can collect values with a few lines of ordinary code:
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(. != "")'For a long list, collect, swipe up, collect again, and stop when nothing new appears:
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);Keep a screenshot when something fails
Wrap a run so that any failure leaves a picture and a dump behind:
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"' ERRRetry a step that sometimes fails
Errors say whether retrying might help. Retry only those:
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
doneOpen a web page or a deep link
d.chrome.go("droidline.dev") # in Chrome, in the current tab
d.open_url("https://droidline.dev/docs/") # in the default browser
d.open_url("myapp://orders/42") # straight to a screen of an app that supports itawait 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/Deep links are often the only way to reach a screen that launch cannot open directly.
Wait for a code in a notification
First allow the app's notifications to reach the PC (once, with notify_filter or on the app's Status tab). Then wait for the message and pull the digits out:
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"Copy and paste through the clipboard
d.clipboard("text to paste")
d.long_touch("id", "message") # the field's menu offers Paste
d.touch("text", "Paste")
print(d.clipboard()) # read it backawait 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 clipboardReading the clipboard on Android 10 and later needs the Droidline keyboard to be the current keyboard.
Get a new mobile IP address
Turning airplane mode on and off makes the carrier assign a new address. The phone loses its connection in between, so run it as one batch that the phone finishes by itself:
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]]' --waitThe phone reconnects to the PC by itself once airplane mode is off; Concepts explains how commands like this report back.
Run the same steps on every phone
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
waitMany phones goes further: naming, per-phone proxies and keeping a shelf healthy.