feat: 设备已装应用查看(含应用名/版本号)
- adb_helper.list_installed_apps:pm list + pm dump 取包名/versionCode/versionName/APK路径 - 应用名解析:常见应用映射 + 小APK流式pyaxmlparser解析(并行+缓存)+ 包名兜底 - web 新增 GET /api/devices/<serial>/apps
This commit is contained in:
@@ -154,6 +154,148 @@ def screenshot(serial, timeout=5):
|
||||
return False, str(e)
|
||||
|
||||
|
||||
# 常见应用名映射:快速识别大/重要应用,避免拉取 APK 解析(慢)
|
||||
COMMON_APPS = {
|
||||
"com.ss.android.ugc.aweme": "抖音",
|
||||
"com.ss.android.ugc.aweme.lite": "抖音极速版",
|
||||
"com.smile.gifmaker": "快手",
|
||||
"com.kuaishou.nebula": "快手极速版",
|
||||
"com.tencent.mm": "微信",
|
||||
"com.tencent.mobileqq": "QQ",
|
||||
"com.tencent.qqlive": "腾讯视频",
|
||||
"com.eg.android.AlipayGphone": "支付宝",
|
||||
"com.taobao.taobao": "淘宝",
|
||||
"com.tmall.wireless": "天猫",
|
||||
"com.jingdong.app.mall": "京东",
|
||||
"com.xunmeng.pinduoduo": "拼多多",
|
||||
"com.ss.android.article.news": "今日头条",
|
||||
"com.zhihu.android": "知乎",
|
||||
"com.miui.home": "桌面",
|
||||
"com.android.settings": "设置",
|
||||
"com.android.systemui": "系统",
|
||||
"com.github.uiautomator": "ATX",
|
||||
"com.github.uiautomator.test": "ATX测试",
|
||||
"jp.co.cyberagent.stf": "STF服务",
|
||||
"moe.shizuku.privileged.api": "Shizuku",
|
||||
"moe.shizuku.api": "Shizuku",
|
||||
"io.tailscale.android": "Tailscale",
|
||||
"com.immomo.momo": "陌陌",
|
||||
"com.xingin.xhs": "小红书",
|
||||
}
|
||||
# 解析应用名时,超过该大小的 APK 不拉取(太慢),用映射或包名兜底
|
||||
_LABEL_PARSE_MAX_BYTES = 5 * 1024 * 1024 # 5MB
|
||||
# 应用名缓存:(serial, package) -> label
|
||||
_APP_LABEL_CACHE = {}
|
||||
|
||||
|
||||
def list_installed_apps(serial, timeout=40):
|
||||
"""获取设备上已安装应用列表:应用名 + 包名 + versionCode + versionName + APK路径。
|
||||
|
||||
- `pm list packages --show-versioncode -f` 取包名/版本码/路径(快)
|
||||
- `pm dump packages` 取人性化 versionName(实测 ~0.8s)
|
||||
- 应用名 label:常见应用走映射;用户安装且较小(<=10MB)的未知应用流式拉 APK 用
|
||||
pyaxmlparser 解析(带缓存),其余用包名兜底。
|
||||
返回 (ok, apps) 或 (False, 错误消息)。
|
||||
"""
|
||||
if not serial:
|
||||
return False, "缺少 serial"
|
||||
try:
|
||||
# 1. 包名 + versionCode + APK 路径
|
||||
r1 = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "shell", "pm", "list", "packages",
|
||||
"--show-versioncode", "-f"],
|
||||
capture_output=True, timeout=timeout)
|
||||
apps = {}
|
||||
for line in (r1.stdout or b"").decode("utf-8", errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
m = re.match(r'package:(.+)=(\S+) versionCode:(\d+)', line)
|
||||
if m:
|
||||
apps[m.group(2)] = {"package": m.group(2), "path": m.group(1),
|
||||
"version_code": int(m.group(3)), "version_name": "",
|
||||
"label": COMMON_APPS.get(m.group(2), "")}
|
||||
else:
|
||||
m2 = re.match(r'package:(.+)=(\S+)', line)
|
||||
if m2:
|
||||
apps[m2.group(2)] = {"package": m2.group(2), "path": m2.group(1),
|
||||
"version_code": 0, "version_name": "",
|
||||
"label": COMMON_APPS.get(m2.group(2), "")}
|
||||
# 2. 人性化版本号 versionName
|
||||
try:
|
||||
r2 = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "shell", "pm", "dump", "packages"],
|
||||
capture_output=True, timeout=timeout)
|
||||
out2 = (r2.stdout or b"").decode("utf-8", errors="replace")
|
||||
for m in re.finditer(r'Package \[([^\]]+)\]', out2):
|
||||
pkg = m.group(1)
|
||||
if pkg not in apps:
|
||||
continue
|
||||
nxt = out2.find("Package [", m.end())
|
||||
seg = out2[m.end():nxt if nxt != -1 else len(out2)]
|
||||
vm = re.search(r'versionName=(\S+)', seg)
|
||||
if vm:
|
||||
apps[pkg]["version_name"] = vm.group(1)
|
||||
except Exception:
|
||||
pass # versionName 拿不到不阻塞
|
||||
# 3. 应用名 label:用户安装且未映射的小 APK,流式拉取解析真实名
|
||||
_resolve_app_labels(serial, apps, timeout)
|
||||
return True, sorted(apps.values(), key=lambda a: a["package"])
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "查询超时"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _resolve_app_labels(serial, apps, timeout):
|
||||
"""并行解析用户安装小应用的显示名 label(带缓存)。失败/超时静默跳过。"""
|
||||
import io as _io
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
targets = [(pkg, a["path"]) for pkg, a in apps.items()
|
||||
if a["path"].startswith("/data/") and not a["label"]
|
||||
and (serial, pkg) not in _APP_LABEL_CACHE]
|
||||
if not targets:
|
||||
return
|
||||
# 用 ls -l 一次拿所有候选 APK 大小,跳过过大的(流式拉大 APK 太慢)
|
||||
sizes = {}
|
||||
try:
|
||||
rsz = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "shell", "ls", "-l"] + [p for _, p in targets],
|
||||
capture_output=True, timeout=min(timeout, 15))
|
||||
for line in (rsz.stdout or b"").decode("utf-8", errors="replace").splitlines():
|
||||
parts = line.split()
|
||||
# ls -l(toybox): 权限 链接 属主 属组 大小 日期 时间 路径 = 8 字段
|
||||
if len(parts) >= 6:
|
||||
try:
|
||||
sizes[parts[-1]] = int(parts[4])
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
small = [(pkg, path) for pkg, path in targets if sizes.get(path, 0) <= _LABEL_PARSE_MAX_BYTES]
|
||||
|
||||
def _fetch(pkg_path):
|
||||
pkg, path = pkg_path
|
||||
label = ""
|
||||
try:
|
||||
rp = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "exec-out", "cat", path],
|
||||
capture_output=True, timeout=min(timeout, 8))
|
||||
from pyaxmlparser import APK as _APKParser
|
||||
label = (_APKParser(_io.BytesIO(rp.stdout)).get_app_name() or "").strip()
|
||||
except Exception:
|
||||
label = ""
|
||||
_APP_LABEL_CACHE[(serial, pkg)] = label
|
||||
return pkg, label
|
||||
|
||||
if small:
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
for pkg, label in pool.map(_fetch, small):
|
||||
if pkg in apps:
|
||||
apps[pkg]["label"] = label
|
||||
except Exception:
|
||||
pass # label 解析失败不影响主流程
|
||||
|
||||
|
||||
def identify_device(serial, duration=3):
|
||||
"""让设备响铃以供识别(直连 serial,不经过 STF)。
|
||||
|
||||
|
||||
+11
-1
@@ -29,7 +29,7 @@ from core.task_manager import TaskManager
|
||||
from core.logger import get_logger, _LOG_DIR, _MODULE_FILES
|
||||
from core.models import db, init_db, User, DeviceGroup, TaskJob, CustomAction
|
||||
from core.apk_manager import ApkManager
|
||||
from core.adb_helper import screenshot
|
||||
from core.adb_helper import screenshot, list_installed_apps
|
||||
from core import uiauto_helper
|
||||
from tasks import list_task_types, get_task_class
|
||||
|
||||
@@ -370,6 +370,16 @@ def api_device_screenshot():
|
||||
return jsonify({"ok": False, "error": data}), 500
|
||||
|
||||
|
||||
@app.route("/api/devices/<serial>/apps")
|
||||
@login_required
|
||||
def api_device_apps(serial):
|
||||
"""获取指定设备上已安装的应用列表(包名 + versionCode + versionName + 路径)。"""
|
||||
ok, data = list_installed_apps(serial)
|
||||
if ok:
|
||||
return jsonify({"ok": True, "apps": data})
|
||||
return jsonify({"ok": False, "error": data}), 500
|
||||
|
||||
|
||||
# ================== 自定义动作(步骤打包) ==================
|
||||
@app.route("/api/custom_actions")
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user