Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Android physical devices through the host's default adb server

Open
#441 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
18/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
android, typescript
Domain
mobile

Research direction

Start with src/drivers/android/index.ts to see how AndroidDriver routes calls, then adb-server.ts for the existing probe and timeout pattern the spec reuses. The two new files are default-adb-server.ts (protocol gate) and physical-handler.ts; the spec names their functions and the reason codes. Done means each row of the input table gives the stated result against the fake driver, and a blocked protocol runs no adb command.

Written by the indexing model from the issue text.

Description

task:draft

Part of #428.

Scope

After this PR, an operator can enroll a real Android phone, and agents can lease it, through the Android driver. The driver reaches phones through the host's default adb server (port 5037), always with -s <serial>. It inspects a phone for device add, reads which enrolled phones are present, reclaims a released phone by uninstalling user apps, and runs adb for device.exec on a leased phone. (The lease environment, ANDROID_SERIAL and ANDROID_ADB_SERVER_PORT=5037, is #438's.) Before every physical call it reads the default server's adb protocol version and sends nothing when that differs from Simlock's adb. doctor then names both versions. Tasks 1a–3 built the core side against the fake driver; this task is the real Android side of ADR 0022 §5–§8. The phone lane gets its Android test.

In short

Today, after tasks 1a–4, device add --platform android cannot enroll anything: the Android driver has no physical handler. After this PR, a phone plugged in by USB with USB debugging authorized can be enrolled, leased and reclaimed. Simlock never stops the host's adb server and never restarts one that speaks another protocol version.

New terms:

  • default adb server: the adb server on port 5037 that plain adb and Android Studio use, e.g. the one adb devices talks to on a Mac. Simlock's own server (port 5038, USB off) stays for emulators only.
  • USB transport: a line of adb devices -l that carries a usb: field, e.g. R58M123ABC device usb:1-1 product:o1s model:SM_G991B transport_id:3. A line without it (192.168.1.5:5555 device …) is a network transport. The transport decides; the serial's shape never does.
  • adb protocol version: the number an adb client and server must share, or the client kills the server and starts its own. adb version prints it last (Android Debug Bridge version 1.0.41 is 41). The server answers it to host:version over its socket, which restarts nothing.
  • physical handler: the part of the Android driver that serves physical devices. The driver routes each call to it when the device is physical: true.
flowchart LR
  A["device add / grant / watcher / release / device.exec"] --> D["AndroidDriver"]
  D -->|"physical: false"| E["emulator side<br/>Simlock's server, unchanged"]
  D -->|"physical: true"| P["physical handler (new)"]
  P --> V{"host:version on 5037<br/>same protocol? (new)"}
  V -->|yes| C["adb -P 5037 -s &lt;serial&gt; …"]
  V -->|"no / unreadable"| N["no command: not present, refused;<br/>doctor names both versions"]
