Skip to content

Conversation

@XcantloadX
Copy link

Baas on Android

本 PR 将讨论将 BAAS 移植到手机上,完全脱离电脑运行的可能性,以及我目前验证与移植的进度。

(此 PR 目前仅作讨论,暂不合并)

为什么要移植到 Android 上

根本原因是我想为某个游戏编写脚本,但是那个游戏禁止在模拟器运行。一直以来我都在思考将使用 Python 编写的游戏脚本移植到 Android 上的可能性。最近我认为思路已经足够,因此我决定先以 baas 为例开始实验。

移植到 Android 上可能的优点:

  1. 降低挂机门槛,因为不再需要电脑
  2. 大幅提升截图与操作速度,因为不再需要通过某种手段传递数据,直接调 Android API,就像在 Windows 上直接调 Windows API 一样

可能的缺点:

  1. 受限于手机性能,可能会导致 OCR 与模板匹配等速度下降
  2. 兼容性问题
  3. 项目复杂度可能会上升

思路

下面我将从几个方面分别讨论移植的思路与方案。

Python 本体

流行的 python android 方案有两种:

  1. Python for Android(简称 p4a):Python 为主,Java 为客。理论上可以不碰 Java 代码将 Python 程序移植到 Android 上。
  2. chaquopy:Java 为主,Python 为客。实际上是一个 Python SDK,需要自己将 Android 与 Python 桥接起来。

原本打算选择 2,因为更灵活,根据我下面的思路可能需要大量自定义 Java 代码。但是受限于 GUI 方案,这里只能选择 1。

值得注意的是,p4a 默认提供 Python 3.11,而 BAAS 使用 3.9。考虑到时间成本,这里暂定使用 3.11 版本,如果出现问题,后续再考虑能否可以改用 3.9。

GUI

BAAS 的 GUI 为 PyQt5 + pyqt fluent widgets。经过一番搜索,p4a 支持 PySide6,chaquopy 不支持任何 Qt 框架。尽管 PyQt5 似乎也可以部署到 Android 平台,但是资料久远且稀少,因此我决定将 BAAS 的 GUI 先行移植到 PySide6 上,再采用 p4a 方案。

目前使用的方案是:使用 qtpy + monkey patch

qtpy 是一个 Python Qt 兼容层,抹平了 PyQt5/6 与 PySide6 之间的 API 差异。在引入 qtpy 后,通过 monkey patch,将对 PyQt5 的调用全部转发到 qtpy 上,然后由 qtpy 转发到 PySide6 上。
对于 pyqt fluent widgets,它本身就有 PySide6 版本,因此不需要做太多改动。

具体代码见 patch_window.py

如果后续有机会,可以考虑将 BAAS 的 GUI 重构到 PySide6 或 qtpy 上。

OpenCV/numpy

p4a 支持 OpenCV 与 numpy。

OCR

BAAS 的 OCR 采用 IPC + 共享内存方案,但是很不幸,多进程与内存共享在 Android 上都不原生可用。如果坚持这种方案,可能需要大量修改 C++ 与 Python 代码,以采用 Android 平台上专有的内存共享方案。

因此,短期来看,我的解决方案是舍弃现有 ocr_server,改用 RapidOCR 的 Android 库,对其进行封装,让 Python 可以调用;长期来看,可以考虑(至少在 Android 上)舍弃 C++ 的多进程通信方案,做成 Python native 扩展。

adb

通常游戏脚本会大量依赖 adb 命令,特别是 adb shell 命令。一般情况下 adb shell 命令只有借助 adb 才能执行,无法在 Android 自身上执行,因为权限不够。

但是借助 Shizuku,普通的 Android APP 也可以拥有 shell 权限,因此脱离电脑执行 adb shell 命令是理论上可行的。

分辨率

