layernav-android 0.5.1__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.
@@ -0,0 +1,19 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ target: auto
6
+ threshold: 2%
7
+ patch:
8
+ default:
9
+ target: auto
10
+ threshold: 5%
11
+
12
+ comment:
13
+ layout: "reach, diff, flags, files"
14
+ behavior: default
15
+ require_changes: false
16
+
17
+ ignore:
18
+ - "tests/"
19
+ - "src/layernav_android/contrib/"
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ name: Test (py3.12)
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 5
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install
30
+ run: pip install -e ".[dev]"
31
+
32
+ - name: Run tests with coverage
33
+ run: python -m pytest tests/ -v --tb=short --cov=layernav_android --cov-report=xml
34
+
35
+ - name: Upload coverage to Codecov
36
+ uses: codecov/codecov-action@v5
37
+ with:
38
+ files: ./coverage.xml
39
+ fail_ci_if_error: false
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 yuyidream
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,390 @@
1
+ Metadata-Version: 2.4
2
+ Name: layernav_android
3
+ Version: 0.5.1
4
+ Summary: Multi-layer task-stack navigation framework for Android ADB automation. Define N layers, register per-layer handlers, and let the framework handle BACK recovery, cold-start, cross-layer verification, and sub-page navigation (detect_detail + _recover_to_page).
5
+ Project-URL: Homepage, https://github.com/yuyidream/layernav_android
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: adb,android,app-crawler,automation,crawler,data-collection,fault-tolerance,layer-model,mobile-rpa,navigation,python,task-automation
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Software Development :: Testing
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: loguru>=0.7
17
+ Provides-Extra: dev
18
+ Requires-Dist: numpy>=1.24; extra == 'dev'
19
+ Requires-Dist: opencv-python>=4.8; extra == 'dev'
20
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # LayerNav_Android
25
+
26
+ > A stable page layer navigation framework for Android ADB automation.
27
+ > 基于 ADB 的安卓页面层级导航框架,主打**强校验、自动容错、故障恢复**,适用于 APP 数据采集、移动端 RPA、UI 自动化测试场景。
28
+ >
29
+ > **Python 3.12+** · 轻量依赖(仅 loguru) · MIT License
30
+
31
+ [![PyPI](https://badge.fury.io/py/layernav_android.svg)](https://badge.fury.io/py/layernav_android)
32
+ [![Python](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
33
+ [![CI](https://github.com/yuyidream/layernav_android/actions/workflows/ci.yml/badge.svg)](https://github.com/yuyidream/layernav_android/actions/workflows/ci.yml)
34
+ [![codecov](https://codecov.io/gh/yuyidream/layernav_android/branch/main/graph/badge.svg)](https://codecov.io/gh/yuyidream/layernav_android)
35
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
36
+
37
+ ---
38
+
39
+ ## 一、项目介绍
40
+
41
+ 市面上主流 ADB / UI 自动化库仅提供点击、滑动、返回等基础原子能力,**缺少页面状态校验、层级管理、异常恢复**。本框架基于 **L0~Ln 页面层级模型** 设计,将桌面、APP主页、内容页、详情页抽象为标准化层级,内置「动作执行 → 截屏校验 → 自动重试 → 冷启动恢复」全链路能力。
42
+
43
+ ### 核心定位
44
+
45
+ - 导航调度 + 容错引擎
46
+ - 框架负责:层级检测、跳转校验、后退/前进/恢复、**子页面导航**
47
+ - 业务脚本负责:控件点击、数据采集、业务逻辑
48
+
49
+ ---
50
+
51
+ ## 二、核心特性
52
+
53
+ ✅ **标准化层级模型**
54
+ 统一抽象 `L0(手机桌面) / L1(APP主页) / L2(内容页) / L3(详情页) ... Ln`,一套模型适配绝大多数 APP。
55
+
56
+ ✅ **闭环跳转校验**
57
+ 执行操作后自动截屏检测页面,**拒绝盲操作**,跳转失败即时感知。guard(前置校验)+ validator(后置轮询)语义分离。
58
+
59
+ ✅ **目标感知检测(v0.5.0)**
60
+ `detect()` 负责"我在哪"(返回 `str | None`),`detect_layer(target)` 负责"到达目标了吗"(返回 `bool`)。导航 API 全部使用 `detect_layer` 做目标验证,`detect()` 无法判定时(返回 `None`)自动回退到 `back_recover`。
61
+
62
+ ✅ **同层多页面导航(v0.3.0)**
63
+ `detect_detail()` 一次截图返回层级 + 子页面;`back_recover` / `back` / `restore` 支持 `target_page` 参数,恢复后自动精确定位到指定子页面。
64
+
65
+ ✅ **完整导航原子 API**
66
+ 内置 `detect / detect_layer / enter_next / back_one / back_recover` 五原子操作 + `advance / back / restore` 三组合操作,一行代码完成跨层级跳转。
67
+
68
+ ✅ **故障自动恢复(v0.5.0 强化)**
69
+ `detect()` 无法判定层级时 → `back()` / `restore()` 直接走 `back_recover`(HOME → 冷启动 → 前进恢复),不再盲试 BACK。返回键失效、页面卡死、意外退回桌面时同样自动恢复。
70
+
71
+ ✅ **Quick 快速模式**
72
+ 专为恢复场景设计,handlers 收到 `quick=True` 时可精简业务逻辑(如选第一个未读),提升导航速度。
73
+
74
+ ✅ **可观测监听器**
75
+ 内置 `LayerListener` 观察者接口,零侵入监控层切换、超时、恢复事件,方便接入指标采集与告警。
76
+
77
+ ✅ **解耦设计**
78
+ - 页面检测 `detect` 接口可自由接入:OCR / 图像匹配 / UI 控件解析
79
+ - ADB 客户端通过 `AdbProtocol` 完全抽象,原生 ADB / 风控加固 ADB 均可无缝接入
80
+ - 分层 Handler 机制(`_on_Lx`),业务代码与框架逻辑完全隔离
81
+
82
+ ---
83
+
84
+ ## 三、架构设计
85
+
86
+ ### 1. 层级模型
87
+
88
+ ```
89
+ L0 手机主屏幕(非 APP 前台)
90
+ L1 APP 主界面
91
+ L2 二级内容页
92
+ L3 三级详情页
93
+ ...
94
+ Ln 最深业务层级
95
+ ```
96
+
97
+ #### 同层多页面支持(v0.3.0)
98
+
99
+ 同一个层级可包含多个子页面(如 L1 的会话列表/通讯录/发现/我)。`LayerDef` 通过 `page_name` 和 `detection_extra` 字段声明:
100
+
101
+ ```python
102
+ layers = [
103
+ LayerDef("L1", "main_list", "微信主界面", "is_main_list_chrome()",
104
+ page_name="chat_list",
105
+ detection_extra="子页面: chat_list/contacts/discover/profile"),
106
+ ]
107
+ ```
108
+
109
+ | 能力 | 方法 | 说明 |
110
+ |------|------|------|
111
+ | 子页面检测 | `detect_detail() → DetectResult` | 返回 `(layer_key, page_name)` |
112
+ | 子页面恢复 | `restore(..., target_page="chat_list")` | 到达目标层后调用 `_recover_to_page` |
113
+ | Tab 切换(L1) | `WeChatGroupLayerModel._recover_to_page` | 框架自动计算底部 tab 坐标并点击 |
114
+ | 校验型(L2/L3) | `detect_detail` 验证 | 无需额外操作,校验 page_name 即可 |
115
+
116
+ ### 2. 职责划分
117
+
118
+ | 模块 | 框架能力 | Task 能力 |
119
+ |------|---------|----------|
120
+ | 状态检测 | 调用 `detect()` / `detect_detail()`、校验结果 | 实现截图/识别逻辑 |
121
+ | 页面动作 | 流程调度、等待、重试 | 实现 `_on_Lx` 点击/滑动等业务动作 |
122
+ | 导航逻辑 | `advance` / `back` / `restore` / 恢复 | 无 |
123
+ | 子页面导航 | `detect_detail` / `_recover_to_page` | 无(框架提供 `target_page` 路由) |
124
+ | 点击 | **不负责** — 框架不 `tap` | handler 内 `adb.tap()` |
125
+
126
+ ### 3. 核心流程
127
+
128
+ ```
129
+ 1. detect() 实时识别当前页面层级(无法识别时返回 None)
130
+ 2. 调用对应层级 _on_Lx handler 执行业务操作
131
+ 3. detect_layer() 二次校验页面是否到达目标层级(截屏 + 轮询)
132
+ 4. detect() 返回 None 或跳转失败 → 直接 back_recover 冷启动恢复
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 四、快速上手
138
+
139
+ ### 1. 安装
140
+
141
+ **方式一:pip 安装(推荐)**
142
+
143
+ ```bash
144
+ pip install layernav_android
145
+ ```
146
+
147
+ 如需使用 WeChat contrib 模块,需额外安装 `opencv-python` 和 `numpy`:
148
+
149
+ ```bash
150
+ pip install opencv-python numpy
151
+ ```
152
+
153
+ **方式二:从源码安装**
154
+
155
+ ```bash
156
+ git clone https://github.com/yuyidream/layernav_android.git
157
+ cd layernav_android
158
+ pip install -e .
159
+ ```
160
+
161
+ 如需运行测试:
162
+
163
+ ```bash
164
+ pip install -e ".[dev]"
165
+ pytest tests/ -v --tb=short
166
+ ```
167
+
168
+ ### 2. 基础使用
169
+
170
+ 继承 `BaseLayerModel`,实现层级检测与页面处理器,即可使用全套导航能力:
171
+
172
+ ```python
173
+ from layernav_android import BaseLayerModel, LayerDef
174
+
175
+ class DemoAppModel(BaseLayerModel):
176
+ layers = [
177
+ LayerDef(key="L0", name="desktop", label_cn="手机桌面", detection="截屏识别桌面图标"),
178
+ LayerDef(key="L1", name="app_home", label_cn="APP 主页", detection="OCR 识别主页文字",
179
+ page_name="home", detection_extra="子页面: home/search"),
180
+ LayerDef(key="L2", name="content", label_cn="内容列表页", detection="图像特征匹配"),
181
+ LayerDef(key="L3", name="detail", label_cn="详情页", detection="模板匹配"),
182
+ ]
183
+
184
+ def detect(self, adb, scale_w: float) -> str | None:
185
+ screenshot = adb.screencap()
186
+ if is_desktop(screenshot):
187
+ return "L0"
188
+ elif is_app_home(screenshot):
189
+ return "L1"
190
+ elif is_content_list(screenshot):
191
+ return "L2"
192
+ elif is_detail(screenshot):
193
+ return "L3"
194
+ return None # 无法判定 → 框架走 back_recover
195
+
196
+ def detect_layer(self, adb, scale_w: float, layer: str) -> bool:
197
+ screenshot = adb.screencap()
198
+ if layer == "L0":
199
+ return is_desktop(screenshot)
200
+ elif layer == "L1":
201
+ return is_app_home(screenshot) and not is_content_list(screenshot)
202
+ elif layer == "L2":
203
+ return is_content_list(screenshot)
204
+ elif layer == "L3":
205
+ return is_detail(screenshot)
206
+ return False
207
+
208
+ def _on_L0(self, adb, scale_w, *, quick=False):
209
+ self._cold_start(adb, "L1", scale_w)
210
+ return "L1"
211
+
212
+ def _on_L1(self, adb, scale_w, *, quick=False) -> str | None:
213
+ if quick:
214
+ row = self._pick_first_row(adb, scale_w)
215
+ else:
216
+ row = self._scan_and_select(adb, scale_w)
217
+ if row is None:
218
+ return None
219
+ adb.tap(row.x, row.y)
220
+ return "L2"
221
+
222
+ def _on_L2(self, adb, scale_w, *, quick=False) -> str | None:
223
+ item = self._pick_item(adb, scale_w, quick=quick)
224
+ if item is None:
225
+ return None
226
+ adb.tap(item.x, item.y)
227
+ return "L3"
228
+
229
+ def _on_L3(self, adb, scale_w, *, quick=False) -> str | None:
230
+ return None # 最深层,不再前进
231
+
232
+
233
+ # 执行导航流程
234
+ model = DemoAppModel()
235
+ adb = get_adb_client()
236
+
237
+ # 检测层级 + 子页面
238
+ dr = model.detect_detail(adb, scale_w=1.0)
239
+ print(f"当前: {dr.layer_key} / {dr.page_name}")
240
+
241
+ # 智能恢复到 L1 的 home 子页面
242
+ model.restore(adb, target_layer="L1", scale_w=1.0, target_page="home")
243
+ # 逐层前进到 L3
244
+ model.advance(adb, target_layer="L3", scale_w=1.0)
245
+ # 后退回 L1 的 search 子页面
246
+ model.back(adb, to_layer="L1", scale_w=1.0, target_page="search")
247
+ ```
248
+
249
+ ### 3. 核心 API
250
+
251
+ **原子操作**
252
+
253
+ | 方法 | 说明 |
254
+ |------|------|
255
+ | `detect(adb, scale_w) → str \| None` | 检测当前所在层级(Task 覆盖实现)。无法判定时返回 `None`,框架自动走恢复 |
256
+ | `detect_layer(adb, scale_w, layer) → bool` | 目标感知检测:当前屏幕是否匹配指定层级(v0.5.0,Task 覆盖实现) |
257
+ | `detect_detail(adb, scale_w) → DetectResult` | 检测层级 + 子页面名称(v0.3.0,默认调用 `detect` + `LayerDef.page_name`) |
258
+ | `enter_next(adb, scale_w, *, quick, max_wait_s) → bool` | 单步进入下一层 ← guard + validator + 轮询 |
259
+ | `back_one(adb, scale_w) → str` | 单步 `KEYCODE_BACK`,返回新层级 |
260
+ | `back_recover(adb, target, scale_w, *, target_page=None) → bool` | 故障恢复:HOME → 冷启动 → 快速前进 → 子页面(v0.4.3: 冷启动 3 次重试 + `adb reboot` 兜底) |
261
+
262
+ **组合操作**
263
+
264
+ | 方法 | 说明 |
265
+ |------|------|
266
+ | `back(adb, to_layer, scale_w, *, target_page=None) → bool` | 后退至目标层(v0.5.0: 直接用 `detect_layer` 验证,`detect()` 返回 `None` 时直接 `back_recover`)**(v0.3.0: 支持 `target_page`)** |
267
+ | `advance(adb, target, scale_w, *, quick, max_wait_s) → bool` | 逐层前进至目标(v0.5.0: 用 `detect_layer` 验证到达;目标层 always `quick=False`) |
268
+ | `restore(adb, target, scale_w, *, target_page=None) → bool` | 智能判断方向,从任意位置恢复至目标 + 子页面(v0.5.0: `detect()` 返回 `None` 时直接 `back_recover`)**(v0.3.0: 支持 `target_page`)** |
269
+
270
+ **可观测**
271
+
272
+ ```python
273
+ from layernav_android import LayerListener
274
+
275
+ class MetricsListener:
276
+ def on_transition(self, from_layer, to_layer, method):
277
+ print(f"{from_layer} → {to_layer} via {method}")
278
+
279
+ def on_timeout(self, from_layer, target_layer, elapsed_s):
280
+ print(f"Timeout {from_layer}→{target_layer} after {elapsed_s:.1f}s")
281
+
282
+ def on_recovery(self, target_layer, ok):
283
+ print(f"Recovery to {target_layer}: {'OK' if ok else 'FAILED'}")
284
+
285
+ model.add_listener(MetricsListener())
286
+ ```
287
+
288
+ ---
289
+
290
+ ## 五、通用冷启动工具
291
+
292
+ `cold_start_app_from_launcher` 提供统一的 APP 冷启动能力,支持 monkey 主路径 + Dock 图标兜底 + session tab 点击:
293
+
294
+ ```python
295
+ from layernav_android.cold_start import cold_start_app_from_launcher
296
+
297
+ # 微信 — 最简调用(尺寸自动获取)
298
+ ok = cold_start_app_from_launcher(
299
+ adb, "com.tencent.mm",
300
+ app_name="wechat", M=4, N=3,
301
+ )
302
+
303
+ # 微信 — 含 session tab
304
+ ok = cold_start_app_from_launcher(
305
+ adb, "com.tencent.mm",
306
+ app_name="wechat", M=4, N=3,
307
+ session_tab_x=108, session_tab_y=2192,
308
+ )
309
+
310
+ # 小红书
311
+ ok = cold_start_app_from_launcher(
312
+ adb, "com.xingin.xhs",
313
+ app_name="xhs", M=4, N=1,
314
+ )
315
+ ```
316
+
317
+ **关键设计**:使用普通 ADB tap(非防风控触控),因为是系统级操作(桌面 Dock 图标点击),不涉及 APP 内反爬检测,方便所有系统集成。
318
+
319
+ **最后一搏 — adb reboot 兜底**(`allow_reboot=True`,默认关闭):
320
+
321
+ 当 monkey、am start、Dock icon tap 三条路径全部失败时,可选执行 `adb reboot` 作为终极恢复手段。重启后等待设备上线 + boot 完成,然后重新尝试 monkey 启动。
322
+
323
+ > ⚠️ 重启耗时 60–120 s,且要求设备无需手动解锁(无 PIN/图案锁)。适用于无人值守的 7×24 自动化。
324
+
325
+
326
+ ## 六、适用场景
327
+
328
+
329
+ - **移动端 RPA 自动化** — 加速开发
330
+ - **Android UI 自动化测试** — 提升脚本稳定性,减少维护成本
331
+ - **APP 流程逆向 / 行为模拟** — 稳定进入深层页面
332
+
333
+ ---
334
+
335
+ ## 七、优势对比
336
+
337
+ | 能力 | 本框架 | Appium / uiautomator2 / Airtest |
338
+ |------|--------|---------------------------------|
339
+ | 标准化页面层级 | ✅ 内置模型 | ❌ 无统一抽象 |
340
+ | 同层多页面导航 | ✅ `detect_detail` + `_recover_to_page` | ❌ 需手动分支 |
341
+ | 操作后页面校验 | ✅ 闭环 guard + validator | ❌ 仅执行动作,不校验结果 |
342
+ | 自动后退恢复 | ✅ 3 次重试 + 冷启动兜底 | ❌ 需手动编写重试逻辑 |
343
+ | 层级穿越 API | ✅ `advance` / `back` / `restore` | ❌ 仅基础点击/返回 |
344
+ | 可观测监听器 | ✅ `LayerListener` 事件回调 | ❌ 需自行埋点 |
345
+ | ADB 解耦 | ✅ `AdbProtocol` 接口抽象 | ⚠️ 部分耦合 |
346
+
347
+ ---
348
+
349
+ ## 八、拓展建议
350
+
351
+ - **页面检测能力**:可接入 PaddleOCR / EasyOCR / OpenCV 图像匹配
352
+ - **状态机拓展**:可结合 `python-statemachine` 优化状态管理(本框架的 `LayerListener` 即借鉴其设计)
353
+ - **多设备并行**:每设备独立 `BaseLayerModel` 实例即可天然支持多设备
354
+
355
+ ---
356
+
357
+ ## 九、目录结构
358
+
359
+ ```
360
+ layernav_android/
361
+ ├── src/layernav_android/
362
+ │ ├── __init__.py # 公开导出
363
+ │ ├── _protocol.py # AdbProtocol 接口
364
+ │ ├── base.py # LayerDef, LayerListener, BaseLayerModel (v0.5.0 + detect_layer)
365
+ │ ├── cold_start.py # 通用冷启动工具(含 adb reboot 兜底)
366
+ │ └── contrib/
367
+ │ ├── __init__.py
368
+ │ ├── wechat.py # WeChatGroupLayerModel(微信示例)
369
+ │ └── xhs.py # XhsLayerModel(小红书占位)
370
+ ├── tests/
371
+ │ └── test_base.py # 23 个单元测试
372
+ ├── pyproject.toml
373
+ ├── README.md
374
+ └── LICENSE
375
+ ```
376
+
377
+ ---
378
+
379
+ ## 十、参与贡献
380
+
381
+ 欢迎提交 Issue、PR,共建安卓自动化导航生态:
382
+
383
+ - **Bug 反馈、功能建议** → [Issues](https://github.com/yuyidream/layernav_android/issues)
384
+ - **代码优化、新增示例** → Pull Request
385
+
386
+ ---
387
+
388
+ ## 十一、开源协议
389
+
390
+ 本项目基于 [MIT License](LICENSE) 开源,可自由用于个人、商业项目。