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 的文档)都属于这一类。
列出在线设备:discover()
按 wlan0 MAC 去重,一台物理机器算一台。必须去重是因为 MuMu 会同时占两个 adb 名字
(自注册的 emulator-555X + ensure_connected() connect 出来的 127.0.0.1:<port>):
from androtools.android_sdk.platform_tools import ADB
from androtools.core import Device, DeviceInfo, LDConsole, MumuConsole, discover
adb = ADB(r"D:\ProgramFiles\LDPlayer9.0.79.2\adb.exe")
entries = discover(adb, [("雷电", LDConsole(...)), ("MuMu", MumuConsole(...))])
# adb 上 4 个条目 -> 3 台
# PFFM10 bdfbafac 别名=['bdfbafac']
# SM-A5560 127.0.0.1:16416 别名=['127.0.0.1:16416', 'emulator-5556']
# GM1910 emulator-5554 别名=['emulator-5554'] 雷电 index 0
for e in entries:
device = Device(DeviceInfo(e.name, e.serial, adb.bin_path)) # serial 用 primary
每台会读一次机型(ro.product.model)当展示名,读不到就回退到 serial;认得出厂商和
实例就填 identity,认不出是 None(真机、或 MuMu 这类拿不到指纹的)。两个别名的
机器合并成一条,但 serials 里两个名字都留着,你拿哪个来都能用。
抓包
按应用抓 —— 用应用的 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")
日常操作:清单 → 会话 → 配置
上面的例子都是「我已经知道要操作哪一台」。日常用起来更常见的是反过来:先问现在有 几台、哪台在跑,挑一台动手,再改它的配置。下面每一节的输出都是在本机跑出来的。
1. 列出实例
list_instances() 一次调用问出全部实例,包括没启动的,而且不依赖 adb:
from androtools.core import MumuConsole
console = MumuConsole(r"D:\Program Files\Netease\MuMu\nx_main\MuMuManager.exe")
for i in console.list_instances():
print(f"{i.index} {i.name} {i.status.name} {i.serial or '-'} android {i.android_version}")
0 A15 STOP - android 15
1 A12 BOOT 127.0.0.1:16416 android 12
status 只区分 STOP(进程根本没起来)和 BOOT(起来了)—— 「能不能真的 adb 交互」
是 session 的问题。serial 只有运行中的实例才有:MuMu 的 adb 端口是启动时才分配的,
不要自己按 index 算(见上面「多开模拟器时 serial 会被认错」)。
拿到的是 EmulatorInstance —— 某一刻的快照,要新状态就再列一次。它的相等性不看
serial,所以重启一次不会让同一台机器变成两台。
2. 从清单到可操作的 session
play() 是清单的下一站,调用方不必知道厂商的 session 类叫什么:
instance = console.list_instances()[1]
player = console.play(instance) # MumuPlayer;只构造,不发任何命令
print(player.ensure_ready()) # True —— 启动并等到开机完成
player.shell.tap(100, 200)
play() 本身不做 IO:serial 的确定、adb connect 都属于 session 的启动流程
(launch() / ensure_connected())。已经在跑的实例上 ensure_ready() 只是确认它
就绪。
3. 改配置
常用项有中立动词,不用记厂商方言:
idx = instance.index
console.set_resolution(idx, 540, 960, 240)
console.set_cpu(idx, 2)
console.set_root(idx, True)
console.get_settings(idx)["resolution_width"] # '540.000000'
内存的单位统一是 MB 整数(MuMu 内部用 GB、雷电用 MB,adapter 自己换算),CPU 是
核数。越界值抛 ValueError 且不下发命令 —— 两家在这件事上都会静默骗你:实测
MuMu 把超范围的宽高夹到边界,雷电把不接受的宽高整个丢掉,两次都是成功退出码。
写下去的值在实例下次启动时生效,本库不替你重启。 实测两家都能在实例运行中写,
所以故意不做「停机 → 写 → 恢复运行」的编排:为了改个配置白关一次别人的模拟器
(重启 30s+、丢掉正在跑的东西)是比「下次启动才生效」更坏的意外。想立刻看到效果就
自己 reboot_device(idx)。
4. 改「对外自称的设备信息」
模拟(Simulation)的那五个字段(android_id / imei / mac / model / brand):
console.set_simulation(idx, "imei", "861234567890123")
console.get_simulation(idx)["imei"] # '861234567890123'
key 不是那五个之一就抛 ValueError —— 两家对任何 key 名都不报错,不拦就是静默
成功。值原样透传:厂商不校验,本库也不替它猜(厂商自己那套 "auto" 写法照传)。
5. 逃生舱
中立动词没包住的长尾(sort、control tool func、将来新增的子命令)直接透传:
result = console.run("version")
print(result.output.strip())
{
"version": "6.8.2.0"
}
run() 不做任何解析(要 JSON 自己 parse),也不看返回码 —— 非零不会抛,自己
看 result.has_error() / result.exit_code。输出按 adapter 声明的厂商编码解(MuMu
是 UTF-8、雷电是 GBK),与本机 locale 无关。
雷电的 LDConsole 有同一套方法。两个厂商的差异收在 adapter 后面:雷电读设置是直接
解析它自己的实例配置文件(它没有读的 CLI),而 MuMu 是一条命令拿到整份 JSON。
设计
Device 真机。serial、状态判定、超时探测、应用操作
└─ EmulatorSession 加上厂商 Console 和 launch / close / ensure_ready / recover
└─ ConsoleSession 雷电、MuMu
AndroidShell adb 命令字典(shallow:省的是打字,不是藏复杂度)
EmulatorConsole 厂商 CLI 的方言止步于此。四个面:
· 会话必需 probe_state / getprop / run_app / kill_app / …
· 实例编目 list_instances / create / clone / delete / rename
· 配置读写 get_settings / set_settings / set_resolution /
set_cpu / set_memory / set_root /
get_simulation / set_simulation
· 通路 play() 造 session,run() 透传长尾
两个 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 —— 与 pyproject.toml 里 classifiers 声明的范围是同一段。
术语
领域词汇见 CONTEXT.md —— 尤其「Device 与 Emulator 差在哪」「Serial 为什么是运行期才确定的」这两条。
破坏性变更
1.x → 2.0 的迁移说明见 CHANGELOG.md。
Metadata
Release files for androtools 2.2.1
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.2.1.tar.gz | 58.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| androtools-2.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 127.9 kB
Release files / androtools-2.2.1.tar.gz
| Download URL | androtools-2.2.1.tar.gz |
|---|---|
| Size | 58.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ec6a577edc3473e256e794e96f614883b5ba3f3ab3c9d8be5426216ccf6d9355
|
|
BLAKE2b-256 checksum How to use checksums |
64071c5223cdbabde3de89cd20eb460f41c72d1ad63dc514aa55a58f7e91bd9e
|
| 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.2.1-py3-none-any.whl
| Download URL | androtools-2.2.1-py3-none-any.whl |
|---|---|
| Size | 69.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4d8044141d607d77c6f476842e2f9999c7cfe4185e7a66ab971aedc84445927c
|
|
BLAKE2b-256 checksum How to use checksums |
5c3742e8636e72f9f080331b1f68ac12c89aba9661c553ce03957fa0ff11f31d
|
| 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.2.1
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.