目前有两种方案可以在手机上模拟出 1280x720 分辨率:

  1. wm size 命令:在安卓手机上更改分辨率后仍然无法使用 #202 (comment)
  2. 虚拟显示器:[Discuss] 使用 Scrcpy 替代截图的可行性 #49 (comment)

方案 1 会更简单,而且对现有控制代码的兼容性会更好,因为不涉及多显示器的处理。
但是采用方案 1,脚本执行时 BAAS 会被置于后台,存在被杀后台的风险,而方案 2 不存在这个问题。而且方案 2 理论上可以让用户在挂机的同时,干其他任何事情,只要手机性能允许。

截图

  1. 仍然采用 screencap,与现有 adb 方案保持一致,可能存在性能问题(主要是数据接收的问题)
  2. MediaProjection API
  3. 想办法让 uiautomator2 在 Android 上能够正常运行,然后保持 u2 方案

方案 2 在速度与性能上最佳,基本上录屏与投屏类 APP 都使用 MediaProjection API,不存在性能问题。
但是考虑到控制的问题,因此可能暂时会采用方案 3。

控制

  1. adb shell input
  2. Accessibility API
  3. 强兼 u2

方案 1 最简单;方案 2 功能最强大,但是需要用户手动授权,而且授权很容易掉,目前不清楚是否可以通过 shell 完成。

考虑到 #395 (comment) ,方案 3 如果可行,可能是目前的最佳选择。

其他

关于代码修改,根据上面的思路,会有很多地方需要针对平台特判。如果

  1. 作者打算合并这个 PR,作为官方支持,那么我会直接修改代码;
  2. 如果不打算合并,那么我会尽量通过 monkey patch 来实现,尽量少修改核心代码,保持我的移植作为 BAAS 的一个 fork 存在

进度与问题

GUI

目前我已成功移植 GUI,并且大部分功能正常可用,除了:

  1. Qt Android 对似乎没有内置对触摸屏的特别支持,目前滑动操作会被认为是鼠标拖拽,导致滚动只能通过拖拽右侧细小的滚动条完成
  2. 下面的截图是在平板上的运行效果,在手机上,屏幕会更小,导致很多布局异常
  3. Dropdown/Combobox 组件的弹出层无法正常显示

截图:
472af38cca5a526b85cfd3f7c75ba0d4
8e6ecb9074b501a9fb2bfb955781fb45_720
550ab6262e9a7aa8143628a96a6bdb7c

adb

目前已成功验证通过 Shizuku 调用 shell 命令可行。

下面的视频演示了 input swipe 命令。可以看到速度还是不错的,点击按钮后几乎瞬间就执行了滑动。

e9ec73c3a3bf925b2a8d4f460ae00018.mp4

分辨率

根据上面提到的 issue,已经有人成功验证了通过 wm size 修改分辨率可行。

控制

根据刚才的演示视频,input 方案可行。

@pur1fying
Copy link
Owner

感谢你的pr

  1. 移植app到安卓是非常不错的想法,我完全支持
  2. 有关GUI,目前GUI正在使用Web重构,或许可以舍弃PyQT, @Kiramei 可以对这个问题做下说明
  3. 有关OCR,我会尝试修改c++代码使其能在安卓平台能正常运行,如果实在无法实现才会考虑rapidocr
  4. 有关控制方式,兼容u2应该不是特别困难, 我在u2的群聊里看到有人成功实现过

ps: 我仅有在安卓开发java app的经验,有关在安卓进行python和c++开发需要先了解一下才能更进一步讨论

@Kiramei
Copy link
Collaborator

Kiramei commented Oct 5, 2025

感谢你的pr

  1. 移植app到安卓是非常不错的想法,我完全支持

  2. 有关GUI,目前GUI正在使用Web重构,或许可以舍弃PyQT, @Kiramei 可以对这个问题做下说明

  3. 有关OCR,我会尝试修改c++代码使其能在安卓平台能正常运行,如果实在无法实现才会考虑rapidocr

  4. 有关控制方式,兼容u2应该不是特别困难, 我在u2的群聊里看到有人成功实现过

