Running many phones

Name phones, run them in parallel from Python, Node.js or the shell, renew mobile IP addresses, and keep a shelf of phones connected for days.

One droidline serve drives as many phones as your PC and network carry. Each phone gets its own command queue: commands to one phone run in order, and different phones run at the same time.

Give every phone a name

Every phone has a random ID such as k7d2q9xa. Give it a name you can remember when you pair it, or rename it later:

droidline pair 482913 --name shelf-01
droidline rename k7d2q9xa shelf-02
droidline devices

Names may use letters, digits, dots, dashes and underscores. A label with the same name stuck on the phone itself saves a lot of guessing.

Run them in parallel

Ask the server which phones are online, then give each one its own thread, task or background job:

from concurrent.futures import ThreadPoolExecutor
from droidline import Droidline

dl = Droidline()

def morning(name: str) -> str:
    d = dl.device(name)
    d.wake()
    d.launch("com.example.app")
    d.touch("text", "Check in", timeout=20)
    return d.get_text("id", "status")

names = [p["name"] for p in dl.devices() if p["online"]]
with ThreadPoolExecutor(len(names) or 1) as pool:
    for name, status in zip(names, pool.map(morning, names)):
        print(name, status)
import { Droidline } from "droidline";

const dl = new Droidline();
const phones = (await dl.devices()).filter((p) => p.online);

const results = await Promise.allSettled(phones.map(async (p) => {
  const d = dl.device(p.name);
  await d.wake();
  await d.launch("com.example.app");
  await d.touch("text", "Check in", { timeout: 20 });
  return d.getText("id", "status");
}));

results.forEach((r, i) => console.log(phones[i].name, r.status === "fulfilled" ? r.value : r.reason.message));
for name in $(droidline --json devices | jq -r '.value[] | select(.online) | .name'); do
  (
    droidline -d "$name" wake
    droidline -d "$name" launch com.example.app
    droidline -d "$name" touch text "Check in" --timeout 20
    echo "$name: $(droidline -d "$name" get_text id status)"
  ) &
done
wait

One phone failing does not stop the others: in Python the exception comes out of pool.map when you reach that phone's result, in Node.js allSettled records it, and in the shell each phone runs in its own subshell.

Give each phone its own network

With the per-app proxy, each phone can send one app through a different upstream. Save the upstreams once and assign them by name:

droidline proxy add kr1 socks5://user:pass@203.0.113.10:1080
droidline proxy add kr2 socks5://user:pass@203.0.113.11:1080
plan = {"shelf-01": "@kr1", "shelf-02": "@kr2"}
for name, upstream in plan.items():
    dl.device(name).proxy(upstream, app="com.android.chrome")
const plan = { "shelf-01": "@kr1", "shelf-02": "@kr2" };
for (const [name, upstream] of Object.entries(plan)) {
  await dl.device(name).proxy(upstream, { app: "com.android.chrome" });
}
droidline -d shelf-01 proxy @kr1 --app com.android.chrome
droidline -d shelf-02 proxy @kr2 --app com.android.chrome

New mobile IP addresses

On mobile data, switching airplane mode on and off usually gets a new address from the carrier. The phone cannot receive commands while it is offline, so send both switches in one batch, which the phone finishes on its own:

d.batch([("airplane", True), ("sleep", 3000), ("airplane", False)], wait=True)
await d.batch([["airplane", true], ["sleep", 3000], ["airplane", false]], { wait: true });
droidline -d shelf-01 batch '[["airplane",true],["sleep",3000],["airplane",false]]' --wait

Without wait the call returns {"accepted": true} at once and the result arrives later as a result event.

Screens that turn off

Taps and swipes fail while a phone's screen is off. Call wake() before each job; it turns the screen on and dismisses a lock screen that has no PIN. For phones that run unattended, remove the PIN, pattern or password lock in the phone's settings, because Droidline cannot unlock it.

Staying connected for days

  • Set battery optimization to Not restricted for Droidline, and allow it in any extra background limits your phone brand adds (Troubleshooting lists the usual places).
  • The app starts by itself when the phone boots, once it is paired.
  • After a reconnect the phone resends replies the PC missed, and repeated commands are not run twice. See Concepts.
  • droidline devices shows each phone's route and missing permissions. last_seen in devices() tells you when an offline phone was last connected.
  • Keep the phones charging. A USB charger is enough; no data connection is needed.

Watching the shelf

Subscribe to device events to hear the moment a phone drops off or comes back:

from droidline import Droidline

dl = Droidline()
dl.subscribe(["device"])
dl.on("device", lambda e: print(e["name"], e["state"], e.get("route")))
input("Watching. Press Enter to stop.\n")
droidline --json devices | jq -r '.value[] | "\(.name)\t\(if .online then "online" else "offline" end)\t\(.route)"'

Updates

The app checks GitHub Releases once a day and shows a notification when a new version exists. Installing the update over the old app keeps the pairing.