Your first script
Build a complete login script step by step against a simulated phone, with every line explained, in Python, Node.js or the shell.
In this tutorial you write a script that opens an app, closes an ad if one is showing, signs in, checks that the sign-in worked, reads two values from the screen and saves a screenshot. Along the way you meet the ideas you will use in every script: finding elements, waiting, conditions and errors.
You do not need a phone. Droidline comes with droidline-fakephone, a small program that pretends to be a phone running a demo app. It speaks exactly the same protocol as the real app, so the script you write here also runs on a real phone.
Before you start, finish Installation steps 1, 2 and 5: the droidline programs on your PATH, and the library for your language if you use Python or Node.js.
1. Start the server and the simulated phone
You need three terminal windows.
Terminal 1 runs the server. Leave it open:
droidline serve
Terminal 2 runs the simulated phone. It finds the server on your network and prints a pairing code:
droidline-fakephone
Found MY-PC at 192.168.0.12:8779
Pairing code: 482913
On the PC run: droidline pair 482913
Terminal 3 is where you work. Approve the code and give the phone a name:
droidline pair 482913 --name fake-01
droidline devices
NAME ID MODEL ANDROID STATE ROUTE MISSING
fake-01 lpez3oxg Droidline Fake 14 online lan
2. Look at the screen first
Before writing a script for any app, look at how Droidline sees its screens. Open the demo app and save its screen:
droidline launch dev.droidline.demo
droidline dump screen.json
screen.json describes every element on the screen. Here are the parts of the login screen that matter, with the other fields left out:
{
"package": "dev.droidline.demo",
"activity": ".LoginActivity",
"tree": {
"children": [
{ "text": "Sign in", "id": "", "class": "android.widget.TextView" },
{ "text": "", "id": "dev.droidline.demo:id/email", "desc": "Email", "class": "android.widget.EditText" },
{ "text": "", "id": "dev.droidline.demo:id/password", "desc": "Password", "class": "android.widget.EditText" },
{ "text": "Keep me signed in", "id": "dev.droidline.demo:id/auto_login", "class": "android.widget.CheckBox", "checked": false },
{ "text": "Log in", "id": "dev.droidline.demo:id/login", "class": "android.widget.Button", "enabled": false },
{ "text": "Close ad", "id": "dev.droidline.demo:id/ad_close", "class": "android.widget.Button" }
]
}
}
Three fields tell elements apart:
textis what the element shows, such asLog in.idis a name the app's developer gave the element. It does not change with the phone's language, so it is the most reliable choice when it exists. You can leave out the part before:id/:emailmatchesdev.droidline.demo:id/email.descis a description for screen readers, often the only label an icon has.
A field and a value together are called a selector: ("text", "Log in") or ("id", "email"). Every command that acts on an element takes a selector. Notice that Log in is enabled: false: the app only enables it after an email is typed.
3. Write the script step by step
Each step below adds a few lines. Pick your language in any of the examples and the rest of the page follows.
Connect and open the app
from droidline import connect
d = connect()
d.launch("dev.droidline.demo")
d.wait("text", "Sign in")import { connect } from "droidline";
const d = await connect();
await d.launch("dev.droidline.demo");
await d.wait("text", "Sign in");#!/usr/bin/env bash
set -e # stop at the first command that fails
droidline launch dev.droidline.demo
droidline wait text "Sign in"connect() finds the only phone that is online and returns an object that sends commands to it, called d by convention. launch starts an app by its package name, the unique name every Android app has. wait holds until an element appears, up to 10 seconds by default, so the next line never runs against a screen that is still loading.
In the shell there is nothing to connect: every droidline command finds the server and the phone by itself.
Close the ad only if it is there
if d.exists("text", "Close ad"):
d.touch("text", "Close ad")if (await d.exists("text", "Close ad")) {
await d.touch("text", "Close ad");
}if [ "$(droidline exists text "Close ad")" = "true" ]; then
droidline touch text "Close ad"
fiexists is a condition: it answers at once with true or false and never fails. That makes it the right tool for things that may or may not be on the screen, like ads and pop-ups. touch finds the element, waits for it if needed and taps it.
Type the email
d.input("id", "email", "knife")await d.input("id", "email", "knife");droidline input id email knifeinput takes a selector and the text to type. It replaces whatever was in the field. Here the selector uses id, because the email field has no text of its own to match.
Tick the checkbox unless it is ticked
if not d.checked("id", "auto_login"):
d.touch("id", "auto_login")if (!(await d.checked("id", "auto_login"))) {
await d.touch("id", "auto_login");
}if [ "$(droidline checked id auto_login)" = "false" ]; then
droidline touch id auto_login
fiTapping a checkbox toggles it, so tapping blindly would untick it when it was already ticked. checked reads the current state first.
Log in and check that it worked
d.touch("text", "Log in")
screen = d.which([("text", "Log in"), ("id", "main_tab")], timeout=15)
if screen != 1:
d.screenshot("login-failed.png")
raise SystemExit("Did not reach the main screen")await d.touch("text", "Log in");
const screen = await d.which([["text", "Log in"], ["id", "main_tab"]], { timeout: 15 });
if (screen !== 1) {
await d.screenshot("login-failed.png");
throw new Error("Did not reach the main screen");
}droidline touch text "Log in"
screen=$(droidline which text="Log in" id=main_tab --timeout 15)
if [ "$screen" != "1" ]; then
droidline screenshot login-failed.png
echo "Did not reach the main screen" >&2
exit 1
fiAfter tapping Log in, two things can happen: the main screen opens, or the login screen stays because something went wrong. which waits for whichever of its candidates appears first and returns its position in the list: 0 for the first, 1 for the second, -1 if none appeared before the timeout. A screenshot of the failure makes it easy to see later what went wrong.
Read values and save a screenshot
print(d.get_text("id", "greeting"))
print("Balance:", d.get_text("id", "balance"))
print("Chats:", d.count("class", "android.widget.CheckBox"))
d.screenshot("after-login.png")console.log(await d.getText("id", "greeting"));
console.log("Balance:", await d.getText("id", "balance"));
console.log("Chats:", await d.count("class", "android.widget.CheckBox"));
await d.screenshot("after-login.png");droidline get_text id greeting
echo "Balance: $(droidline get_text id balance)"
echo "Chats: $(droidline count class android.widget.CheckBox)"
droidline screenshot after-login.pngget_text returns an element's text, and count returns how many elements match. The screenshot is saved on your PC, not on the phone.
4. The whole script
from droidline import connect
d = connect()
# Open the demo app and wait for its login screen.
d.launch("dev.droidline.demo")
d.wait("text", "Sign in")
# Close the ad, but only if it is showing.
if d.exists("text", "Close ad"):
d.touch("text", "Close ad")
# Type the email and tick "Keep me signed in" unless it is ticked.
d.input("id", "email", "knife")
if not d.checked("id", "auto_login"):
d.touch("id", "auto_login")
# Log in, then check which screen appeared.
d.touch("text", "Log in")
screen = d.which([("text", "Log in"), ("id", "main_tab")], timeout=15)
if screen != 1:
d.screenshot("login-failed.png")
raise SystemExit("Did not reach the main screen")
# Read what the app shows and keep a picture.
print(d.get_text("id", "greeting"))
print("Balance:", d.get_text("id", "balance"))
print("Chats:", d.count("class", "android.widget.CheckBox"))
d.screenshot("after-login.png")
print("done")import { connect } from "droidline";
const d = await connect();
// Open the demo app and wait for its login screen.
await d.launch("dev.droidline.demo");
await d.wait("text", "Sign in");
// Close the ad, but only if it is showing.
if (await d.exists("text", "Close ad")) {
await d.touch("text", "Close ad");
}
// Type the email and tick "Keep me signed in" unless it is ticked.
await d.input("id", "email", "knife");
if (!(await d.checked("id", "auto_login"))) {
await d.touch("id", "auto_login");
}
// Log in, then check which screen appeared.
await d.touch("text", "Log in");
const screen = await d.which([["text", "Log in"], ["id", "main_tab"]], { timeout: 15 });
if (screen !== 1) {
await d.screenshot("login-failed.png");
throw new Error("Did not reach the main screen");
}
// Read what the app shows and keep a picture.
console.log(await d.getText("id", "greeting"));
console.log("Balance:", await d.getText("id", "balance"));
console.log("Chats:", await d.count("class", "android.widget.CheckBox"));
await d.screenshot("after-login.png");
console.log("done");#!/usr/bin/env bash
set -e # stop at the first command that fails
# Open the demo app and wait for its login screen.
droidline launch dev.droidline.demo
droidline wait text "Sign in"
# Close the ad, but only if it is showing.
if [ "$(droidline exists text "Close ad")" = "true" ]; then
droidline touch text "Close ad"
fi
# Type the email and tick "Keep me signed in" unless it is ticked.
droidline input id email knife
if [ "$(droidline checked id auto_login)" = "false" ]; then
droidline touch id auto_login
fi
# Log in, then check which screen appeared.
droidline touch text "Log in"
screen=$(droidline which text="Log in" id=main_tab --timeout 15)
if [ "$screen" != "1" ]; then
droidline screenshot login-failed.png
echo "Did not reach the main screen" >&2
exit 1
fi
# Read what the app shows and keep a picture.
droidline get_text id greeting
echo "Balance: $(droidline get_text id balance)"
echo "Chats: $(droidline count class android.widget.CheckBox)"
droidline screenshot after-login.png
echo doneSave it as login.py, login.mjs or login.sh and run it:
python login.pynode login.mjsbash login.shWelcome, knife
Balance: 12,500
Chats: 3
done
The shell version also prints each command's reply, such as {"ms":0,"via":"node"}. On Windows, run the shell version in Git Bash, or see the CLI guide for the same script in PowerShell.
5. When something goes wrong
Change "Log in" to "Sign up" in the touch step and run the script again. It stops with an error like this (the Python version shows it as the last line of a traceback):
droidline._generated.NotFoundError: Could not find text 'Sign up' within 10s. Current screen: dev.droidline.demo / .LoginActivity
The message says what was searched for, how long, and which screen was showing instead. Every error also has a code (NOT_FOUND here) and says whether retrying might help. To handle an error instead of stopping:
from droidline import connect, NotFoundError
d = connect()
try:
d.touch("text", "Sign up", timeout=2)
except NotFoundError as e:
print(e.code, e.retryable, e)import { connect, DroidlineError } from "droidline";
const d = await connect();
try {
await d.touch("text", "Sign up", { timeout: 2 });
} catch (e) {
if (!(e instanceof DroidlineError)) throw e;
console.log(e.code, e.retryable, e.message);
}if ! droidline touch text "Sign up" --timeout 2; then
echo "not there, carrying on"
fitimeout=2 makes the command give up after 2 seconds instead of 10. In the shell, a failed command prints the error and exits with status 1, which if ! catches. Error codes lists every code.
6. Run it on a real phone
The same script runs on a real phone. Only the details of the app change:
- Pair the real phone (Quick start). If the simulated phone is still online, stop it with Ctrl+C, or pass the phone's name:
connect("shelf-01")in the SDKs,--device shelf-01in the CLI. - Replace
dev.droidline.demowith the package name of the app you want to automate.droidline appslists the apps on the phone with their package names. - Open each screen on the phone, run
droidline dump screen.json, and choose selectors from what you find. Finding elements explains how to choose well. - Write texts in the phone's language. On a phone set to Korean, a button labelled "Log in" in English is probably "로그인".
Next, read Concepts to understand what happens behind each command, or browse the Recipes for ready-made patterns.