ps: 我仅有在安卓开发java app的经验,有关在安卓进行python和c++开发需要先了解一下才能更进一步讨论

关于第2点,正在开发中的新ui的前端是基于react的,并且准备完全分离前后端进行开发,打包方面可以考虑react-native或者其他更有利的框架;关于桌面端则考虑tauri和electron择一,个人偏好体积较小的前者,但这一点有待社区讨论。

@XcantloadX
Copy link
Author

确实采用 Web 方案对移动端的兼容性更好,p4a 已有对 Webview 的支持,可以很轻松的打包。至于 React Native,刚才看了一下,理论上可以整合到 p4a 中,不过需要比较多的修改与自定义。如果 React Native 相对 Webview 没有很大的性能优势,我还是偏向于采用简单的 Webview 方案。

对于兼容 u2,u2 的依赖中不兼容的应该只有 adbutils,因此我会先尝试为 adbutils 做一个兼容层,让 adbutils 底层调用 Shizuku 而不是 adb。这样不仅 u2 能正常使用, BAAS 中其他调用 adbutils 的代码也可以正常执行。

另外非常感谢作者能够配合修改 C++ 部分的代码。

@pur1fying
Copy link
Owner

有关ocr图像数据传递, 虽然共享内存无法使用, 但是ocr_server还提供了直接将图片附在http请求file中的方法, 另外我发现安卓直接运行c++编译后的程序十分困难, 最终应该会用ndk把ocr_server编译为so库使用

@MC-ALL
Copy link
Contributor

MC-ALL commented Oct 5, 2025

  1. 分辨率 2 实现不复杂,对着 scrcpy 抄应该非常简单。
  2. 根据 QA ,python 官方没有支持的安卓多进程,需要替代方案。psutils 库报错了,试试本地编译或者加载个 arm 的会不会有用。
  3. 我个人支持 web 套壳,所有平台都网页套壳还更简单。

@XcantloadX
Copy link
Author

  1. 根据 QA ,python 官方没有支持的安卓多进程,需要替代方案。psutils 库报错了,试试本地编译或者加载个 arm 的会不会有用。

多进程应该只有 OCR 部分用到,现在改用 so 库,所以现在不再需要多进程了。

psutil 只有 installer 和模拟器控制部分用到,这两个在 Android 上都不需要,因此 psutil 也不需要了,只要在代码里条件性 import 即可。

@pur1fying
Copy link
Owner

我成功使用ndk编译了onnxruntime 1.22.1 在x86_64 和 arm64-v8a 两个abi下的动态库, armeabi-v7a, x86 无法成功编译, 不知道影响大不大, 后续会测试动态库可用性

@pur1fying
Copy link
Owner

截屏2025-10-11 16 49 32

成功在android studio运行起来了 :)

@XcantloadX
Copy link
Author

等 C++ 部分准备好了我就开始研究怎么用 Python 调库🤗。


adb 部分,目前已经给 adbutils 的基本功能写了兼容层,足够支持 u2 setup,并且测试可以正常使用 u2。

测试了截图速度,为 ~0.6s,感觉有点慢。