# before (tasks 1a-4 landed)
$ simlock device add --platform android R58M123ABC
Refused: this driver cannot read physical devices yet.   (#437's placeholder, reason not-connected)

# after
$ simlock device add --platform android R58M123ABC
Enrolled SM-G991B (API 34), 12 user apps recorded.

$ simlock doctor        # Android Studio's older adb runs the default server
driver-advisory android adb-protocol-mismatch: the adb server on port 5037 speaks protocol 40 and Simlock's adb speaks 41. …
Input Before (after task 4) After
device add --platform android R58M123ABC, phone on USB, USB debugging authorized refused enrolled under R58M123ABC: ro.product.model, API level, user apps of the current user
same, USB debugging not authorized (unauthorized) refused refused: USB debugging not authorized (untrusted)
device add --platform android 192.168.1.5:5555 (network transport only) refused refused: only USB-connected devices can be enrolled (network-only)
device add --platform android emulator-5554 (a developer's own emulator on the default server) refused refused: only USB-connected devices can be enrolled (network-only)
device add --platform android R58M123ABC, not listed, or listed offline refused refused: not connected (not-connected)
any device add --platform android … while the default server speaks another protocol refused refused: not connected (not-connected), the message naming both protocol versions; no adb command runs
lease --platform android --physical, the enrolled phone free and present no phone enrollable granted; environment ANDROID_SERIAL=R58M123ABC, ANDROID_ADB_SERVER_PORT=5037 (#438)
an enrolled phone unauthorized, or unplugged, for the grace period stays ready (no real presence read) absent (untrusted), absent (not-present)
an enrolled phone with its screen locked — present: adb shows no lock state, so it stays in rotation
release of that lease — each user app not on the enrollment list uninstalled for the current user; listed apps found missing reported
device.exec adb shell getprop on that lease — runs adb -P 5037 -s R58M123ABC shell getprop
device.exec adb kill-server on that lease — refused
the default server switches to another protocol while the phone is leased — no command sent; the phone reads not present, so the lease ends device-lost after the grace period (task 3)
simlock adb devices, lease --platform android Simlock's server, emulators unchanged

If this goes wrong, an operator would see an authorized phone refused, apps left on a released phone, Android Studio's adb server restarted, emulator leases stalling while a phone hangs, or a command run on a phone nobody enrolled.

Technical spec

Modules touched
flowchart LR
  CORE["core callers: enrollment, grant, reclaim,<br/>watcher, device.exec (tasks 1b-4)"] --> AD["drivers/android/index.ts<br/>AndroidDriver *"]
  DOC["core/doctor: advisories"] --> AD
  AD --> EM["emulator side: AdbServerSupervisor,<br/>AdbRegistrar (unchanged)"]
  AD --> PH["drivers/android/physical-handler.ts (new)"]
  PH --> PG["drivers/android/default-adb-server.ts (new)<br/>protocol gate"]
  PH --> PR["ports: ProcessRunner"]
  PG --> TCP["ports: TcpProbe"]
  PG --> PR
  EM --> PR
  EM --> TCP

One device add through the hops, with the failure reply:

sequenceDiagram
  participant C as CLI
  participant D as Daemon (enrollment, task 1b)
  participant P as Android physical handler
  participant S as Default adb server :5037
  participant R as Phone on USB
  C->>D: device add --platform android R58M123ABC
  D->>P: inspectPhysical("R58M123ABC")
  P->>S: host:version (socket)
  S-->>P: OKAY 0004 0029 (protocol 41)
  alt same protocol as Simlock's adb
    P->>S: adb -P 5037 devices -l
    S-->>P: R58M123ABC device usb:1-1 …
    P->>R: -s R58M123ABC: getprop, am get-current-user, pm list packages -3
    R-->>P: model, API level, user id, apps
    P-->>D: inspection
    D-->>C: Enrolled SM-G991B (API 34), 12 user apps recorded
  else other protocol, unreadable reply, or no USB transport in state device
    P-->>D: refusal with its reason; no command named the serial
    D-->>C: Refused: <reason>
  end
  • src/drivers/android/default-adb-server.ts (new): the protocol gate. It is the one place that decides whether a physical command may run (architecture rule 10).
    • Simlock's protocol: the last number of adb version's first line (1.0.41 → 41). Read with Simlock's adb, 30 s limit. Cached for the driver's life once read; a failed read is not cached.
    • The server's protocol: TcpProbe.isListening(5037), then TcpProbe.send(5037, "000chost:version") with a 5 s limit (the IDENTITY_TIMEOUT_MS precedent in adb-server.ts:16). A reply OKAY + 0004 + four hex digits is the version. Anything else is unreadable.
    • Verdict: open when nothing listens on 5037 (Simlock's adb then starts a server of its own version, as any adb does) or both numbers match. blocked when they differ or either is unreadable. Fail closed (safety rule 9).
    • Every physical call reads the verdict anew, so a fixed server is used again on the next call, with no restart. The latest verdict is kept for the synchronous passthrough, which uses the most recent check.
    • Never sends kill, never runs kill-server or start-server, never signals a process.
  • src/drivers/android/physical-handler.ts (new): every physical call. Each runs adb -P 5037, with the injected base environment plus ANDROID_ADB_SERVER_PORT=5037, and without ANDROID_SERIAL or ADB_SERVER_SOCKET from that base. It never uses AndroidDriver#env (index.ts:713), which points at Simlock's server. Limits: 30 s per read, 60 s per uninstall (ADR 0022 §5; architecture rule 11). A read past its limit, or with no exit code, is a failed read. Physical calls take no per-device lock and no port allocation, so they never wait behind, or block, an emulator call.
    • inspectPhysical(id): gate; one adb devices -l. The line whose serial equals id decides:
      • USB transport in state device: inspected. Canonical ID is that serial. Then, with -s <serial>: shell getprop ro.product.model (model), shell getprop ro.build.version.sdk (OS version, the API level), shell am get-current-user (user id), shell pm list packages -3 --user <id> (enrollment app list). Class is always phone (ADR 0022 §6: every Android device is phone).
      • USB transport in state unauthorized: refused untrusted, USB debugging not authorized.
      • Only network transports, or an emulator line (emulator-5554 device …, no usb: field): refused network-only, "only USB-connected devices can be enrolled".
      • Not listed, another state (offline, no permissions, …), or a failed adb devices -l: refused not-connected.
      • Gate blocked: refused not-connected, with a message naming both protocol versions (or "unreadable" for one it could not read). No adb command runs.
      • A failed read after the device was found (getprop, user id, package list): inspectPhysical rejects with the read's error; task 1b's path for a failed inspection answers INTERNAL and writes nothing.
      • The reason codes are #437's DEVICE_NOT_ENROLLABLE reasons.
      • No command names the serial until its line is a USB transport in state device.
    • listPresentPhysical(enrolledIds): gate; one adb devices -l. One entry per given serial, none for any other (safety rule 8 as amended): a serial that is not enrolled gets no entry and no command. Present: on a USB transport in state device, with its OS version from getprop ro.build.version.sdk on that serial (#439's osVersion). Not present untrusted: state unauthorized. Not present not-present: missing, offline, another state, or only on a network transport. A locked phone is present: adb shows no lock state (ADR 0022 §7). A failed or timed-out listing, a failed getprop on a serial, or a blocked gate answers the serials concerned not present not-present and does not reject (ADR 0022 §5: a read past its limit is "not present").
    • reclaim of a physical device: gate (blocked rejects, no command); read the user id and the user's pm list packages -3; for each package not on enrolledApps, shell pm uninstall --user <id> <package>, one at a time, stopping at the first failure; then list again. The packages come from #438's uninstallPlan. Result: strategy uninstall, missingApps = enrolled packages not on the first list. It rejects when an uninstall exits non-zero, prints anything but Success, or passes 60 s; or when the second list still holds a package off the enrollment list. The core then reads presence and picks absent or quarantined (task 3).
    • Package lines are a claim (safety rule 10): a line must be package: followed by letters, digits, _ and . only. Any other line fails the read, so no unchecked text reaches adb shell.
    • passthrough("adb", <physical device>, args, context) (the signature task 4 left, ADR 0022 §5): adb -P 5037 -s <serial> <args>, env ANDROID_ADB_SERVER_PORT=5037. Refused with PassthroughRefusedError:
      • when the most recent gate verdict is blocked, or no physical call has read one yet in this daemon ("try again");
      • any caller argument before the subcommand (-s, -t, -d, -e, -P, -H, -L, --one-device, any other): Simlock supplies the scope, so the command stays aimed at the leased phone;
      • kill-server, start-server, connect, disconnect, pair, reconnect, tcpip, usb, anywhere in the arguments: each stops, restarts or moves a server or transport other holders share, or takes the phone off USB for the next holder;
      • bare shell with no terminal, as for emulators.
    • estimate({operation: "reclaim"}, <physical spec>): 30 000 ms, both clean levels (one listing, a few uninstalls, one listing). Only doctor's stall rule reads it for an unclaimed reclaim (stallThresholdMs, core/doctor.ts:973).
  • src/drivers/android/index.ts: AndroidDriver routes on physical to the handler for inspectPhysical, listPresentPhysical, reclaim, passthrough and estimate. leaseEnvironment keeps #438's physical answer. Every emulator path is unchanged. advisories() (new on this driver) runs the gate and returns adb-protocol-mismatch when it is blocked with a server present, whether or not any Android device is enrolled. The message names both protocol numbers, or "unreadable" for one it could not read, says no command goes to physical Android devices until they match, and says Simlock will not restart that server. dispose() is unchanged: it stops Simlock's own server and nothing on 5037.
  • e2e/slow-android-physical.test.ts (new): the phone lane for Android, tag physical (task 4), skipped unless SIMLOCK_PHYSICAL_ANDROID names the phone's USB serial and SIMLOCK_PHYSICAL_ANDROID_APK names an APK to install. The skip message names the missing variable. Each person step (unplug, plug back) is a prompt in the lane's log, and the lane waits up to 120 s for it.
  • docs/internal/agent-rules/toolchain.md: in the slow-lane table task 4 extended, a row for the Android phone: command -v adb finds it, adb devices -l lists $SIMLOCK_PHYSICAL_ANDROID on a USB transport in state device, SIMLOCK_PHYSICAL_ANDROID_APK names an APK, and the command is scripts/slow-e2e.sh --physical e2e/slow-android-physical.test.ts.
  • docs/CLI.md:
    • Prerequisites gain "Android physical devices": connected by USB, USB debugging authorized for this host. Simlock reaches them through the host's default adb server (port 5037), which it starts if none runs and never stops. Simlock cannot see an Android lock screen: a locked phone stays in rotation.
    • The doctor section gains the adb-protocol-mismatch advisory: what it means and that the fix is running that server with an adb of the same protocol.
  • docs/internal/KNOWN-PITFALLS.md: an entry. A default server of another protocol started between Simlock's version read and the command it guards is restarted by Simlock's adb client. adb offers no way to stop that. The window is one call long.
Contract and event changes

None. Searched src/contract/ and src/bus/index.ts: the driver methods, PresentPhysical with osVersion and its reasons, ReclaimResult.missingApps, strategy uninstall, absentReason and the events were added by tasks 1b–3. The driver-advisory doctor finding exists (core/doctor.ts:88); its code is the driver's own text. No protocol bump.

Other code on the same state

The state: the default adb server on 5037, the enrolled phones on it, and the gate's verdict. Line numbers are on main at bfeb5ca; tasks 1a–4 move them.

  • AdbServerSupervisor (src/drivers/android/adb-server.ts:104) owns Simlock's server. It refuses 5037 as its port (adb-server.ts:167), and starts its server with ADB_USB=0 (:43-49), so phones never appear there. The handler never uses Simlock's port. No overlap.
  • Every emulator call goes through AndroidDriver#env (index.ts:713): listManaged (:1015) and #scanAdbSerials (:1067, matches only emulator-\d+), makeReady, emulator reclaim (:932), passthrough with no device (:650), leaseEnvironment for an emulator (:636). None reaches 5037; the handler reaches nothing else. Unchanged.
  • #withDeviceLock (index.ts:2041) and PortAllocator (index.ts:2068) serialise emulator work. Physical calls take neither, so a hung phone call never delays an emulator call, and the reverse.
  • AdbRegistrar (src/drivers/android/adb-registrar.ts:25) sends host:emulator to Simlock's port only. The gate sends host:version to 5037 only. Different servers.
  • Doctor#collectAdvisories (src/core/doctor.ts:492) calls advisories(); a rejecting call contributes nothing. So advisories() returns the mismatch as a finding and never rejects for it.
  • Core callers of the physical methods: enrollment (task 1b), grant presence check and reclaim (task 2), the physical-device watcher (task 3), device.exec routing (src/core/driver-catalog.ts:74, src/daemon/dispatcher.ts:539, as task 4 left them). The core's operation claim orders enrollment, reclaim and quarantine retries; the driver keeps no per-device state. The watcher's presence read can run while a reclaim uninstalls. Both are calls on one adb server; the read changes nothing on the phone. The core's claim decides the state, so the reclaim's outcome wins.
  • Other adb clients on the host (Android Studio, an agent's plain adb, a second Simlock instance) share 5037 and see the phones. One of another protocol restarts the server, as adb does; Simlock's next gate read sees it and sends nothing. Simlock never restarts it, except in the one-call window in the KNOWN-PITFALLS entry. An agent's own adb on its leased phone is the agent's; Simlock uninstalls only after release.
  • A device.exec still running when the watcher marks the phone absent runs until it ends or exec.timeoutMs stops it, as for emulators today (KNOWN-PITFALLS "A device.exec command is authorized once, at its start").
Rules in play
  • safety.md rule 1 as amended: the only destructive act is pm uninstall of a user app off the enrollment list, for the current user. No reboot, erase, or command on a phone that is not enrolled, apart from the admin's own device add inspection.
  • safety.md rules 7, 8 as amended: presence reads only enrolled serials; a non-enrolled serial gets no entry and no command.
  • safety.md rule 9 as amended: the default server is never stopped and never restarted by Simlock; the gate fails closed on an unreadable version.
  • safety.md rule 10: adb devices -l lines and package lines are claims; package names are checked before they reach adb shell.
  • architecture.md rules 2, 10, 11: adb knowledge stays in src/drivers/android/; the gate is the one place that allows a physical command; every physical wait is bounded.
  • testing.md rules 1–4: the "never" lines below assert over every physical method's calls, not one.
Tests

Seam: AndroidDriver.create with ScriptedProcessRunner, FakeTcpProbe and MemoryFilesystem, modelled on src/drivers/android/index.test.ts. Phone lane: withDaemon({ driver: "real" }) and the HTTP device.exec route, modelled on e2e/slow-android-smoke.test.ts and e2e/device-exec.test.ts.

  • every physical method reads host:version on port 5037 before its first adb command
  • no physical method, and no passthrough result, ever runs kill-server or start-server against 5037, and dispose() sends nothing to 5037
  • with a default server of another protocol: listPresentPhysical answers every given serial not present not-present, inspectPhysical is refused not-connected with a message naming both protocol numbers, reclaim rejects, and none of them starts a process after adb version
  • a host:version reply that is not OKAY0004 plus four hex digits, and an adb version with no protocol number, each block physical commands like a mismatch
  • with nothing listening on 5037, physical commands run
  • a server that matched again after a mismatch gets commands on the next call, and nothing restarts it
  • advisories() returns adb-protocol-mismatch naming both protocol numbers on a mismatch, "unreadable" for one it could not read, and nothing on a match or with no server on 5037
  • advisories() reports the mismatch on a driver that has never been asked about a physical device (no Android device enrolled)
  • every physical command runs adb -P 5037 with ANDROID_ADB_SERVER_PORT=5037 and no ANDROID_SERIAL from the base environment; every device command carries -s <serial>
  • inspectPhysical of a serial on a USB transport in state device returns that serial, ro.product.model, the API level, class phone, and the packages pm list packages -3 --user <id> lists for the id am get-current-user prints
  • inspectPhysical of a serial in state unauthorized is refused untrusted, and runs no command naming it
  • inspectPhysical of a serial only on a network transport (192.168.1.5:5555, an mDNS _adb-tls-connect._tcp name) is refused network-only with "only USB-connected devices can be enrolled", and runs no command naming it
  • inspectPhysical of emulator-5554 listed on the default server is refused network-only with "only USB-connected devices can be enrolled", and runs no command naming it
  • the transport decides, not the shape: a serial shaped like host:port on a USB transport is inspected, and a USB-shaped serial on a network line is refused
  • inspectPhysical of a serial not listed, listed offline, or with a failed adb devices -l is refused not-connected
  • inspectPhysical rejects when a read after the device was found fails
  • listPresentPhysical answers a given serial on USB in state device present with its API level, an unauthorized one not present untrusted, and an offline, network-only or missing one not present not-present
  • listPresentPhysical gives no entry, and runs no command, for a USB phone that is not among the given serials
  • listPresentPhysical answers every given serial not present not-present, and does not reject, when adb devices -l fails or has no exit code
  • listPresentPhysical answers a present serial whose getprop fails not present not-present
  • reclaim uninstalls, for the current user, each package off the enrollment list, uninstalls no listed package, reports listed packages not installed as missingApps, and returns strategy uninstall
  • reclaim with nothing off the list runs no uninstall
  • reclaim rejects, and runs no later uninstall, when an uninstall prints Failure […], exits non-zero, or has no exit code
  • reclaim rejects when the second listing still holds a package off the enrollment list
  • a package line with a character outside letters, digits, _ and . fails the listing, and no uninstall runs
  • reads pass a 30 000 ms limit and uninstalls a 60 000 ms limit to the process runner
  • while a physical adb devices -l hangs, listManaged for emulators resolves
  • passthrough("adb", <physical device>, ["shell", "getprop"]) returns adb -P 5037 -s <serial> shell getprop with ANDROID_ADB_SERVER_PORT=5037
  • passthrough on a physical device refuses a caller argument before the subcommand (-s, -t, -d, -e, -P, -H, -L, --one-device, --reply-fd), and kill-server, start-server, connect, disconnect, pair, reconnect, tcpip, usb, and bare shell with no terminal
  • passthrough on a physical device is refused when the latest gate verdict is blocked, and before any gate read
  • passthrough with no device still runs on Simlock's own server (today's index.test.ts passthrough tests, unchanged and green)
  • estimate of a reclaim for a physical spec is 30 000 ms at both clean levels; for an emulator spec it is unchanged
  • (phone lane) simlock device add --platform android $SIMLOCK_PHYSICAL_ANDROID enrolls it with the model and API level adb shell getprop reports and as many apps as pm list packages -3 lists for the current user
  • (phone lane) simlock lease --platform android --physical grants that serial with ANDROID_SERIAL set to it and ANDROID_ADB_SERVER_PORT=5037, and device.exec adb shell getprop ro.serialno over HTTP prints the serial
  • (phone lane) after installing $SIMLOCK_PHYSICAL_ANDROID_APK with plain adb -s and releasing, the device is ready, the APK's package is gone, and every enrolled package is still installed
  • (phone lane) with a short health.probeIntervalMs, the phone unplugged when the test asks in its log becomes absent with absentReason not-present within 120 s, and plugged back becomes ready
  • (phone lane) without SIMLOCK_PHYSICAL_ANDROID or SIMLOCK_PHYSICAL_ANDROID_APK the file skips, naming the missing variable

Done when

  • With a default adb server of another protocol version, the Android driver runs no command on a physical device, device add is refused not-connected naming both versions, and advisories() names both versions even with no Android device enrolled (driver tests above).
  • No physical call stops or starts the default server, and dispose() leaves it (driver tests above).
  • A hung phone call does not delay an emulator call (driver test above).
  • On a Mac with an Android SDK, one Android phone on USB with USB debugging authorized, SIMLOCK_PHYSICAL_ANDROID set to its serial and SIMLOCK_PHYSICAL_ANDROID_APK to an APK: device add enrolls it, a --physical lease gets it with ANDROID_SERIAL, device.exec reaches it, and after release the installed app is gone and every enrolled app remains. Proved by e2e/slow-android-physical.test.ts, run through scripts/slow-e2e.sh as task 4's iOS phone lane runs.
  • On the same machine, the phone unplugged on the test's prompt becomes absent (not-present), and plugged back becomes ready (same test).
  • Without the two variables, e2e/slow-android-physical.test.ts skips and names the missing one.
  • docs/CLI.md lists the Android physical-device prerequisites and the adb-protocol-mismatch advisory; toolchain.md has the Android phone row.

Out of scope

  • Routing physical requests and device.exec through a gateway (task 6).
  • Wi-Fi debugging, work profiles and other Android users.
  • Restarting or fixing a default server of another protocol version.
  • Guarding against another adb client restarting the default server.
  • The iOS physical handler (task 4).

Depends on

  • #435
  • #440

Approval

  • Approved for delivery

Written by an agent.

Dominant language
TypeScript
Stars
19
Forks
1
Avg merge
9h 56m
Merged PRs (30d)
134

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from callstackincubator/simlock

All issues in callstackincubator/simlock

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.