damon-devtools 0.1.0__tar.gz → 0.2.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.
Files changed (30) hide show
  1. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/PKG-INFO +1 -1
  2. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/damon_devtools.egg-info/PKG-INFO +1 -1
  3. damon_devtools-0.2.0/devtools/tools/apkpull/core.py +303 -0
  4. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/apkpull/ui.py +27 -14
  5. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/pyproject.toml +1 -1
  6. damon_devtools-0.1.0/devtools/tools/apkpull/core.py +0 -123
  7. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/README.md +0 -0
  8. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/damon_devtools.egg-info/SOURCES.txt +0 -0
  9. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/damon_devtools.egg-info/dependency_links.txt +0 -0
  10. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/damon_devtools.egg-info/entry_points.txt +0 -0
  11. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/damon_devtools.egg-info/requires.txt +0 -0
  12. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/damon_devtools.egg-info/top_level.txt +0 -0
  13. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/__init__.py +0 -0
  14. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/__main__.py +0 -0
  15. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/launcher.py +0 -0
  16. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/__init__.py +0 -0
  17. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/apkpull/__init__.py +0 -0
  18. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/apkpull/__main__.py +0 -0
  19. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/apkpull/models.py +0 -0
  20. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/portkill/__init__.py +0 -0
  21. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/portkill/__main__.py +0 -0
  22. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/portkill/core.py +0 -0
  23. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/portkill/models.py +0 -0
  24. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/portkill/ui.py +0 -0
  25. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/removebg/__init__.py +0 -0
  26. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/removebg/__main__.py +0 -0
  27. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/removebg/cli.py +0 -0
  28. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/tools/removebg/core.py +0 -0
  29. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/devtools/types.py +0 -0
  30. {damon_devtools-0.1.0 → damon_devtools-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: damon-devtools
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Developer CLI tools — portkill, removebg, apkpull
5
5
  Author: damon
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: damon-devtools
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Developer CLI tools — portkill, removebg, apkpull
5
5
  Author: damon
6
6
  License: MIT
@@ -0,0 +1,303 @@
1
+ import os
2
+ import subprocess
3
+ import re
4
+ import tempfile
5
+ import urllib.request
6
+ import json
7
+
8
+ # Local aapt2 binary directory
9
+ AAPT2_LOCAL_DIR = os.path.join(
10
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
11
+ "deps", "android-tools", "build"
12
+ )
13
+
14
+ # Remote aapt2 download base URL (android-11.0.0_r33)
15
+ AAPT2_REMOTE_URL = "https://github.com/JonForShort/android-tools/raw/refs/heads/master/build/android-11.0.0_r33/aapt2/{arch}/bin/aapt2"
16
+
17
+ # Device-side aapt2 path
18
+ AAPT2_DEVICE_PATH = "/data/local/tmp/aapt2"
19
+
20
+ # App name cache
21
+ LABEL_CACHE_FILE = os.path.expanduser("~/.devtools/cache/apkpull/label_cache.json")
22
+
23
+ def load_label_cache() -> dict[str, str]:
24
+ """Load cached app labels from file"""
25
+ if os.path.isfile(LABEL_CACHE_FILE):
26
+ try:
27
+ with open(LABEL_CACHE_FILE, "r", encoding="utf-8") as f:
28
+ return json.load(f)
29
+ except Exception:
30
+ return {}
31
+ return {}
32
+
33
+ def save_label_cache(cache: dict[str, str]) -> None:
34
+ """Save app labels cache to file"""
35
+ try:
36
+ os.makedirs(os.path.dirname(LABEL_CACHE_FILE), exist_ok=True)
37
+ with open(LABEL_CACHE_FILE, "w", encoding="utf-8") as f:
38
+ json.dump(cache, f, ensure_ascii=False, indent=2)
39
+ except Exception:
40
+ pass
41
+
42
+ # Global cache
43
+ _label_cache: dict[str, str] | None = None
44
+
45
+ def get_label_cache() -> dict[str, str]:
46
+ """Get or initialize the global label cache"""
47
+ global _label_cache
48
+ if _label_cache is None:
49
+ _label_cache = load_label_cache()
50
+ return _label_cache
51
+
52
+ def check_adb() -> bool:
53
+ """检查 ADB 是否可用"""
54
+ try:
55
+ subprocess.run(["adb", "version"], capture_output=True, check=True)
56
+ return True
57
+ except (subprocess.CalledProcessError, FileNotFoundError):
58
+ return False
59
+
60
+ def get_device_serial() -> str | None:
61
+ """获取第一个已连接的设备序列号,多设备时返回第一个"""
62
+ try:
63
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
64
+ lines = result.stdout.strip().split("\n")
65
+ for line in lines[1:]:
66
+ if line.strip() and not line.startswith("*"):
67
+ parts = line.split()
68
+ if len(parts) >= 2 and parts[1] == "device":
69
+ return parts[0]
70
+ return None
71
+ except (subprocess.CalledProcessError, FileNotFoundError):
72
+ return None
73
+
74
+ def check_device() -> bool:
75
+ """检查设备是否连接"""
76
+ return get_device_serial() is not None
77
+
78
+ def adb_cmd(args: list[str], serial: str | None = None) -> list[str]:
79
+ """构建带 -s 参数的 adb 命令"""
80
+ if serial:
81
+ return ["adb", "-s", serial] + args
82
+ return ["adb"] + args
83
+
84
+ def check_aapt2_on_device() -> bool:
85
+ """Check if /data/local/tmp/aapt2 exists on device"""
86
+ serial = get_device_serial()
87
+ try:
88
+ result = subprocess.run(
89
+ adb_cmd(["shell", "test", "-f", AAPT2_DEVICE_PATH], serial),
90
+ capture_output=True, check=False
91
+ )
92
+ return result.returncode == 0
93
+ except (subprocess.CalledProcessError, FileNotFoundError):
94
+ return False
95
+
96
+ def detect_device_arch() -> str | None:
97
+ """Get device CPU architecture via getprop"""
98
+ serial = get_device_serial()
99
+ try:
100
+ result = subprocess.run(
101
+ adb_cmd(["shell", "getprop", "ro.product.cpu.abi"], serial),
102
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
103
+ )
104
+ arch = result.stdout.strip()
105
+ return arch if arch else None
106
+ except (subprocess.CalledProcessError, FileNotFoundError):
107
+ return None
108
+
109
+ # Local aapt2 dir name -> device arch values
110
+ ARCH_MAP = {
111
+ "arm64-v8a": ["arm64-v8a", "aarch64", "arm64"],
112
+ "armeabi-v7a": ["armeabi-v7a", "armeabi", "arm"],
113
+ "x86": ["x86", "i386", "i686"],
114
+ "x86_64": ["x86_64", "x64", "amd64"],
115
+ }
116
+
117
+ def find_local_aapt2(arch: str) -> str | None:
118
+ """Find local aapt2 binary matching device arch"""
119
+ if not os.path.isdir(AAPT2_LOCAL_DIR):
120
+ return None
121
+ for version in sorted(os.listdir(AAPT2_LOCAL_DIR), reverse=True):
122
+ version_path = os.path.join(AAPT2_LOCAL_DIR, version, "aapt2")
123
+ if not os.path.isdir(version_path):
124
+ continue
125
+ for arch_dir, aliases in ARCH_MAP.items():
126
+ if arch in aliases:
127
+ aapt2_path = os.path.join(version_path, arch_dir, "bin", "aapt2")
128
+ if os.path.isfile(aapt2_path):
129
+ return aapt2_path
130
+ return None
131
+
132
+ def download_aapt2(arch: str) -> str | None:
133
+ """Download aapt2 from GitHub and cache locally. Returns local path or None on failure."""
134
+ # Map device arch to download arch
135
+ arch_to_url_arch = {
136
+ "arm64-v8a": "arm64-v8a",
137
+ "aarch64": "arm64-v8a",
138
+ "arm64": "arm64-v8a",
139
+ "armeabi-v7a": "armeabi-v7a",
140
+ "armeabi": "armeabi-v7a",
141
+ "arm": "armeabi-v7a",
142
+ "x86": "x86",
143
+ "i386": "x86",
144
+ "i686": "x86",
145
+ "x86_64": "x86_64",
146
+ "x64": "x86_64",
147
+ "amd64": "x86_64",
148
+ }
149
+ url_arch = arch_to_url_arch.get(arch, arch)
150
+ url = AAPT2_REMOTE_URL.format(arch=url_arch)
151
+
152
+ # Cache path: AAPT2_LOCAL_DIR/android-11.0.0_r33/aapt2/{arch}/bin/aapt2
153
+ cache_dir = os.path.join(AAPT2_LOCAL_DIR, "android-11.0.0_r33", "aapt2", url_arch, "bin")
154
+ cache_path = os.path.join(cache_dir, "aapt2")
155
+
156
+ if os.path.isfile(cache_path):
157
+ return cache_path
158
+
159
+ try:
160
+ os.makedirs(cache_dir, exist_ok=True)
161
+ print(f"正在下载 aapt2 ({arch})...")
162
+ urllib.request.urlretrieve(url, cache_path)
163
+ # Make executable
164
+ os.chmod(cache_path, 0o755)
165
+ return cache_path
166
+ except Exception as e:
167
+ print(f"下载失败: {e}")
168
+ return None
169
+
170
+ def ensure_aapt2_on_device() -> bool:
171
+ """Push local aapt2 to device if missing; return success"""
172
+ if check_aapt2_on_device():
173
+ return True
174
+
175
+ serial = get_device_serial()
176
+ arch = detect_device_arch()
177
+ if not arch:
178
+ print("警告: 无法获取设备架构,使用 arm64-v8a")
179
+ arch = "arm64-v8a"
180
+
181
+ local_aapt2 = find_local_aapt2(arch)
182
+ if not local_aapt2:
183
+ print(f"本地未找到 aapt2 ({arch}),尝试下载...")
184
+ local_aapt2 = download_aapt2(arch)
185
+ if not local_aapt2:
186
+ print(f"警告: 下载失败,回退到 pm dump 方式")
187
+ return False
188
+
189
+ try:
190
+ print(f"正在推送 aapt2 ({arch}) 到设备...")
191
+ subprocess.run(
192
+ adb_cmd(["push", local_aapt2, AAPT2_DEVICE_PATH], serial),
193
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
194
+ )
195
+ subprocess.run(
196
+ adb_cmd(["shell", "chmod", "0755", AAPT2_DEVICE_PATH], serial),
197
+ capture_output=True, check=True
198
+ )
199
+ return True
200
+ except (subprocess.CalledProcessError, FileNotFoundError):
201
+ return False
202
+
203
+ def scan_packages() -> list[str]:
204
+ """执行 adb shell pm list packages -3,返回包名列表"""
205
+ serial = get_device_serial()
206
+ result = subprocess.run(
207
+ adb_cmd(["shell", "pm", "list", "packages", "-3"], serial),
208
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
209
+ )
210
+ pkgs = []
211
+ for line in result.stdout.strip().split("\n"):
212
+ line = line.strip()
213
+ if line.startswith("package:"):
214
+ pkgs.append(line[8:])
215
+ return pkgs
216
+
217
+ def get_apk_path(pkg: str) -> str | None:
218
+ """执行 pm path,返回 APK 物理路径"""
219
+ serial = get_device_serial()
220
+ try:
221
+ result = subprocess.run(
222
+ adb_cmd(["shell", "pm", "path", pkg], serial),
223
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
224
+ )
225
+ for line in result.stdout.strip().split("\n"):
226
+ if line.startswith("package:"):
227
+ return line[8:].strip()
228
+ return None
229
+ except (subprocess.CalledProcessError, FileNotFoundError):
230
+ return None
231
+
232
+ def get_app_name_pm_dump(pkg: str) -> str:
233
+ """通过设备端 pm dump 提取 application label(失败时回退包名)"""
234
+ serial = get_device_serial()
235
+ try:
236
+ result = subprocess.run(
237
+ adb_cmd(["shell", "pm", "dump", pkg], serial),
238
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
239
+ )
240
+ if result.returncode != 0:
241
+ return pkg
242
+ for line in result.stdout.splitlines():
243
+ # application-label:'Facebook'
244
+ # application-label-zh-CN:'微信'
245
+ m = re.search(r"application-label(?:-[^:]+)?:'(.+?)'", line)
246
+ if m:
247
+ return m.group(1)
248
+ return pkg
249
+ except Exception:
250
+ return pkg
251
+
252
+ def get_app_name_aapt2(pkg: str) -> str | None:
253
+ """Use device-side aapt2 dump badging to get app label. Returns None on failure."""
254
+ apk_path = get_apk_path(pkg)
255
+ if not apk_path:
256
+ return None
257
+ serial = get_device_serial()
258
+ try:
259
+ cmd = f'/data/local/tmp/aapt2 dump badging "{apk_path}"'
260
+ result = subprocess.run(
261
+ adb_cmd(["shell", cmd], serial),
262
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
263
+ )
264
+ if result.returncode != 0:
265
+ return None
266
+ for line in result.stdout.splitlines():
267
+ m = re.search(r"^application-label:'(.+?)'", line)
268
+ if m:
269
+ return m.group(1)
270
+ return None
271
+ except (subprocess.CalledProcessError, FileNotFoundError):
272
+ return None
273
+
274
+ def get_app_name(pkg: str) -> str:
275
+ """Get app label: check cache first, then fetch and cache"""
276
+ cache = get_label_cache()
277
+ if pkg in cache:
278
+ return cache[pkg]
279
+
280
+ label = get_app_name_aapt2(pkg)
281
+ if not label:
282
+ label = get_app_name_pm_dump(pkg)
283
+
284
+ cache[pkg] = label
285
+ save_label_cache(cache)
286
+ return label
287
+
288
+ def sanitize_filename(name: str) -> str:
289
+ """Sanitize filename for cross-platform compatibility."""
290
+ sanitized = re.sub(r'[\\/:*?"<>|]+', "_", name).strip().rstrip(".")
291
+ return sanitized or "unknown"
292
+
293
+ def pull_apk(apk_path: str, dest_path: str) -> bool:
294
+ """执行 adb pull,拉取 APK 到目标文件路径"""
295
+ serial = get_device_serial()
296
+ try:
297
+ subprocess.run(
298
+ adb_cmd(["pull", apk_path, dest_path], serial),
299
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
300
+ )
301
+ return True
302
+ except (subprocess.CalledProcessError, FileNotFoundError):
303
+ return False
@@ -3,12 +3,13 @@ if sys.platform == "win32":
3
3
  sys.stdout.reconfigure(encoding="utf-8")
4
4
 
5
5
  import os
6
+ import argparse
6
7
  import threading
7
8
  from devtools.tools.apkpull.models import PackageInfo
8
9
 
9
10
  def scan_packages_with_labels() -> list[PackageInfo]:
10
11
  """Scan packages and fetch labels in parallel threads (max 10 concurrent)"""
11
- from devtools.tools.apkpull.core import scan_packages, check_adb, check_device, get_app_name
12
+ from devtools.tools.apkpull.core import scan_packages, check_adb, check_device, get_app_name, ensure_aapt2_on_device
12
13
 
13
14
  if not check_adb():
14
15
  print("错误: 未找到 adb 命令,请确保 Android SDK platform-tools 已安装并加入 PATH")
@@ -17,6 +18,7 @@ def scan_packages_with_labels() -> list[PackageInfo]:
17
18
  print("错误: 未检测到设备,请确保手机已通过 USB 连接并开启了 USB 调试模式")
18
19
  return []
19
20
 
21
+ ensure_aapt2_on_device()
20
22
  print("正在获取应用列表...")
21
23
  pkgs = scan_packages()
22
24
  if not pkgs:
@@ -53,12 +55,12 @@ def scan_packages_with_labels() -> list[PackageInfo]:
53
55
 
54
56
  def show_packages(packages: list[PackageInfo], short: bool = True) -> None:
55
57
  print()
56
- print(f"{'#':<5} {'应用名':<30} {'包名'}")
58
+ print(f"{'#':<5} {'包名':<50} {'应用名'}")
57
59
  print("-" * 90)
58
60
  for i, p in enumerate(packages):
59
- label = p.label[:28] + ".." if short and len(p.label) > 30 else p.label
60
- pkg = p.pkg[:50] if short and len(p.pkg) > 50 else p.pkg
61
- print(f"{i+1:<5} {label:<30} {pkg}")
61
+ pkg = p.pkg[:48] + ".." if short and len(p.pkg) > 50 else p.pkg
62
+ label = p.label[:38] + ".." if short and len(p.label) > 40 else p.label
63
+ print(f"{i+1:<5} {pkg:<50} {label}")
62
64
 
63
65
  def select_packages(packages: list[PackageInfo], user_input: str) -> list[PackageInfo]:
64
66
  """Parse user input: keyword search or numeric selection (1,3-5, 1,3,5)"""
@@ -108,17 +110,26 @@ def run(args: list[str] | None = None):
108
110
  if args is None:
109
111
  args = []
110
112
 
111
- # Determine destination directory
112
- if args:
113
- dest_dir = args[0]
114
- else:
115
- dest_dir = os.getcwd()
113
+ parser = argparse.ArgumentParser(prog="apkpull", description="Pull APK from Android device")
114
+ parser.add_argument("dest_dir", nargs="?", default=os.getcwd(), help="目标目录 (默认当前目录)")
115
+ parser.add_argument("-s", "--search", metavar="KEYWORD", help="模糊搜索应用名/包名并退出")
116
+
117
+ parsed = parser.parse_args(args)
116
118
 
117
119
  packages = scan_packages_with_labels()
118
120
  if not packages:
119
121
  return
120
122
 
121
- show_packages(packages)
123
+ # Fuzzy search mode: pre-filter packages and enter selection loop
124
+ if parsed.search:
125
+ keyword = parsed.search.lower()
126
+ packages = [p for p in packages if keyword in p.label.lower() or keyword in p.pkg.lower()]
127
+ if not packages:
128
+ print(f"未找到包含 '{parsed.search}' 的应用")
129
+ return
130
+ show_packages(packages)
131
+ else:
132
+ show_packages(packages)
122
133
 
123
134
  while True:
124
135
  print()
@@ -142,12 +153,12 @@ def run(args: list[str] | None = None):
142
153
 
143
154
  show_packages(selected, short=False)
144
155
 
145
- if not confirm_pull(selected, dest_dir):
156
+ if not confirm_pull(selected, parsed.dest_dir):
146
157
  print("取消拉取")
147
158
  continue
148
159
 
149
160
  # Pull APKs
150
- from devtools.tools.apkpull.core import get_apk_path, pull_apk
161
+ from devtools.tools.apkpull.core import get_apk_path, pull_apk, sanitize_filename
151
162
  succeeded: list[str] = []
152
163
  failed: list[tuple] = []
153
164
 
@@ -156,7 +167,9 @@ def run(args: list[str] | None = None):
156
167
  if not apk_path:
157
168
  failed.append((p.label, "未找到 APK 路径"))
158
169
  continue
159
- if pull_apk(apk_path, dest_dir):
170
+ file_base = p.label if p.label and p.label != p.pkg else p.pkg
171
+ dest_apk = os.path.join(parsed.dest_dir, f"{sanitize_filename(file_base)}.apk")
172
+ if pull_apk(apk_path, dest_apk):
160
173
  succeeded.append(p.label)
161
174
  else:
162
175
  failed.append((p.label, "拉取失败"))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "damon-devtools"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Developer CLI tools — portkill, removebg, apkpull"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,123 +0,0 @@
1
- import subprocess
2
- import re
3
- import os
4
- import tempfile
5
-
6
- def _find_aapt() -> str | None:
7
- """Find aapt.exe — checks PATH first, then Android SDK build-tools"""
8
- # 1. Check PATH
9
- import shutil as _shutil
10
- path_aapt = _shutil.which("aapt")
11
- if path_aapt and os.path.isfile(path_aapt):
12
- return path_aapt
13
- path_aapt2 = _shutil.which("aapt2")
14
- if path_aapt2 and os.path.isfile(path_aapt2):
15
- return path_aapt2
16
- # 2. Check Android SDK build-tools
17
- sdk_base = os.environ.get("LOCALAPPDATA", "") + "/Android/Sdk"
18
- build_tools = os.path.join(sdk_base, "build-tools")
19
- if os.path.isdir(build_tools):
20
- versions = sorted(os.listdir(build_tools), reverse=True)
21
- for v in versions:
22
- for binary in ("aapt.exe", "aapt2.exe"):
23
- aapt = os.path.join(build_tools, v, binary)
24
- if os.path.isfile(aapt):
25
- return aapt
26
- return None
27
-
28
- AAPT_PATH = _find_aapt()
29
-
30
- def check_adb() -> bool:
31
- """检查 ADB 是否可用"""
32
- try:
33
- subprocess.run(["adb", "version"], capture_output=True, check=True)
34
- return True
35
- except (subprocess.CalledProcessError, FileNotFoundError):
36
- return False
37
-
38
- def check_device() -> bool:
39
- """检查设备是否连接"""
40
- try:
41
- result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
42
- lines = result.stdout.strip().split("\n")
43
- for line in lines[1:]:
44
- if line.strip() and not line.startswith("*"):
45
- parts = line.split()
46
- if len(parts) >= 2 and parts[1] == "device":
47
- return True
48
- return False
49
- except (subprocess.CalledProcessError, FileNotFoundError):
50
- return False
51
-
52
- def scan_packages() -> list[str]:
53
- """执行 adb shell pm list packages -3,返回包名列表"""
54
- result = subprocess.run(
55
- ["adb", "shell", "pm", "list", "packages", "-3"],
56
- capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
57
- )
58
- pkgs = []
59
- for line in result.stdout.strip().split("\n"):
60
- line = line.strip()
61
- if line.startswith("package:"):
62
- pkgs.append(line[8:])
63
- return pkgs
64
-
65
- def get_apk_path(pkg: str) -> str | None:
66
- """执行 pm path,返回 APK 物理路径"""
67
- try:
68
- result = subprocess.run(
69
- ["adb", "shell", "pm", "path", pkg],
70
- capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
71
- )
72
- for line in result.stdout.strip().split("\n"):
73
- if line.startswith("package:"):
74
- return line[8:].strip()
75
- return None
76
- except (subprocess.CalledProcessError, FileNotFoundError):
77
- return None
78
-
79
- def get_app_name(pkg: str) -> str:
80
- """通过 aapt 提取 APK 的 application label"""
81
- if not AAPT_PATH:
82
- return pkg
83
- apk_path = get_apk_path(pkg)
84
- if not apk_path:
85
- return pkg
86
- tmp = os.path.join(tempfile.gettempdir(), f"apkpull_{os.getpid()}_{pkg.replace('.', '_')}.apk")
87
- try:
88
- # pull APK to temp file
89
- pull_ok = subprocess.run(
90
- ["adb", "pull", apk_path, tmp],
91
- capture_output=True, encoding="utf-8", errors="replace"
92
- ).returncode == 0
93
- if not pull_ok or not os.path.exists(tmp):
94
- return pkg
95
- # extract label via aapt
96
- result = subprocess.run(
97
- [AAPT_PATH, "dump", "badging", tmp],
98
- capture_output=True, encoding="utf-8", errors="replace"
99
- )
100
- for line in result.stdout.split("\n"):
101
- m = re.search(r"application-label:'(.+?)'", line)
102
- if m:
103
- return m.group(1)
104
- return pkg
105
- except Exception:
106
- return pkg
107
- finally:
108
- if os.path.exists(tmp):
109
- try:
110
- os.remove(tmp)
111
- except OSError:
112
- pass
113
-
114
- def pull_apk(apk_path: str, dest_dir: str) -> bool:
115
- """执行 adb pull,拉取 APK 到目标目录"""
116
- try:
117
- subprocess.run(
118
- ["adb", "pull", apk_path, dest_dir],
119
- capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
120
- )
121
- return True
122
- except (subprocess.CalledProcessError, FileNotFoundError):
123
- return False
File without changes
File without changes