DEBUG:urllib3.connectionpool:http://127.0.0.1:7912 "GET /screenshot/0 HTTP/1.1" 200 47456
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'sRGB' 41 1
DEBUG:PIL.PngImagePlugin:STREAM b'sBIT' 54 4
DEBUG:PIL.PngImagePlugin:b'sBIT' 54 4 (unknown)
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 70 8192
0.5641121864318848
DEBUG:urllib3.connectionpool:http://127.0.0.1:7912 "GET /screenshot/0 HTTP/1.1" 200 47315
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'sRGB' 41 1
DEBUG:PIL.PngImagePlugin:STREAM b'sBIT' 54 4
DEBUG:PIL.PngImagePlugin:b'sBIT' 54 4 (unknown)
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 70 8192
0.5275218486785889
DEBUG:urllib3.connectionpool:http://127.0.0.1:7912 "GET /screenshot/0 HTTP/1.1" 200 47315
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'sRGB' 41 1
DEBUG:PIL.PngImagePlugin:STREAM b'sBIT' 54 4
DEBUG:PIL.PngImagePlugin:b'sBIT' 54 4 (unknown)
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 70 8192
0.5749030113220215
DEBUG:urllib3.connectionpool:http://127.0.0.1:7912 "GET /screenshot/0 HTTP/1.1" 200 47405
DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
DEBUG:PIL.PngImagePlugin:STREAM b'sRGB' 41 1
DEBUG:PIL.PngImagePlugin:STREAM b'sBIT' 54 4
DEBUG:PIL.PngImagePlugin:b'sBIT' 54 4 (unknown)
DEBUG:PIL.PngImagePlugin:STREAM b'IDAT' 70 8192
0.6252896785736084

直接访问 RPC 大概在 0.5s 左右。

marble:/ $ time curl http://127.0.0.1:7912/screenshot/0 > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 48104  100 48104    0     0   104k      0 --:--:-- --:--:-- --:--:--  104k
    0m00.46s real     0m00.01s user     0m00.00s system
marble:/ $ time curl http://127.0.0.1:7912/screenshot/0 > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 48104  100 48104    0     0  95895      0 --:--:-- --:--:-- --:--:-- 95824
    0m00.51s real     0m00.00s user     0m00.01s system
marble:/ $ time curl http://127.0.0.1:7912/screenshot/0 > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 48104  100 48104    0     0   106k      0 --:--:-- --:--:-- --:--:--  106k
    0m00.45s real     0m00.01s user     0m00.00s system
marble:/ $

不过有个地方有点奇怪。如果我使用 baas 的 u2 initer,那么滑动和点击会无法正常使用。没有任何错误,但是也没有效果。

import uiautomator2 as u2
from core.device.uiautomator2_client import BAAS_U2_Initer
d = u2.connect('baas') # 'baas' 是模拟设备的 serial id
init = BAAS_U2_Initer(d._adb_device, logger)
init.install()

print('Swipe')
d.swipe(0.5, 0, 0.5, 0.7, 3)
print('Swipe End')

日志:

