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
fi

Start 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.shop

kill 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
done

If 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 ;;
esac

Wait 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" 120

Type 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 enter

Any 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")
    raise
try {
  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

Retry 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
done
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 it
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/

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 back
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

Reading 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]]' --wait

The 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
wait

Many phones goes further: naming, per-phone proxies and keeping a shelf healthy.