apkdev 2.0.0__py3-none-any.whl

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.
apkdev/sdk.py ADDED
@@ -0,0 +1,526 @@
1
+ """SDK manager โ€” cross-platform setup and tool checking."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from rich.panel import Panel
12
+
13
+ from .utils import ANDROID_HOME, ensure_android_home
14
+
15
+ console = Console()
16
+
17
+
18
+ def detect_platform() -> dict:
19
+ """Detect platform and return package manager info."""
20
+ info = {
21
+ "system": sys.platform,
22
+ "is_termux": "TERMUX_VERSION" in os.environ,
23
+ "is_android": os.environ.get("ANDROID_ROOT") == "/system",
24
+ "pkg_manager": None,
25
+ "pkg_install_cmd": None,
26
+ "pkg_update_cmd": None,
27
+ "java_pkg": "java",
28
+ "home_dir": str(Path.home()),
29
+ }
30
+
31
+ if info["is_termux"]:
32
+ info["pkg_manager"] = "pkg"
33
+ info["pkg_install_cmd"] = ["pkg", "install", "-y"]
34
+ info["pkg_update_cmd"] = ["pkg", "update"]
35
+ info["java_pkg"] = "openjdk-21"
36
+ info["platform_name"] = "Termux (Android)"
37
+ elif sys.platform == "linux":
38
+ if shutil.which("apt-get"):
39
+ info["pkg_manager"] = "apt"
40
+ info["pkg_install_cmd"] = ["sudo", "apt-get", "install", "-y"]
41
+ info["pkg_update_cmd"] = ["sudo", "apt-get", "update"]
42
+ info["java_pkg"] = "openjdk-21-jdk"
43
+ info["platform_name"] = "Linux (Debian/Ubuntu)"
44
+ elif shutil.which("dnf"):
45
+ info["pkg_manager"] = "dnf"
46
+ info["pkg_install_cmd"] = ["sudo", "dnf", "install", "-y"]
47
+ info["java_pkg"] = "java-21-openjdk-devel"
48
+ info["platform_name"] = "Linux (Fedora/RHEL)"
49
+ elif shutil.which("pacman"):
50
+ info["pkg_manager"] = "pacman"
51
+ info["pkg_install_cmd"] = ["sudo", "pacman", "-S", "--noconfirm"]
52
+ info["java_pkg"] = "jdk21-openjdk"
53
+ info["platform_name"] = "Linux (Arch)"
54
+ else:
55
+ info["platform_name"] = "Linux (unknown package manager)"
56
+ elif sys.platform == "darwin":
57
+ if shutil.which("brew"):
58
+ info["pkg_manager"] = "brew"
59
+ info["pkg_install_cmd"] = ["brew", "install"]
60
+ info["java_pkg"] = "openjdk@21"
61
+ info["platform_name"] = "macOS (Homebrew)"
62
+ else:
63
+ info["platform_name"] = "macOS (no Homebrew)"
64
+ elif sys.platform == "win32":
65
+ if shutil.which("winget"):
66
+ info["pkg_manager"] = "winget"
67
+ info["pkg_install_cmd"] = ["winget", "install"]
68
+ info["java_pkg"] = "Amazon.Corretto.21"
69
+ info["platform_name"] = "Windows (winget)"
70
+ elif shutil.which("choco"):
71
+ info["pkg_manager"] = "choco"
72
+ info["pkg_install_cmd"] = ["choco", "install", "-y"]
73
+ info["java_pkg"] = "openjdk21"
74
+ info["platform_name"] = "Windows (Chocolatey)"
75
+ else:
76
+ info["platform_name"] = "Windows"
77
+ else:
78
+ info["platform_name"] = sys.platform
79
+
80
+ return info
81
+
82
+
83
+ def _version_or_none(cmd: str, *args: str) -> str:
84
+ """Get version string from a tool."""
85
+ tool = shutil.which(cmd)
86
+ if not tool:
87
+ return "โŒ Not found"
88
+ try:
89
+ r = subprocess.run([tool] + list(args), capture_output=True, text=True, timeout=5)
90
+ out = r.stdout.split("\n")[0].strip()
91
+ return out[:80] if out else "โœ… OK"
92
+ except Exception:
93
+ return "โœ… OK"
94
+
95
+
96
+ def info() -> None:
97
+ """Display environment info with platform detection."""
98
+ ensure_android_home()
99
+ plat = detect_platform()
100
+
101
+ console.print(f"[bold cyan]๐Ÿ“ฑ APKDev โ€” {plat['platform_name']}[/]")
102
+ console.print()
103
+
104
+ # Build tools table
105
+ table = Table(title="Build Tools", border_style="blue")
106
+ table.add_column("Tool", style="bold yellow")
107
+ table.add_column("Status", style="green")
108
+
109
+ table.add_row("Java", _version_or_none("java", "-version"))
110
+ table.add_row("Kotlin", _version_or_none("kotlin", "-version"))
111
+ table.add_row("Gradle", _version_or_none("gradle", "--version"))
112
+ table.add_row("AAPT2", _version_or_none("aapt2", "version"))
113
+ table.add_row("D8", _version_or_none("d8", "--version"))
114
+ table.add_row("ADB", _version_or_none("adb", "--version"))
115
+ table.add_row("Apksigner", _version_or_none("apksigner", "version"))
116
+ table.add_row("Zipalign", "โœ… OK" if shutil.which("zipalign") else "โŒ Not found")
117
+
118
+ sdk = "โŒ Not found"
119
+ platforms_dir = Path(ANDROID_HOME) / "platforms"
120
+ if platforms_dir.exists():
121
+ plats = [p.name for p in platforms_dir.iterdir() if p.is_dir()]
122
+ sdk = ", ".join(plats) if plats else "โŒ No platforms"
123
+ table.add_row("Android SDK", sdk)
124
+
125
+ console.print(table)
126
+ console.print()
127
+
128
+ # RE tools table
129
+ table2 = Table(title="Reverse Engineering Tools", border_style="magenta")
130
+ table2.add_column("Tool", style="bold yellow")
131
+ table2.add_column("Status", style="green")
132
+
133
+ table2.add_row("Apktool", _version_or_none("apktool", "--version"))
134
+ table2.add_row("jadx", _version_or_none("jadx", "--version"))
135
+
136
+ smali_sh = shutil.which("smali")
137
+ baksmali_sh = shutil.which("baksmali")
138
+ table2.add_row("smali", f"โœ… {smali_sh}" if smali_sh else "โŒ Not found")
139
+ table2.add_row("baksmali", f"โœ… {baksmali_sh}" if baksmali_sh else "โŒ Not found")
140
+ table2.add_row("dex2jar", "โœ… OK" if shutil.which("d2j-dex2jar") else "โŒ Not found")
141
+
142
+ aej = Path.home() / ".APKEditor.jar"
143
+ if aej.exists():
144
+ try:
145
+ r = subprocess.run(["java", "-jar", str(aej), "--version"],
146
+ capture_output=True, text=True, timeout=5)
147
+ table2.add_row("APKEditor", r.stdout.strip() or "โœ… OK")
148
+ except Exception:
149
+ table2.add_row("APKEditor", "โœ… OK")
150
+ else:
151
+ table2.add_row("APKEditor", "โŒ Not found")
152
+
153
+ try:
154
+ __import__("androguard")
155
+ table2.add_row("androguard", "โœ… OK")
156
+ except ImportError:
157
+ table2.add_row("androguard", "โŒ Not found")
158
+
159
+ console.print(table2)
160
+ console.print(f"\n[dim]Platform: {plat['platform_name']} | Python: {sys.version.split()[0]}[/]")
161
+
162
+
163
+ def _download_jar(url: str, dest: Path) -> bool:
164
+ """Download a JAR file using pure Python urllib."""
165
+ import urllib.request
166
+ try:
167
+ urllib.request.urlretrieve(url, str(dest))
168
+ return dest.exists() and dest.stat().st_size > 1000
169
+ except Exception:
170
+ return False
171
+
172
+
173
+ def _create_wrapper(path: Path, name: str) -> None:
174
+ """Create a wrapper script (platform-aware)."""
175
+ import stat
176
+ path.parent.mkdir(parents=True, exist_ok=True)
177
+ if sys.platform == "win32":
178
+ path = path.with_suffix(".bat")
179
+ content = f"@echo off\njava -jar \"%USERPROFILE%\\.{name}.jar\" %*\n"
180
+ else:
181
+ content = f"#!/usr/bin/env bash\nexec java -jar \"$HOME/.{name}.jar\" \"$@\"\n"
182
+ path.write_text(content)
183
+ if sys.platform != "win32":
184
+ path.chmod(0o755 | stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
185
+
186
+
187
+ def _install_java(plat: dict) -> bool:
188
+ """Try to install Java via package manager.
189
+ Returns True if Java was installed or already present."""
190
+ if shutil.which("java"):
191
+ return True
192
+
193
+ console.print()
194
+ console.print("[bold]๐Ÿ“Œ Java is required. Attempting to install...[/]")
195
+
196
+ try:
197
+ if plat["is_termux"]:
198
+ # Termux โ€” no root needed
199
+ console.print(f" Running: pkg install {plat['java_pkg']}")
200
+ r = subprocess.run(["pkg", "install", "-y", plat["java_pkg"]],
201
+ capture_output=True, text=True, timeout=120)
202
+ if r.returncode == 0:
203
+ console.print(" โœ… Java installed via pkg")
204
+ return shutil.which("java") is not None
205
+ else:
206
+ console.print(f" โš ๏ธ Failed: {r.stderr[-200:]}")
207
+ return False
208
+
209
+ elif sys.platform == "linux":
210
+ # Linux โ€” try sudo apt/dnf/pacman
211
+ pm = plat["pkg_install_cmd"]
212
+ if pm:
213
+ console.print(f" Running: {' '.join(pm)} {plat['java_pkg']}")
214
+ console.print(" [yellow](you may be prompted for sudo password)[/]")
215
+ r = subprocess.run(pm + [plat["java_pkg"]],
216
+ capture_output=True, text=True, timeout=180)
217
+ if r.returncode == 0:
218
+ console.print(" โœ… Java installed")
219
+ return shutil.which("java") is not None
220
+ else:
221
+ console.print(f" โš ๏ธ Install may have failed: {r.stderr[-200:]}")
222
+ return False
223
+
224
+ elif sys.platform == "darwin":
225
+ # macOS โ€” try Homebrew
226
+ brew = shutil.which("brew")
227
+ if not brew:
228
+ console.print(" Homebrew not found. Attempting to install Homebrew...")
229
+ console.print(" [yellow]This may take a few minutes.[/]")
230
+ r = subprocess.run([
231
+ "/bin/bash", "-c",
232
+ "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
233
+ ], capture_output=True, text=True, timeout=300)
234
+ if r.returncode == 0:
235
+ brew = shutil.which("brew") or "/opt/homebrew/bin/brew"
236
+ console.print(" โœ… Homebrew installed")
237
+ else:
238
+ console.print(f" โš ๏ธ Homebrew install failed: {r.stderr[-200:]}")
239
+ return False
240
+
241
+ if brew:
242
+ console.print(f" Running: {brew} install openjdk@21")
243
+ r = subprocess.run([brew, "install", "openjdk@21"],
244
+ capture_output=True, text=True, timeout=300)
245
+ if r.returncode == 0:
246
+ console.print(" โœ… Java installed via Homebrew")
247
+ return shutil.which("java") is not None
248
+ else:
249
+ console.print(f" โš ๏ธ Install issue: {r.stderr[-200:]}")
250
+ return False
251
+
252
+ elif sys.platform == "win32":
253
+ # Windows โ€” try Chocolatey, then winget
254
+ choco = shutil.which("choco") or shutil.which("choco.exe")
255
+
256
+ if not choco:
257
+ console.print(" Chocolatey not found. Attempting to install...")
258
+ console.print(" [yellow](requires PowerShell as Administrator)[/]")
259
+ ps_cmd = (
260
+ "Set-ExecutionPolicy Bypass -Scope Process -Force; "
261
+ "[System.Net.ServicePointManager]::SecurityProtocol = "
262
+ "[System.Net.ServicePointManager]::SecurityProtocol -bor 3072; "
263
+ "iex ((New-Object System.Net.WebClient).DownloadString("
264
+ "'https://chocolatey.org/install.ps1'))"
265
+ )
266
+ r = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd],
267
+ capture_output=True, text=True, timeout=120)
268
+ if r.returncode == 0:
269
+ choco = shutil.which("choco") or shutil.which("choco.exe")
270
+ console.print(" โœ… Chocolatey installed")
271
+ else:
272
+ console.print(f" โš ๏ธ Chocolatey install failed: {r.stderr[-200:]}")
273
+ # Try winget as fallback
274
+ winget = shutil.which("winget") or shutil.which("winget.exe")
275
+ if winget:
276
+ console.print(" Trying winget instead...")
277
+ r = subprocess.run([winget, "install", "--id", "Amazon.Corretto.21", "--accept-package-agreements"],
278
+ capture_output=True, text=True, timeout=120)
279
+ if r.returncode == 0:
280
+ console.print(" โœ… Java installed via winget")
281
+ return shutil.which("java") is not None
282
+ return False
283
+
284
+ if choco:
285
+ console.print(f" Running: choco install openjdk21")
286
+ r = subprocess.run([choco, "install", "openjdk21", "-y"],
287
+ capture_output=True, text=True, timeout=180)
288
+ if r.returncode == 0:
289
+ console.print(" โœ… Java installed via Chocolatey")
290
+ return shutil.which("java") is not None
291
+ else:
292
+ console.print(f" โš ๏ธ Install issue: {r.stderr[-200:]}")
293
+ return False
294
+ except Exception as e:
295
+ console.print(f" โš ๏ธ Java install failed: {e}")
296
+
297
+ return False
298
+
299
+
300
+ def setup() -> None:
301
+ """Setup: installs everything possible via Python.
302
+
303
+ What this installs:
304
+ - Java 17+ (via pkg/apt/brew/choco โ€” may need sudo/admin)
305
+ - androguard (via pip)
306
+ - smali, baksmali, APKEditor JARs (via urllib)
307
+ - Android SDK (if Java is available)
308
+
309
+ Run without arguments for full auto-setup.
310
+ """
311
+ import urllib.request
312
+ import zipfile
313
+
314
+ plat = detect_platform()
315
+ console.print(f"[bold cyan]๐Ÿ“ฑ APKDev Setup โ€” {plat['platform_name']}[/]")
316
+ console.print()
317
+
318
+ # Step 0: Install Java (if missing)
319
+ has_java = shutil.which("java") is not None
320
+ if not has_java:
321
+ console.print("[bold]Step 0: Install Java 17+[/]")
322
+ has_java = _install_java(plat)
323
+ if has_java:
324
+ console.print(" โœ… Java is ready!")
325
+ else:
326
+ console.print(" โš ๏ธ Could not auto-install Java.")
327
+ console.print(" Install it manually:")
328
+ if sys.platform == "win32":
329
+ console.print(" https://adoptium.net/temurin/releases/ (JDK 21)")
330
+ elif sys.platform == "darwin":
331
+ console.print(" brew install openjdk@21")
332
+ else:
333
+ console.print(" sudo apt-get install openjdk-21-jdk")
334
+ console.print()
335
+
336
+ # Step 0b: Install Termux build + RE tools (if on Termux)
337
+ if plat["is_termux"]:
338
+ console.print("[bold]Step 0b: Android build + RE tools[/]")
339
+ needed = ["aapt2", "d8", "apksigner", "jadx", "dex2jar"]
340
+ missing = [t for t in needed if not shutil.which(t)]
341
+ if missing:
342
+ console.print(f" Installing: {' '.join(missing)}")
343
+ subprocess.run(["pkg", "install", "-y"] + missing,
344
+ capture_output=True, timeout=120)
345
+ for t in missing:
346
+ if shutil.which(t):
347
+ console.print(f" โœ… {t} installed")
348
+ else:
349
+ console.print(f" โš ๏ธ {t} failed")
350
+ else:
351
+ console.print(" โœ… All build + RE tools already installed")
352
+ console.print()
353
+ else:
354
+ # Non-Termux: try to install RE tools via platform package manager
355
+ re_tools = {"linux": ["jadx", "dex2jar"], "darwin": ["jadx", "dex2jar"], "win32": ["jadx", "dex2jar"]}
356
+ console.print("[bold]Step 0b: Reverse engineering tools[/]")
357
+ pm = plat.get("pkg_install_cmd", [])
358
+ if pm:
359
+ missing = [t for t in re_tools.get(sys.platform, []) if not shutil.which(t)]
360
+ if missing:
361
+ console.print(f" Installing: {' '.join(missing)}")
362
+ try:
363
+ subprocess.run(pm + missing, capture_output=True, timeout=120)
364
+ for t in missing:
365
+ if shutil.which(t):
366
+ console.print(f" โœ… {t} installed")
367
+ except:
368
+ console.print(f" โš ๏ธ Try: sudo apt-get install jadx dex2jar")
369
+ else:
370
+ console.print(" โœ… RE tools already installed")
371
+ console.print()
372
+
373
+ python_bin_dir = Path(shutil.which("python3") or sys.executable).parent
374
+
375
+ # Step 1: smali/baksmali (Java JARs)
376
+ console.print("[bold]Step 1: smali/baksmali (JARs)[/]")
377
+ for name, jar_path, url in [
378
+ ("smali", Path.home() / ".smali.jar",
379
+ "https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar"),
380
+ ("baksmali", Path.home() / ".baksmali.jar",
381
+ "https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar"),
382
+ ]:
383
+ if not jar_path.exists():
384
+ console.print(f" Downloading {name} 2.5.2...")
385
+ if _download_jar(url, jar_path):
386
+ _create_wrapper(python_bin_dir / name, name)
387
+ console.print(f" โœ… {name} installed")
388
+ else:
389
+ console.print(f" โš ๏ธ {name} download failed")
390
+ else:
391
+ console.print(f" โœ… {name} already installed")
392
+
393
+ # Step 2: APKEditor
394
+ console.print("[bold]Step 2: APKEditor (JAR)[/]")
395
+ apkeditor_jar = Path.home() / ".APKEditor.jar"
396
+ if not apkeditor_jar.exists() or apkeditor_jar.stat().st_size < 1000:
397
+ console.print(" Downloading APKEditor 1.4.9...")
398
+ if _download_jar(
399
+ "https://github.com/REAndroid/APKEditor/releases/download/V1.4.9/APKEditor-1.4.9.jar",
400
+ apkeditor_jar
401
+ ):
402
+ _create_wrapper(python_bin_dir / "apkeditor", "APKEditor")
403
+ console.print(" โœ… APKEditor installed")
404
+ else:
405
+ console.print(" โš ๏ธ Download failed")
406
+ else:
407
+ console.print(" โœ… APKEditor already installed")
408
+
409
+ # Step 3: androguard (Python package)
410
+ console.print("[bold]Step 3: Python packages[/]")
411
+ try:
412
+ import androguard
413
+ console.print(f" โœ… androguard {androguard.__version__} already")
414
+ except ImportError:
415
+ console.print(" Installing androguard via pip...")
416
+ try:
417
+ subprocess.run(
418
+ [sys.executable, "-m", "pip", "install", "androguard"],
419
+ capture_output=True, timeout=120
420
+ )
421
+ console.print(" โœ… androguard installed")
422
+ except Exception:
423
+ console.print(" โš ๏ธ pip install failed, try: pip3 install androguard")
424
+
425
+ # Step 4: Android SDK
426
+ console.print("[bold]Step 4: Android SDK (for building)[/]")
427
+ sdk_dir = Path.home() / "android-sdk"
428
+ sdk_done = (sdk_dir / "platforms" / "android-34").exists()
429
+ has_java = shutil.which("java") is not None # Re-check after possible install
430
+
431
+ if not sdk_done and has_java:
432
+ console.print(" Setting up Android SDK...")
433
+ sdk_dir.mkdir(parents=True, exist_ok=True)
434
+
435
+ zip_path = Path.home() / "cmdline-tools.zip"
436
+ if not zip_path.exists():
437
+ console.print(" Downloading SDK tools (~150 MB)...")
438
+ try:
439
+ urllib.request.urlretrieve(
440
+ "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip",
441
+ str(zip_path)
442
+ )
443
+ except Exception as e:
444
+ console.print(f" โš ๏ธ Download failed: {e}")
445
+
446
+ if zip_path.exists() and zip_path.stat().st_size > 1000:
447
+ with zipfile.ZipFile(str(zip_path), "r") as zf:
448
+ zf.extractall(str(sdk_dir))
449
+
450
+ cmdline_dir = sdk_dir / "cmdline-tools"
451
+ latest_dir = cmdline_dir / "latest"
452
+ if not latest_dir.exists():
453
+ latest_dir.mkdir(parents=True, exist_ok=True)
454
+ # Move bin/, lib/ etc. into latest/
455
+ for item in cmdline_dir.iterdir():
456
+ if item.name != "latest" and item.name != ".." and item.name != ".":
457
+ dest = latest_dir / item.name
458
+ if not dest.exists():
459
+ item.rename(dest)
460
+ sdkmanager = latest_dir / "bin" / "sdkmanager"
461
+ if sdkmanager.exists():
462
+ os.environ["ANDROID_HOME"] = str(sdk_dir)
463
+ # Ensure executable
464
+ sdkmanager.chmod(0o755)
465
+ sdk_root_arg = f"--sdk_root={sdk_dir}"
466
+ console.print(" Accepting SDK licenses...")
467
+ subprocess.run([str(sdkmanager), sdk_root_arg, "--licenses"],
468
+ input=b"y\ny\ny\ny\ny\n", capture_output=True, timeout=30)
469
+ console.print(" Installing android-34...")
470
+ r = subprocess.run([str(sdkmanager), sdk_root_arg, "platforms;android-34"],
471
+ capture_output=True, timeout=300)
472
+ if r.returncode == 0:
473
+ console.print(" โœ… Android SDK installed")
474
+ sdk_done = True
475
+ else:
476
+ console.print(f" โš ๏ธ SDK issue: {r.stderr[-200:].decode()}")
477
+ elif sdk_done:
478
+ console.print(" โœ… Android SDK already installed")
479
+ elif not has_java:
480
+ console.print(" โš ๏ธ Java needed for SDK")
481
+
482
+ # Step 5: Debug keystore
483
+ console.print("[bold]Step 5: Debug keystore[/]")
484
+ keystore = Path.home() / ".android" / "debug.keystore"
485
+ if not keystore.exists() and has_java:
486
+ console.print(" Creating debug keystore...")
487
+ keystore.parent.mkdir(parents=True, exist_ok=True)
488
+ try:
489
+ subprocess.run(["keytool", "-genkey", "-v", "-keystore", str(keystore),
490
+ "-storepass", "android", "-alias", "androiddebugkey",
491
+ "-keypass", "android", "-keyalg", "RSA", "-keysize", "2048",
492
+ "-validity", "10000",
493
+ "-dname", "CN=Android Debug,O=Android,C=US"],
494
+ capture_output=True, timeout=15)
495
+ console.print(" โœ… Debug keystore created")
496
+ except Exception:
497
+ console.print(" โš ๏ธ keytool not available")
498
+ elif keystore.exists():
499
+ console.print(" โœ… Debug keystore exists")
500
+
501
+ # Summary
502
+ has_java = shutil.which("java") is not None
503
+ console.print()
504
+ console.print("[bold green]โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•[/]")
505
+ console.print("[bold green] Setup complete![/]")
506
+ console.print("[bold green]โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•[/]")
507
+ console.print()
508
+ console.print(f" {'โœ…' if has_java else 'โŒ'} Java โ€” {'installed' if has_java else 'install manually'}")
509
+ console.print(f" {'โœ…' if sdk_done else 'โŒ'} SDK โ€” {'installed' if sdk_done else 'needs Java'}")
510
+ console.print(f" โœ… apkdev โ€” always ready (pure Python)")
511
+ console.print()
512
+ console.print(" [bold]Commands without Java:[/] ls, extract, new, info, doctor")
513
+ if not has_java:
514
+ console.print()
515
+ console.print(" [yellow]To install Java manually:[/]")
516
+ if sys.platform == "win32":
517
+ console.print(" https://adoptium.net/temurin/releases/")
518
+ elif sys.platform == "darwin":
519
+ console.print(" brew install openjdk@21")
520
+ elif plat["is_termux"]:
521
+ console.print(" pkg install openjdk-21")
522
+ else:
523
+ console.print(" sudo apt-get install openjdk-21-jdk")
524
+ console.print()
525
+ if has_java and not sdk_done:
526
+ console.print("[yellow]Re-run after Java install: apkdev setup[/]")
apkdev/utils.py ADDED
@@ -0,0 +1,74 @@
1
+ """Utilities: subprocess helpers, env, path resolution."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ ANDROID_HOME = Path.home() / "android-sdk"
10
+ APKDEV_HOME = Path.home() / ".pi-tui"
11
+
12
+
13
+ def check_tool(name: str, hint: str = "") -> str:
14
+ """Return path to a tool, or raise if not found."""
15
+ path = shutil.which(name)
16
+ if path:
17
+ return path
18
+ msg = f"โŒ '{name}' not found. "
19
+ if name == "java":
20
+ msg += "Java is required. Install JDK 17+ from https://adoptium.net"
21
+ elif name == "aapt2":
22
+ msg += "Install: pkg install aapt2 (Termux) or sudo apt-get install aapt2"
23
+ elif name == "apktool":
24
+ msg += "Install: pkg install apktool (Termux) or sudo apt-get install apktool"
25
+ elif name == "jadx":
26
+ msg += "Install: pkg install jadx (Termux) or sudo apt-get install jadx"
27
+ else:
28
+ msg += hint or f"Install the '{name}' package for your OS"
29
+ raise FileNotFoundError(msg)
30
+
31
+
32
+ def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
33
+ """Run a command with nice error handling."""
34
+ try:
35
+ return subprocess.run(cmd, check=True, **kwargs)
36
+ except subprocess.CalledProcessError as e:
37
+ print(f"โš ๏ธ Command failed: {' '.join(cmd)}", file=sys.stderr)
38
+ sys.exit(e.returncode)
39
+
40
+
41
+ def run_java(jar: str, args: list[str], **kwargs) -> subprocess.CompletedProcess:
42
+ """Run a Java JAR."""
43
+ java = check_tool("java")
44
+ return run([java, "-jar", jar] + args, **kwargs)
45
+
46
+
47
+ def ensure_android_home():
48
+ """Set ANDROID_HOME if not already set."""
49
+ if "ANDROID_HOME" not in os.environ:
50
+ os.environ["ANDROID_HOME"] = str(ANDROID_HOME)
51
+ if "ANDROID_SDK_ROOT" not in os.environ:
52
+ os.environ["ANDROID_SDK_ROOT"] = str(ANDROID_HOME)
53
+
54
+
55
+ def is_termux() -> bool:
56
+ """Check if we're running inside Termux on Android."""
57
+ return "TERMUX_VERSION" in os.environ or os.environ.get("ANDROID_ROOT") == "/system"
58
+
59
+
60
+ def is_android() -> bool:
61
+ """Check if we're running on Android."""
62
+ return os.environ.get("ANDROID_ROOT") == "/system" or os.uname().sysname == "Linux" and os.uname().release.find("android") >= 0
63
+
64
+
65
+ def banner():
66
+ """Return a nice ascii banner."""
67
+ from rich.panel import Panel
68
+ from rich.text import Text
69
+ text = Text()
70
+ text.append(" โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—\n")
71
+ text.append(" โ•‘ ๐Ÿ“ฑ APKDev v2.0 โ•‘\n")
72
+ text.append(" โ•‘ Build ยท Reverse ยท Engineer โ•‘\n")
73
+ text.append(" โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n")
74
+ return Panel(text, style="bold cyan", border_style="cyan")