0.00s - Note: Debugging will proceed. Set PYDEVD_DISABLE_FILE_VALIDATION=1 to disable this validation.
2025-10-07 22:07:55 | ShizukuClient: Binding Shizuku user service...
2025-10-07 22:07:57 | ShizukuClient: Service connected
2025-10-07 22:08:01 | ShizukuClient: Executing command: getprop ro.build.version.sdk
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='35\n', stderr='')
2025-10-07 22:08:01 | ShizukuClient: Executing command: getprop ro.product.cpu.abi
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='arm64-v8a\n', stderr='')
2025-10-07 22:08:01 | ShizukuClient: Executing command: getprop ro.build.version.preview_sdk
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='0\n', stderr='')
2025-10-07 22:08:01 | ShizukuClient: Executing command: getprop ro.arch
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='\n', stderr='')
2025-10-07 22:08:01 | ShizukuClient: Executing command: getprop ro.product.cpu.abilist
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='arm64-v8a,armeabi-v7a,armeabi\n', stderr='')
2025-10-07 22:08:01 | uiautomator2 version: [ 2.16.23 ].
2025-10-07 22:08:01 | ShizukuClient: Executing command: pm path com.github.uiautomator
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='package:/data/app/~~5AnndMG_kFfvz0Yq7_R1hQ==/com.github.uiautomator-4p_JS83GbZlpHSeubbLDOQ==/base.apk\n', stderr='')
2025-10-07 22:08:01 | ShizukuClient: Executing command: dumpsys package com.github.uiautomator
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='Activity Resolver Table:\n  Non-Data Actions:\n      android.intent.action.MAIN:\n        9a5352 com.github.uiautomator/.MainActivity filter a84a623\n          Action: "android.intent.action.MAIN"\n          Action: "android.intent.action.VIEW"\n          Category: "android.intent.category.LAUNCHER"\n      android.intent.action.VIEW:\n        9a5352 com.github.uiautomator/.MainActivity filter a84a623\n          Action: "android.intent.action.MAIN"\n          Action: "android.intent.action.VIEW"\n          Category: "android.intent.category.LAUNCHER"\n      com.github.uiautomator.ACTION_IDENTIFY:\n        18f3b4 com.github.uiautomator/.IdentifyActivity filter 618cbdd\n          Action: "com.github.uiautomator.ACTION_IDENTIFY"\n          Category: "android.intent.category.DEFAULT"\n\nReceiver Resolver Table:\n  Non-Data Actions:\n      send.mock:\n        72b9720 com.github.uiautomator/.AdbBroadcastReceiver filter 85b48d9\n          Action: "send.mock"\n          Action: "stop.mock"\n      stop.mock:\n        72b9720 com.github.uiautomator/.AdbBroadcastReceiver filter 85b48d9\n          Action: "send.mock"\n          Action: "stop.mock"\n\nService Resolver Table:\n  Non-Data Actions:\n      android.intent.action.MAIN:\n        fe6529e com.github.uiautomator/.Service filter 671817f\n          Action: "android.intent.action.MAIN"\n          Action: "com.github.uiautomator.ACTION_START"\n          Action: "com.github.uiautomator.ACTION_STOP"\n          Category: "android.intent.category.LAUNCHER"\n          mPriority=999, mOrder=0, mHasStaticPartialTypes=false, mHasDynamicPartialTypes=false\n      com.github.uiautomator.ACTION_START:\n        fe6529e com.github.uiautomator/.Service filter 671817f\n          Action: "android.intent.action.MAIN"\n          Action: "com.github.uiautomator.ACTION_START"\n          Action: "com.github.uiautomator.ACTION_STOP"\n          Category: "android.intent.category.LAUNCHER"\n          mPriority=999, mOrder=0, mHasStaticPartialTypes=false, mHasDynamicPartialTypes=false\n      android.view.InputMethod:\n        b03054c com.github.uiautomator/.FastInputIME filter ae9995 permission android.permission.BIND_INPUT_METHOD\n          Action: "android.view.InputMethod"\n      com.github.uiautomator.ACTION_STOP:\n        fe6529e com.github.uiautomator/.Service filter 671817f\n          Action: "android.intent.action.MAIN"\n          Action: "com.github.uiautomator.ACTION_START"\n          Action: "com.github.uiautomator.ACTION_STOP"\n          Category: "android.intent.category.LAUNCHER"\n          mPriority=999, mOrder=0, mHasStaticPartialTypes=false, mHasDynamicPartialTypes=false\n\nDomain verification status:\n\nKey Set Manager:\n  [com.github.uiautomator]\n      Signing KeySets: 615\n\nPackages:\n  Package [com.github.uiautomator] (1865b89):\n    appId=10959\n    pkg=Package{b663db com.github.uiautomator}\n    codePath=/data/app/~~5AnndMG_kFfvz0Yq7_R1hQ==/com.github.uiautomator-4p_JS83GbZlpHSeubbLDOQ==\n    resourcePath=/data/app/~~5AnndMG_kFfvz0Yq7_R1hQ==/com.github.uiautomator-4p_JS83GbZlpHSeubbLDOQ==\n    legacyNativeLibraryDir=/data/app/~~5AnndMG_kFfvz0Yq7_R1hQ==/com.github.uiautomator-4p_JS83GbZlpHSeubbLDOQ==/lib\n    extractNativeLibs=true\n    primaryCpuAbi=null\n    secondaryCpuAbi=null\n    cpuAbiOverride=null\n    versionCode=2003003 minSdk=18 targetSdk=28\n    minExtensionVersions=[]\n    versionName=2.3.3\n    hiddenApiEnforcementPolicy=2\n    usesNonSdkApi=false\n    isMiuiPreinstall=false\n    splits=[base]\n    apkSigningVersion=2\n    flags=[ DEBUGGABLE HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]\n    privateFlags=[ PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING ]\n    forceQueryable=false\n    scannedAsStoppedSystemApp=false\n    supportsScreens=[small, medium, large, xlarge, resizeable, anyDensity]
2025-10-07 22:08:01 | ShizukuClient: Executing command: pm path com.github.uiautomator.test
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='package:/data/app/~~IC3unUF9DIgOY04RzDF4Jw==/com.github.uiautomator.test-HRgxNtA6nPHsrMeEmtlMpw==/base.apk\n', stderr='')
2025-10-07 22:08:01 | ShizukuClient: Executing command: dumpsys package com.github.uiautomator.test
2025-10-07 22:08:01 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='Domain verification status:\n\nKey Set Manager:\n  [com.github.uiautomator.test]\n      Signing KeySets: 615\n\nPackages:\n  Package [com.github.uiautomator.test] (50eb8b4):\n    appId=10960\n    pkg=Package{a3aebbb com.github.uiautomator.test}\n    codePath=/data/app/~~IC3unUF9DIgOY04RzDF4Jw==/com.github.uiautomator.test-HRgxNtA6nPHsrMeEmtlMpw==\n    resourcePath=/data/app/~~IC3unUF9DIgOY04RzDF4Jw==/com.github.uiautomator.test-HRgxNtA6nPHsrMeEmtlMpw==\n    legacyNativeLibraryDir=/data/app/~~IC3unUF9DIgOY04RzDF4Jw==/com.github.uiautomator.test-HRgxNtA6nPHsrMeEmtlMpw==/lib\n    extractNativeLibs=true\n    primaryCpuAbi=null\n    secondaryCpuAbi=null\n    cpuAbiOverride=null\n    versionCode=0 minSdk=18 targetSdk=28\n    minExtensionVersions=[]\n    versionName=null\n    hiddenApiEnforcementPolicy=2\n    usesNonSdkApi=false\n    isMiuiPreinstall=false\n    splits=[base]\n    apkSigningVersion=2\n    flags=[ DEBUGGABLE HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]\n    privateFlags=[ PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING ]\n    forceQueryable=false\n    scannedAsStoppedSystemApp=false\n    supportsScreens=[small, medium, large, xlarge, resizeable, anyDensity]\n    usesLibraries:\n      android.test.base\n      android.test.mock\n      android.test.runner\n    usesLibraryFiles:\n      /system/framework/android.test.base.jar\n      /system/framework/android.test.mock.jar\n      /system/framework/android.test.runner.jar\n    timeStamp=2025-10-07 22:00:11\n    lastUpdateTime=2025-10-07 22:00:13\n    installerPackageName=null\n    installerPackageUid=-1\n    initiatingPackageName=com.android.shell\n    originatingPackageName=null\n    packageSource=1\n    appMetadataFilePath=null\n    appMetadataSource=0\n    signatures=PackageSignatures{c8b88d8 version:2, signatures:[ae17cd86], past signatures:[]}\n    installPermissionsFixed=false\n    pkgFlags=[ DEBUGGABLE HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]\n    privatePkgFlags=[ PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING ]\n    apexModuleName=null\n    requested permissions:\n      android.permission.POST_NOTIFICATIONS\n    User 0: ceDataInode=1998871 deDataInode=1998877 installed=true hidden=false suspended=false distractionFlags=0 stopped=true notLaunched=true enabled=0 instant=false virtual=false quarantined=false\n      installReason=0\n      dataDir=/data/user/0/com.github.uiautomator.test\n      firstInstallTime=2025-10-07 22:00:13\n      uninstallReason=0\n      overlay paths:\n        /product/overlay/GestureLineOverlay.apk\n        /data/resource-cache/com.android.systemui-neutral-S9Iu.frro\n        /data/resource-cache/com.android.systemui-accent-6x4w.frro\n        /data/resource-cache/com.android.systemui-dynamic-xrGX.frro\n      legacy overlay paths:\n        /product/overlay/GestureLineOverlay.apk\n      runtime permissions:\n        android.permission.POST_NOTIFICATIONS: granted=false, flags=[ USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED]\n    User 999: ceDataInode=0 deDataInode=0 installed=false hidden=false suspended=false distractionFlags=0 stopped=true notLaunched=true enabled=0 instant=false virtual=false quarantined=false\n      installReason=0\n      dataDir=null\n      firstInstallTime=1970-01-01 08:00:00\n      uninstallReason=0\n      runtime permissions:\n\nQueries:\n  system apps queryable: false\n  queries via forceQueryable:\n  queries via package name:\n    com.github.uiautomator:\n      com.github.uiautomator.test\n    com.github.uiautomator.test:\n      com.github.uiautomator\n  queries via component:\n  queryable via interaction:\n    User 0:\n    User 999:\n  queryable via uses-library:\n\nDexopt state:\n  [com.github.uiautomator.test]\n   
2025-10-07 22:08:01 | apk-debug package-info: [ {'package_name': 'com.github.uiautomator', 'version_name': '2.3.3', 'version_code': 2003003, 'flags': ['DEBUGGABLE', 'HAS_CODE', 'ALLOW_CLEAR_USER_DATA', 'ALLOW_BACKUP'], 'first_install_time': datetime.datetime(2025, 10, 7, 22, 0, 10), 'last_update_time': datetime.datetime(2025, 10, 7, 22, 0, 10), 'signature': 'ae17cd86], past signatures:[', 'path': '/data/app/~~5AnndMG_kFfvz0Yq7_R1hQ==/com.github.uiautomator-4p_JS83GbZlpHSeubbLDOQ==/base.apk', 'sub_apk_paths': []} ].
2025-10-07 22:08:01 | apk-debug-test package-info: [ {'package_name': 'com.github.uiautomator.test', 'version_name': None, 'version_code': 0, 'flags': ['DEBUGGABLE', 'HAS_CODE', 'ALLOW_CLEAR_USER_DATA', 'ALLOW_BACKUP'], 'first_install_time': datetime.datetime(2025, 10, 7, 22, 0, 13), 'last_update_time': datetime.datetime(2025, 10, 7, 22, 0, 13), 'signature': 'ae17cd86], past signatures:[', 'path': '/data/app/~~IC3unUF9DIgOY04RzDF4Jw==/com.github.uiautomator.test-HRgxNtA6nPHsrMeEmtlMpw==/base.apk', 'sub_apk_paths': []} ].
2025-10-07 22:08:01 | Already installed com.github.uiautomator apks
2025-10-07 22:08:01 | Stop atx-agent.
2025-10-07 22:08:01 | ShizukuClient: Executing command: /data/local/tmp/atx-agent server --stop
2025-10-07 22:08:02 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='', stderr='time="2025-10-07T22:08:01+08:00" level=info msg="stop server self"\ntime="2025-10-07T22:08:01+08:00" level=info msg="wait server stopped"\n')
2025-10-07 22:08:02 | ShizukuClient: Executing command: /data/local/tmp/atx-agent version
2025-10-07 22:08:02 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='', stderr='0.10.0\n')
2025-10-07 22:08:02 | Real version: [0, 10, 0], Expect version:[0, 10, 0].
2025-10-07 22:08:02 | Start atx-agent.
2025-10-07 22:08:02 | ShizukuClient: Executing command: /data/local/tmp/atx-agent server --nouia -d --addr 127.0.0.1:7912
2025-10-07 22:08:02 | ShizukuClient: Result: CommandResult(exitCode=0, stdout='', stderr='time="2025-10-07T22:08:02+08:00" level=info msg="run atx-agent in background"\ntime="2025-10-07T22:08:02+08:00" level=info msg="atx-agent listening on 127.0.0.1:7912"\n')
2025-10-07 22:08:02 | Check atx-agent version
2025-10-07 22:08:02 | Forward: local:tcp:7912 -> remote:tcp:7912
2025-10-07 22:08:02 | Forward: local:tcp:7912 -> remote:tcp:7912
2025-10-07 22:08:02 | atx-agent version [ 0.10.0 ].
2025-10-07 22:08:02 | device wlan ip: [ wlan0 have no ip address ].
Swipe
Swipe End
Python for android ended.

