androtools
对 Android SDK、第三方模拟器命令行工具、以及通过 adb 连接的 Android 设备的封装。
核心价值是把「判断状态、等它就绪、卡死了怎么办」这段最容易出错、也最难手工验证的流程收进一处。 支持真机、雷电模拟器、MuMu 模拟器。
安装
pip install androtools
快速开始
真机
真机不需要任何子类 —— 它就是一个 adb 可寻址的目标。
from androtools.android_sdk.platform_tools import ADB
from androtools.core import Device, DeviceInfo
adb = ADB()
serial, status, _ = adb.get_devices()[0]
device = Device(DeviceInfo(name="我的手机", serial=serial, adb_path=adb.bin_path))
print(device.get_status()) # DeviceStatus.BOOT_COMPLETED
print(device.android_version) # 问设备本身要,不是查本地表
print(device.read_prop("ro.product.model"))
device.shell.list_packages() # 所有应用
device.shell.list_packages(flag=0) # 只看第三方应用
device.shell.pull("/sdcard/DCIM", r"D:\backup")
同时插着多台设备时,serial 必须指定 —— 否则 AndroidShell 会抛
AmbiguousDeviceError,而不是让 adb 乱挑。
模拟器
模拟器比真机多两样东西:厂商 Console,以及「启动 / 关闭」。
from androtools.core import EmulatorInfo, LDPlayer
player = LDPlayer(
EmulatorInfo(
name="雷电",
serial=None, # 库会按 MAC 指纹认出来,见下
adb_path=r"D:\ProgramFiles\LDPlayer9.0.79.2\adb.exe",
index="0", # 厂商 Console 里的编号
console_path=r"D:\ProgramFiles\LDPlayer9.0.79.2\ldconsole.exe",
)
)
player.launch() # 启动模拟器并等它就绪
print(player.get_status())
player.shell.tap(100, 200)
player.run_app("com.android.settings")
player.close()
LDPlayer 会自己按 console_path 找 ldconsole.exe,MuMu 同理:
from androtools.core import MumuPlayer
mumu = MumuPlayer(EmulatorInfo(
name="MuMu",
serial=None,
adb_path=r"D:\Program Files\Netease\MuMu\nx_main\adb.exe",
index="1",
console_path=r"D:\Program Files\Netease\MuMu\nx_main\MuMuManager.exe",
))
mumu.launch() # 启动 → 反查 serial → adb connect
MuMu 启动后 adb 端口会变,launch() 会从 Console 查出新的 serial 并 adb connect。
adb connect 不只在 launch() 里做 —— 从 MuMu 启动器外部启动、没经过 launch()
的实例,127.0.0.1:<adb_port> 默认并不在 adb devices 里,库会在用之前补上连接。
多开模拟器时 serial 会被认错
雷电和 MuMu 的 serial 都落在 emulator-5554 这一段号段里,两家会撞。实测两台雷电
- 两台 MuMu 同时开着时:
| serial | 实际是谁 |
|---|---|
emulator-5554 |
雷电 index 0 |
emulator-5556 |
MuMu index 1 —— 正好是雷电 index 1 该去的位置 |
127.0.0.1:16384 / 127.0.0.1:16416 |
MuMu(tcp 地址,MuMu 的权威身份) |
| —— | 雷电 index 1 在 adb 里根本不出现 |
推出来的 serial 不会「连不上」,它会合法地连到别人的模拟器上 —— 连雷电官方的
ldconsole adb --index 1 都会安静地返回 MuMu 那台设备的属性(实测返回 SM-A5560)。
把 MuMu 关掉再跑同样的两台雷电,emulator-5556 就变回雷电 index 1(V1938T)。同一段
代码、同一个 serial,换个环境就换了个主人 —— 这就是为什么不能按 index 算端口。
所以 LDPlayer 在 serial=None 时不按 index 算端口,而是用每实例唯一的 MAC 指纹
(雷电实例配置里的 propertySettings.macAddress,guest 的 wlan0 上是同一个值)在 adb
上认自己的那一台,核不上就报错,绝不猜。多开雷电和 MuMu 混用时,如果某个实例的
serial 被对方占了,你会看到明确的报错而不是错误设备的数据。
想手动指定也完全可以,两个厂商都一样:
EmulatorInfo(..., serial="emulator-5554", ...) # 显式指定,跳过反查
反查:adb 上这个 serial 到底是谁的
撞号之后最需要问的一句话。identify() 拿指纹核对,不问厂商工具 —— 因为实测
ldconsole adb --index 1 在那个环境下会返回 MuMu 那台设备且不报错。
from androtools.android_sdk.platform_tools import ADB
from androtools.core import LDConsole, MumuConsole, identify
adb = ADB(r"D:\ProgramFiles\LDPlayer9.0.79.2\adb.exe")
consoles = [
("雷电", LDConsole(r"D:\ProgramFiles\LDPlayer9.0.79.2\ldconsole.exe")),
("MuMu", MumuConsole(r"D:\Program Files\Netease\MuMu\nx_main\MuMuManager.exe")),
]
identify(adb, consoles, "emulator-5556")
# InstanceIdentity(vendor='雷电', index='1', serial='emulator-5556')
identify(adb, consoles, "A87V026107005402") # 真机
# None
None 是正常结果,不是错误 —— 真机、以及指纹读不出来的厂商(目前是 MuMu,
原因见 MumuConsole.fingerprint 的文档)都属于这一类。
抓包
按应用抓 —— 用应用的 user id 做 iptables 标记。
from androtools.utils import Tcpdump
tcpdump = Tcpdump(device.shell, "com.example.app")
tcpdump.start_capture()
...
tcpdump.stop_capture()
tcpdump.pull_pcap_file(r"D:\capture")
设计
Device 真机。serial、状态判定、超时探测、应用操作
└─ EmulatorSession 加上厂商 Console 和 launch / close / ensure_ready / recover
└─ ConsoleSession 雷电、MuMu
AndroidShell adb 命令字典(shallow:省的是打字,不是藏复杂度)
EmulatorConsole 厂商 CLI 的方言止步于此(probe_state / getprop / run_app / …)
两个 seam 都可以替换,所以生命周期逻辑不需要真机就能测:
from androtools.testing import FakeADB
adb = FakeADB(responses={("get-state",): CmdResult("device", "")}, devices=[...])
device = Device(info, adb=adb, sleeper=lambda _s: None)
FakeADB 未声明的命令会直接抛错,所以「代码多调了一次 adb」会变成红灯而不是悄悄通过。
开发
make sync # uv sync
make test # 单元测试(默认排除 integration)
make typecheck # mypy
make test-integration # 需要真实 Android SDK 和设备
CI 在 .github/workflows/ci.yml,跑 Python 3.10(声明的最低版本)和 3.13。
术语
领域词汇见 CONTEXT.md —— 尤其「Device 与 Emulator 差在哪」「Serial 为什么是运行期才确定的」这两条。
破坏性变更
1.x → 2.0 的迁移说明见 CHANGELOG.md。
Metadata
Release files for androtools 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| androtools-2.1.0.tar.gz | 32.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| androtools-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.2 kB
Release files / androtools-2.1.0.tar.gz
| Download URL | androtools-2.1.0.tar.gz |
|---|---|
| Size | 32.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6713591bf8d07586f13b55b4d904e92b0e482f7883682cdb18f6744281aa9e8d
|
|
BLAKE2b-256 checksum How to use checksums |
6ef48f918f67e2b445d5713c9aabbcb260cd42cc624ef8bef61406bae148cfaf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.12
|
Release files / androtools-2.1.0-py3-none-any.whl
| Download URL | androtools-2.1.0-py3-none-any.whl |
|---|---|
| Size | 42.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fbda00092341793223f5141f6c8909c4d90bc389665ba7bea72cbca6126c8be3
|
|
BLAKE2b-256 checksum How to use checksums |
0641711a32a7fd6544d2019f409cb2903493d73c82879e682249ad824babb0c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.12
|
Security for androtools 2.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.