mobilerun-core-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobilerun_core_cli-0.1.0/.github/workflows/publish.yml +90 -0
- mobilerun_core_cli-0.1.0/.gitignore +27 -0
- mobilerun_core_cli-0.1.0/LICENSE +21 -0
- mobilerun_core_cli-0.1.0/PKG-INFO +154 -0
- mobilerun_core_cli-0.1.0/README.md +126 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/__init__.py +34 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/driver/__init__.py +4 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/driver/android.py +194 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/driver/base.py +140 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/portal.py +798 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/transport/__init__.py +3 -0
- mobilerun_core_cli-0.1.0/mobilerun_core_cli/transport/portal_client.py +744 -0
- mobilerun_core_cli-0.1.0/pyproject.toml +39 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
target:
|
|
10
|
+
description: "Where to publish"
|
|
11
|
+
required: true
|
|
12
|
+
default: "pypi"
|
|
13
|
+
type: choice
|
|
14
|
+
options: [pypi, testpypi]
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
name: Build distribution
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v3
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.12
|
|
28
|
+
|
|
29
|
+
- name: Verify tag matches pyproject version
|
|
30
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
31
|
+
run: |
|
|
32
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
33
|
+
VER=$(uv run --with tomli python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
34
|
+
if [ "$TAG" != "$VER" ]; then
|
|
35
|
+
echo "::error::Tag v$TAG does not match pyproject version $VER"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
echo "tag and pyproject agree on $VER"
|
|
39
|
+
|
|
40
|
+
- name: Build sdist + wheel
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Show built artifacts
|
|
44
|
+
run: ls -la dist/
|
|
45
|
+
|
|
46
|
+
- uses: actions/upload-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
if-no-files-found: error
|
|
51
|
+
|
|
52
|
+
publish-pypi:
|
|
53
|
+
name: Publish to PyPI
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
|
|
57
|
+
environment:
|
|
58
|
+
name: pypi
|
|
59
|
+
url: https://pypi.org/p/mobilerun-core-cli
|
|
60
|
+
permissions:
|
|
61
|
+
id-token: write # required for trusted publishing
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: dist
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish via Trusted Publishing
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
|
|
71
|
+
publish-testpypi:
|
|
72
|
+
name: Publish to TestPyPI
|
|
73
|
+
needs: build
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
|
|
76
|
+
environment:
|
|
77
|
+
name: testpypi
|
|
78
|
+
url: https://test.pypi.org/p/mobilerun-core-cli
|
|
79
|
+
permissions:
|
|
80
|
+
id-token: write
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/download-artifact@v4
|
|
83
|
+
with:
|
|
84
|
+
name: dist
|
|
85
|
+
path: dist/
|
|
86
|
+
|
|
87
|
+
- name: Publish to TestPyPI
|
|
88
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
89
|
+
with:
|
|
90
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Secrets — never commit
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.pyc
|
|
9
|
+
*.pyo
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Virtualenvs / build artifacts
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
.venv-test/
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
|
|
24
|
+
# Editor / OS noise
|
|
25
|
+
.DS_Store
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 droidrun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobilerun-core-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Slim async Portal client + Android device driver, extracted from mobilerun.
|
|
5
|
+
Project-URL: Homepage, https://github.com/droidrun/mobilerun-core-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/droidrun/mobilerun-core-cli
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/droidrun/mobilerun-core-cli/issues
|
|
8
|
+
Project-URL: Upstream, https://github.com/droidrun/mobilerun
|
|
9
|
+
Author: droidrun
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Hardware
|
|
22
|
+
Requires-Python: <3.14,>=3.11
|
|
23
|
+
Requires-Dist: async-adbutils
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: requests>=2.31
|
|
26
|
+
Requires-Dist: rich>=14.1.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<picture align="center">
|
|
30
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
|
|
31
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
|
|
33
|
+
</picture>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<strong>mobilerun-core-cli is the slim async Portal + Android device driver core of <a href="https://github.com/droidrun/mobilerun">mobilerun</a>.</strong><br>
|
|
37
|
+
No CLI, no agent, no LLM providers — just enough to drive a real Android device through the Mobilerun Portal app over ADB. Intended as an embedding library for higher-level tools.
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
|
|
42
|
+
<a href="https://docs.mobilerun.ai">📕 Documentation</a>
|
|
43
|
+
·
|
|
44
|
+
<a href="https://github.com/droidrun/mobilerun">🧠 mobilerun framework</a>
|
|
45
|
+
·
|
|
46
|
+
<a href="https://pypi.org/project/mobilerun-core-cli/">📦 PyPI</a>
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/mobilerun-core-cli/)
|
|
49
|
+
[](https://pypi.org/project/mobilerun-core-cli/)
|
|
50
|
+
[](./LICENSE)
|
|
51
|
+
[](https://discord.gg/ZZbKEZZkwK)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
- 📱 Drive a real Android device — `tap`, `swipe`, `type`, `screenshot`, `get_ui_tree`, `start_app`, `install_app`, …
|
|
58
|
+
- ⚡ TCP-with-content-provider fallback — fast HTTP path over `adb forward`, transparent fallback to content-provider RPC.
|
|
59
|
+
- 🛠 Portal lifecycle — download, install, accessibility enablement, auto-upgrade.
|
|
60
|
+
- 🪶 Slim — four files, four runtime deps (`async_adbutils`, `httpx`, `requests`, `rich`).
|
|
61
|
+
- 🔌 Embeddable — designed to be wrapped by sync facades (e.g. `mobilerun-core`) or used directly.
|
|
62
|
+
- 🤝 In sync with upstream — verbatim slice of [`droidrun/mobilerun`](https://github.com/droidrun/mobilerun); behaviour and API track upstream.
|
|
63
|
+
|
|
64
|
+
## 📦 Installation
|
|
65
|
+
|
|
66
|
+
> **Note:** Python `>=3.11,<3.14`. Requires [ADB](https://developer.android.com/studio/releases/platform-tools) on `PATH` and a device with USB debugging enabled.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv pip install mobilerun-core-cli
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 🚀 Quick usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from async_adbutils import adb
|
|
77
|
+
from mobilerun_core_cli import AndroidDriver, ensure_portal_ready
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def main():
|
|
81
|
+
# 1. Make sure Portal is installed + accessibility is on.
|
|
82
|
+
device = await adb.device()
|
|
83
|
+
await ensure_portal_ready(device)
|
|
84
|
+
|
|
85
|
+
# 2. Drive the device.
|
|
86
|
+
driver = AndroidDriver(serial=device.serial, use_tcp=True)
|
|
87
|
+
await driver.connect()
|
|
88
|
+
|
|
89
|
+
await driver.tap(540, 1200)
|
|
90
|
+
await driver.swipe(540, 1600, 540, 400, duration_ms=300)
|
|
91
|
+
await driver.input_text("hello", clear=True)
|
|
92
|
+
png_bytes = await driver.screenshot()
|
|
93
|
+
tree = await driver.get_ui_tree()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 🧱 Layout
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
mobilerun_core_cli/
|
|
103
|
+
├── __init__.py Re-exports the public surface
|
|
104
|
+
├── portal.py Portal APK lifecycle + content-provider helpers
|
|
105
|
+
├── driver/
|
|
106
|
+
│ ├── base.py DeviceDriver ABC, DeviceDisconnectedError
|
|
107
|
+
│ └── android.py AndroidDriver — ADB-backed concrete driver
|
|
108
|
+
└── transport/
|
|
109
|
+
└── portal_client.py PortalClient — TCP-with-content-provider fallback
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 📚 Public API
|
|
113
|
+
|
|
114
|
+
Re-exported from `mobilerun_core_cli`:
|
|
115
|
+
|
|
116
|
+
| Symbol | What it is |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `AndroidDriver` | ADB+Portal device driver. Async methods: `tap`, `swipe`, `input_text`, `press_button`, `start_app`, `install_app`, `screenshot`, `get_ui_tree`, `get_apps`, `list_packages`, `get_date`. |
|
|
119
|
+
| `DeviceDriver` | Abstract base for drivers. `supported: set[str]` declares which verbs a subclass implements. |
|
|
120
|
+
| `DeviceDisconnectedError` | Raised when the device drops mid-call. |
|
|
121
|
+
| `PortalClient` | HTTP-or-content-provider client for the on-device Portal. Used internally by `AndroidDriver`; can be constructed directly for low-level access. |
|
|
122
|
+
| `setup_portal(device)` | Download + install + enable the Portal APK on a device. |
|
|
123
|
+
| `ensure_portal_ready(device)` | Idempotent: install/upgrade Portal and enable accessibility if needed. |
|
|
124
|
+
| `setup_keyboard(device)` | Switch the device to the Mobilerun IME. |
|
|
125
|
+
| `ping_portal(device)` | Verify Portal is installed and reachable. |
|
|
126
|
+
| `PORTAL_PACKAGE_NAME`, `A11Y_SERVICE_NAME` | Portal identifiers. |
|
|
127
|
+
| `portal_content_uri(pkg, path)` | Build `content://<pkg>/<path>` URIs. |
|
|
128
|
+
| `portal_a11y_service(pkg)`, `portal_ime_id(pkg)` | Accessibility service / IME component names. |
|
|
129
|
+
|
|
130
|
+
`AndroidDriver` accepts:
|
|
131
|
+
|
|
132
|
+
- `serial: str | None` — ADB serial; `None` picks the only connected device.
|
|
133
|
+
- `use_tcp: bool = False` — when `True`, the underlying `PortalClient` port-forwards Portal's HTTP server (`localhost:N → device:8080`) and uses it instead of the content provider. Faster but requires a working forward; falls back transparently.
|
|
134
|
+
|
|
135
|
+
## 🪵 Logging
|
|
136
|
+
|
|
137
|
+
All output goes through the `"mobilerun_core_cli"` logger. Configure it yourself; the package attaches no handlers.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import logging
|
|
141
|
+
logging.basicConfig(level=logging.INFO)
|
|
142
|
+
logging.getLogger("mobilerun_core_cli").setLevel(logging.DEBUG)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 🔗 Relationship to upstream `mobilerun`
|
|
146
|
+
|
|
147
|
+
This package is a verbatim slice of the upstream framework's Android driver + Portal modules, with imports rewritten under `mobilerun_core_cli` and loggers renamed. It tracks the upstream API for those four files; new features (iOS driver, recording driver, cloud driver, agent/CLI) stay upstream.
|
|
148
|
+
|
|
149
|
+
Use `mobilerun-core-cli` when you only need to drive a real Android device and want to keep the dependency footprint small.
|
|
150
|
+
Use [`mobilerun`](https://github.com/droidrun/mobilerun) when you want the full LLM-agent experience, CLI/TUI, and multi-platform support out of the box.
|
|
151
|
+
|
|
152
|
+
## 📄 License
|
|
153
|
+
|
|
154
|
+
MIT — see [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<picture align="center">
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun-dark.png">
|
|
3
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/droidrun/mobilerun/main/static/mobilerun.png" width="full">
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>mobilerun-core-cli is the slim async Portal + Android device driver core of <a href="https://github.com/droidrun/mobilerun">mobilerun</a>.</strong><br>
|
|
9
|
+
No CLI, no agent, no LLM providers — just enough to drive a real Android device through the Mobilerun Portal app over ADB. Intended as an embedding library for higher-level tools.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
<a href="https://docs.mobilerun.ai">📕 Documentation</a>
|
|
15
|
+
·
|
|
16
|
+
<a href="https://github.com/droidrun/mobilerun">🧠 mobilerun framework</a>
|
|
17
|
+
·
|
|
18
|
+
<a href="https://pypi.org/project/mobilerun-core-cli/">📦 PyPI</a>
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/mobilerun-core-cli/)
|
|
21
|
+
[](https://pypi.org/project/mobilerun-core-cli/)
|
|
22
|
+
[](./LICENSE)
|
|
23
|
+
[](https://discord.gg/ZZbKEZZkwK)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
- 📱 Drive a real Android device — `tap`, `swipe`, `type`, `screenshot`, `get_ui_tree`, `start_app`, `install_app`, …
|
|
30
|
+
- ⚡ TCP-with-content-provider fallback — fast HTTP path over `adb forward`, transparent fallback to content-provider RPC.
|
|
31
|
+
- 🛠 Portal lifecycle — download, install, accessibility enablement, auto-upgrade.
|
|
32
|
+
- 🪶 Slim — four files, four runtime deps (`async_adbutils`, `httpx`, `requests`, `rich`).
|
|
33
|
+
- 🔌 Embeddable — designed to be wrapped by sync facades (e.g. `mobilerun-core`) or used directly.
|
|
34
|
+
- 🤝 In sync with upstream — verbatim slice of [`droidrun/mobilerun`](https://github.com/droidrun/mobilerun); behaviour and API track upstream.
|
|
35
|
+
|
|
36
|
+
## 📦 Installation
|
|
37
|
+
|
|
38
|
+
> **Note:** Python `>=3.11,<3.14`. Requires [ADB](https://developer.android.com/studio/releases/platform-tools) on `PATH` and a device with USB debugging enabled.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv pip install mobilerun-core-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 🚀 Quick usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from async_adbutils import adb
|
|
49
|
+
from mobilerun_core_cli import AndroidDriver, ensure_portal_ready
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def main():
|
|
53
|
+
# 1. Make sure Portal is installed + accessibility is on.
|
|
54
|
+
device = await adb.device()
|
|
55
|
+
await ensure_portal_ready(device)
|
|
56
|
+
|
|
57
|
+
# 2. Drive the device.
|
|
58
|
+
driver = AndroidDriver(serial=device.serial, use_tcp=True)
|
|
59
|
+
await driver.connect()
|
|
60
|
+
|
|
61
|
+
await driver.tap(540, 1200)
|
|
62
|
+
await driver.swipe(540, 1600, 540, 400, duration_ms=300)
|
|
63
|
+
await driver.input_text("hello", clear=True)
|
|
64
|
+
png_bytes = await driver.screenshot()
|
|
65
|
+
tree = await driver.get_ui_tree()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 🧱 Layout
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
mobilerun_core_cli/
|
|
75
|
+
├── __init__.py Re-exports the public surface
|
|
76
|
+
├── portal.py Portal APK lifecycle + content-provider helpers
|
|
77
|
+
├── driver/
|
|
78
|
+
│ ├── base.py DeviceDriver ABC, DeviceDisconnectedError
|
|
79
|
+
│ └── android.py AndroidDriver — ADB-backed concrete driver
|
|
80
|
+
└── transport/
|
|
81
|
+
└── portal_client.py PortalClient — TCP-with-content-provider fallback
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 📚 Public API
|
|
85
|
+
|
|
86
|
+
Re-exported from `mobilerun_core_cli`:
|
|
87
|
+
|
|
88
|
+
| Symbol | What it is |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `AndroidDriver` | ADB+Portal device driver. Async methods: `tap`, `swipe`, `input_text`, `press_button`, `start_app`, `install_app`, `screenshot`, `get_ui_tree`, `get_apps`, `list_packages`, `get_date`. |
|
|
91
|
+
| `DeviceDriver` | Abstract base for drivers. `supported: set[str]` declares which verbs a subclass implements. |
|
|
92
|
+
| `DeviceDisconnectedError` | Raised when the device drops mid-call. |
|
|
93
|
+
| `PortalClient` | HTTP-or-content-provider client for the on-device Portal. Used internally by `AndroidDriver`; can be constructed directly for low-level access. |
|
|
94
|
+
| `setup_portal(device)` | Download + install + enable the Portal APK on a device. |
|
|
95
|
+
| `ensure_portal_ready(device)` | Idempotent: install/upgrade Portal and enable accessibility if needed. |
|
|
96
|
+
| `setup_keyboard(device)` | Switch the device to the Mobilerun IME. |
|
|
97
|
+
| `ping_portal(device)` | Verify Portal is installed and reachable. |
|
|
98
|
+
| `PORTAL_PACKAGE_NAME`, `A11Y_SERVICE_NAME` | Portal identifiers. |
|
|
99
|
+
| `portal_content_uri(pkg, path)` | Build `content://<pkg>/<path>` URIs. |
|
|
100
|
+
| `portal_a11y_service(pkg)`, `portal_ime_id(pkg)` | Accessibility service / IME component names. |
|
|
101
|
+
|
|
102
|
+
`AndroidDriver` accepts:
|
|
103
|
+
|
|
104
|
+
- `serial: str | None` — ADB serial; `None` picks the only connected device.
|
|
105
|
+
- `use_tcp: bool = False` — when `True`, the underlying `PortalClient` port-forwards Portal's HTTP server (`localhost:N → device:8080`) and uses it instead of the content provider. Faster but requires a working forward; falls back transparently.
|
|
106
|
+
|
|
107
|
+
## 🪵 Logging
|
|
108
|
+
|
|
109
|
+
All output goes through the `"mobilerun_core_cli"` logger. Configure it yourself; the package attaches no handlers.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import logging
|
|
113
|
+
logging.basicConfig(level=logging.INFO)
|
|
114
|
+
logging.getLogger("mobilerun_core_cli").setLevel(logging.DEBUG)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 🔗 Relationship to upstream `mobilerun`
|
|
118
|
+
|
|
119
|
+
This package is a verbatim slice of the upstream framework's Android driver + Portal modules, with imports rewritten under `mobilerun_core_cli` and loggers renamed. It tracks the upstream API for those four files; new features (iOS driver, recording driver, cloud driver, agent/CLI) stay upstream.
|
|
120
|
+
|
|
121
|
+
Use `mobilerun-core-cli` when you only need to drive a real Android device and want to keep the dependency footprint small.
|
|
122
|
+
Use [`mobilerun`](https://github.com/droidrun/mobilerun) when you want the full LLM-agent experience, CLI/TUI, and multi-platform support out of the box.
|
|
123
|
+
|
|
124
|
+
## 📄 License
|
|
125
|
+
|
|
126
|
+
MIT — see [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Slim async Portal+driver core, extracted from droidrun/mobilerun."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from mobilerun_core_cli.driver.android import AndroidDriver
|
|
6
|
+
from mobilerun_core_cli.driver.base import DeviceDisconnectedError, DeviceDriver
|
|
7
|
+
from mobilerun_core_cli.portal import (
|
|
8
|
+
A11Y_SERVICE_NAME,
|
|
9
|
+
PORTAL_PACKAGE_NAME,
|
|
10
|
+
ensure_portal_ready,
|
|
11
|
+
ping_portal,
|
|
12
|
+
portal_a11y_service,
|
|
13
|
+
portal_content_uri,
|
|
14
|
+
portal_ime_id,
|
|
15
|
+
setup_keyboard,
|
|
16
|
+
setup_portal,
|
|
17
|
+
)
|
|
18
|
+
from mobilerun_core_cli.transport.portal_client import PortalClient
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AndroidDriver",
|
|
22
|
+
"DeviceDriver",
|
|
23
|
+
"DeviceDisconnectedError",
|
|
24
|
+
"PortalClient",
|
|
25
|
+
"PORTAL_PACKAGE_NAME",
|
|
26
|
+
"A11Y_SERVICE_NAME",
|
|
27
|
+
"ensure_portal_ready",
|
|
28
|
+
"setup_portal",
|
|
29
|
+
"setup_keyboard",
|
|
30
|
+
"ping_portal",
|
|
31
|
+
"portal_content_uri",
|
|
32
|
+
"portal_a11y_service",
|
|
33
|
+
"portal_ime_id",
|
|
34
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""AndroidDriver — ADB-based device driver.
|
|
2
|
+
|
|
3
|
+
Wraps ``async_adbutils.AdbDevice`` + ``PortalClient`` to provide clean device I/O
|
|
4
|
+
without event emission, formatting, or element lookup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from async_adbutils import adb
|
|
15
|
+
|
|
16
|
+
from mobilerun_core_cli.transport.portal_client import PortalClient
|
|
17
|
+
from mobilerun_core_cli.driver.base import DeviceDriver
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("mobilerun_core_cli")
|
|
20
|
+
|
|
21
|
+
PORTAL_DEFAULT_TCP_PORT = 8080
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AndroidDriver(DeviceDriver):
|
|
25
|
+
"""Raw Android device I/O via ADB + Portal."""
|
|
26
|
+
|
|
27
|
+
platform = "Android"
|
|
28
|
+
|
|
29
|
+
supported = {
|
|
30
|
+
"tap",
|
|
31
|
+
"swipe",
|
|
32
|
+
"input_text",
|
|
33
|
+
"press_button",
|
|
34
|
+
"start_app",
|
|
35
|
+
"screenshot",
|
|
36
|
+
"get_ui_tree",
|
|
37
|
+
"get_date",
|
|
38
|
+
"get_apps",
|
|
39
|
+
"list_packages",
|
|
40
|
+
"install_app",
|
|
41
|
+
"drag",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
supported_buttons = {"back", "home", "enter"}
|
|
45
|
+
|
|
46
|
+
_BUTTON_KEYCODES = {
|
|
47
|
+
"back": 4,
|
|
48
|
+
"home": 3,
|
|
49
|
+
"enter": 66,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
serial: str | None = None,
|
|
55
|
+
use_tcp: bool = False,
|
|
56
|
+
remote_tcp_port: int = PORTAL_DEFAULT_TCP_PORT,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._serial = serial
|
|
59
|
+
self._use_tcp = use_tcp
|
|
60
|
+
self._remote_tcp_port = remote_tcp_port
|
|
61
|
+
self.device = None
|
|
62
|
+
self.portal: PortalClient | None = None
|
|
63
|
+
self._connected = False
|
|
64
|
+
|
|
65
|
+
# -- lifecycle -----------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
async def connect(self) -> None:
|
|
68
|
+
if self._connected:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
self.device = await adb.device(serial=self._serial)
|
|
72
|
+
state = await self.device.get_state()
|
|
73
|
+
if state != "device":
|
|
74
|
+
raise ConnectionError(f"Device is not online. State: {state}")
|
|
75
|
+
|
|
76
|
+
self.portal = PortalClient(self.device, prefer_tcp=self._use_tcp)
|
|
77
|
+
await self.portal.connect()
|
|
78
|
+
|
|
79
|
+
from mobilerun_core_cli.portal import setup_keyboard # circular import guard
|
|
80
|
+
|
|
81
|
+
await setup_keyboard(self.device)
|
|
82
|
+
self._connected = True
|
|
83
|
+
|
|
84
|
+
async def ensure_connected(self) -> None:
|
|
85
|
+
if not self._connected:
|
|
86
|
+
await self.connect()
|
|
87
|
+
|
|
88
|
+
# -- input actions -------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
async def tap(self, x: int, y: int) -> None:
|
|
91
|
+
await self.ensure_connected()
|
|
92
|
+
await self.device.click(x, y)
|
|
93
|
+
|
|
94
|
+
async def swipe(
|
|
95
|
+
self,
|
|
96
|
+
x1: int,
|
|
97
|
+
y1: int,
|
|
98
|
+
x2: int,
|
|
99
|
+
y2: int,
|
|
100
|
+
duration_ms: float = 1000,
|
|
101
|
+
) -> None:
|
|
102
|
+
await self.ensure_connected()
|
|
103
|
+
await self.device.swipe(x1, y1, x2, y2, float(duration_ms / 1000))
|
|
104
|
+
await asyncio.sleep(duration_ms / 1000)
|
|
105
|
+
|
|
106
|
+
async def input_text(self, text: str, clear: bool = False) -> bool:
|
|
107
|
+
await self.ensure_connected()
|
|
108
|
+
return await self.portal.input_text(text, clear)
|
|
109
|
+
|
|
110
|
+
async def press_button(self, button: str) -> None:
|
|
111
|
+
await self.ensure_connected()
|
|
112
|
+
button_lower = button.lower()
|
|
113
|
+
if button_lower not in self.supported_buttons:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Button '{button}' not supported. "
|
|
116
|
+
f"Supported: {', '.join(sorted(self.supported_buttons))}"
|
|
117
|
+
)
|
|
118
|
+
await self.device.keyevent(self._BUTTON_KEYCODES[button_lower])
|
|
119
|
+
|
|
120
|
+
async def drag(
|
|
121
|
+
self,
|
|
122
|
+
x1: int,
|
|
123
|
+
y1: int,
|
|
124
|
+
x2: int,
|
|
125
|
+
y2: int,
|
|
126
|
+
duration: float = 3.0,
|
|
127
|
+
) -> None:
|
|
128
|
+
await self.ensure_connected()
|
|
129
|
+
raise NotImplementedError("Drag is not implemented yet")
|
|
130
|
+
|
|
131
|
+
# -- app management ------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
async def start_app(self, package: str, activity: Optional[str] = None) -> str:
|
|
134
|
+
await self.ensure_connected()
|
|
135
|
+
try:
|
|
136
|
+
logger.debug(f"Starting app {package} with activity {activity}")
|
|
137
|
+
if not activity:
|
|
138
|
+
dumpsys_output = await self.device.shell(
|
|
139
|
+
f"cmd package resolve-activity --brief {package}"
|
|
140
|
+
)
|
|
141
|
+
activity = dumpsys_output.splitlines()[1].split("/")[1]
|
|
142
|
+
|
|
143
|
+
logger.debug(f"Activity: {activity}")
|
|
144
|
+
await self.device.app_start(package, activity)
|
|
145
|
+
logger.debug(f"App started: {package} with activity {activity}")
|
|
146
|
+
return f"App started: {package} with activity {activity}"
|
|
147
|
+
except Exception as e:
|
|
148
|
+
return f"Failed to start app {package}: {e}"
|
|
149
|
+
|
|
150
|
+
async def install_app(self, path: str, **kwargs) -> str:
|
|
151
|
+
await self.ensure_connected()
|
|
152
|
+
if not os.path.exists(path):
|
|
153
|
+
return f"Failed to install app: APK file not found at {path}"
|
|
154
|
+
|
|
155
|
+
reinstall = kwargs.get("reinstall", False)
|
|
156
|
+
grant_permissions = kwargs.get("grant_permissions", True)
|
|
157
|
+
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"Installing app: {path} with reinstall: {reinstall} "
|
|
160
|
+
f"and grant_permissions: {grant_permissions}"
|
|
161
|
+
)
|
|
162
|
+
result = await self.device.install(
|
|
163
|
+
path,
|
|
164
|
+
nolaunch=True,
|
|
165
|
+
uninstall=reinstall,
|
|
166
|
+
flags=["-g"] if grant_permissions else [],
|
|
167
|
+
silent=True,
|
|
168
|
+
)
|
|
169
|
+
logger.debug(f"Installed app: {path} with result: {result}")
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
async def get_apps(self, include_system: bool = True) -> List[Dict[str, str]]:
|
|
173
|
+
await self.ensure_connected()
|
|
174
|
+
return await self.portal.get_apps(include_system)
|
|
175
|
+
|
|
176
|
+
async def list_packages(self, include_system: bool = False) -> List[str]:
|
|
177
|
+
await self.ensure_connected()
|
|
178
|
+
filter_list = [] if include_system else ["-3"]
|
|
179
|
+
return await self.device.list_packages(filter_list)
|
|
180
|
+
|
|
181
|
+
# -- state / observation -------------------------------------------------
|
|
182
|
+
|
|
183
|
+
async def screenshot(self, hide_overlay: bool = True) -> bytes:
|
|
184
|
+
await self.ensure_connected()
|
|
185
|
+
return await self.portal.take_screenshot(hide_overlay)
|
|
186
|
+
|
|
187
|
+
async def get_ui_tree(self) -> Dict[str, Any]:
|
|
188
|
+
await self.ensure_connected()
|
|
189
|
+
return await self.portal.get_state()
|
|
190
|
+
|
|
191
|
+
async def get_date(self) -> str:
|
|
192
|
+
await self.ensure_connected()
|
|
193
|
+
result = await self.device.shell("date")
|
|
194
|
+
return result.strip()
|