如果改用 u2 自己的,那么就没有问题。

import uiautomator2 as u2
from core.device.uiautomator2_client import BAAS_U2_Initer
d = u2.connect('baas')
d._force_reset_uiautomator_v2()

print('Swipe')
d.swipe(0.5, 0, 0.5, 0.7, 3)
print('Swipe End')

目前还没搞明白是什么原因。不知道是不是调用方式不对?

@pur1fying
Copy link
Owner

我已经成功在原始的C++代码上使用NDK编译构建了jni库
image
这个库已经被验证可以通过 jni 被 java 构建的Android项目使用, 也就是说除了你提到的通过python调库来使用ocr, 或许可以做一个apk来启用ocr服务, 可以考虑一下用哪个方式 @XcantloadX

相关代码在此分支

@XcantloadX
Copy link
Author

这个库已经被验证可以通过 jni 被 java 构建的Android项目使用, 也就是说除了你提到的通过python调库来使用ocr, 或许可以做一个apk来启用ocr服务, 可以考虑一下用哪个方式

应该没必要单独一个 apk,这样分发和管理都不太方便。一个 app 内可以同时包含 activity 与 service,把 ocr 做成 service,service 能以独立进程运行,这样一个 app 也可以把主体和 ocr 隔离执行。

service 有两种:

  1. started service,需要手动管理生命周期。如果没人停止或者自己不停止,就会一直运行。当本体崩溃或者被用户主动停止时,service 会继续在后台运行。
  2. bound service,由系统自动管理生命周期。连接时如果未启动,系统会自动启动 service;同时当最后一个连接销毁后,系统会自动停止 service。但是只能使用 Binder 进行通信,不能用现在已有的 HTTP 方式。

现在算是有三种方案:

  1. 非隔离运行,本体与 ocr 在同一进程内运行。
    • 绑定:利用 pybind11 等为 ocr server 生成 Python 绑定
    • 通信:Python <--dlopen--> C++,无需经过任何 Android 部分
  2. 隔离运行,用 started service。
    • 绑定:jni
      • 用 Java 编写一个 service,简单控制 C++ 端的 HTTP 服务器的启停
      • activity 内需要少量代码控制 service 的启停
      • 服务端需要某种机制自动退出,比如超过一定时间无连接,或者心跳停止
    • 通信:Python <--HTTP--> C++,同样无需经过任何 Android 部分
  3. 隔离运行,用 bound service
    • 绑定:jni
      • 用 Java 编写一个 service,需要暴露所有 Python 侧需要用到的方法,并调用对应的 C++ 部分
      • Python 部分需要调用 Android api 以绑定 service
    • 通信:Python <--jni--> Java <--Binder--> Java <--jni--> C++,需要 Android 部分介入

1 无论如何都是最简单的。可以考虑用 1 或 2,3 实现起来有点复杂。

如果需要内存共享

  1. 非隔离运行:应该不需要
  2. 走 Binder:使用 ashmem + mmap
  3. 走 HTTP:由于 ashmem 返回的文件描述符只能通过 binder 传递,因此这里用不了 ashmem。只能真实文件 + mmap

@pur1fying
Copy link
Owner

抱歉最近有事没有空关注此pr, 后续会跟进

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants