#!/usr/bin/env python3 """ FSI_FELON ANDROID TERMINAL COMPILER v2 No Android Studio. No wrapper download. Uses system Gradle + Android SDK. """ import os, sys, subprocess, shutil, zipfile, urllib.request, glob from pathlib import Path ANDROID_SDK_DIR = os.path.expanduser("~/android-sdk") GRADLE_VERSION = "8.5" class AndroidCompiler: def __init__(self): self.sdk_dir = ANDROID_SDK_DIR self.project_dir = None self.app_name = "FSIApp" self.package = "com.fsi.app" self._ensure_sdk() def _sdk_manager_path(self): p = os.path.join(self.sdk_dir, "cmdline-tools", "latest", "bin", "sdkmanager") if os.path.exists(p): return p p2 = os.path.join(self.sdk_dir, "cmdline-tools", "bin", "sdkmanager") return p2 if os.path.exists(p2) else None def _ensure_sdk(self): """Download Android SDK command-line tools if not present.""" if self._sdk_manager_path(): installed = self._ensure_platform() if installed: print(" ✅ Android SDK ready") return True print(" 📥 Downloading Android SDK command-line tools...") os.makedirs(self.sdk_dir, exist_ok=True) sdk_url = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" zip_path = f"{self.sdk_dir}/cmdline-tools.zip" try: urllib.request.urlretrieve(sdk_url, zip_path) print(" ✅ SDK tools downloaded") # Extract with zipfile.ZipFile(zip_path, 'r') as z: z.extractall(self.sdk_dir) # Move to correct structure old = os.path.join(self.sdk_dir, "cmdline-tools") new = os.path.join(self.sdk_dir, "cmdline-tools", "latest") if os.path.exists(new): shutil.rmtree(new) os.makedirs(os.path.dirname(new), exist_ok=True) # Move contents rather than directory to avoid nested structure for item in os.listdir(old): shutil.move(os.path.join(old, item), new) os.rmdir(old) os.remove(zip_path) print(" ✅ SDK ready") return self._ensure_platform() except Exception as e: print(f" ⚠️ SDK download failed: {e}") print(" 💡 Manual install: https://developer.android.com/studio#command-tools") return False def _ensure_platform(self): """Ensure required Android platform and build tools are installed.""" sm = self._sdk_manager_path() if not sm: return False try: r = subprocess.run([sm, "--list", "--sdk_root=" + self.sdk_dir], capture_output=True, text=True, timeout=30) if "platforms;android-34" not in r.stdout: print(" 📥 Installing Android SDK platform 34...") subprocess.run([sm, "--sdk_root=" + self.sdk_dir, "platforms;android-34"], capture_output=True, timeout=120) if "build-tools;34.0.0" not in r.stdout: print(" 📥 Installing build-tools 34.0.0...") subprocess.run([sm, "--sdk_root=" + self.sdk_dir, "build-tools;34.0.0"], capture_output=True, timeout=120) return True except: return False def _find_aapt(self): bt = os.path.join(self.sdk_dir, "build-tools") if os.path.exists(bt): for v in sorted(os.listdir(bt), reverse=True): aapt = os.path.join(bt, v, "aapt") if os.path.exists(aapt): return aapt return shutil.which("aapt") def verify_apk(self, apk_path=None): """Verify APK using aapt dump badging.""" path = apk_path or self._find_apk() if not path or not os.path.exists(path): return {"valid": False, "error": "APK not found"} aapt = self._find_aapt() if not aapt: return {"valid": False, "error": "aapt not found — install Android build-tools"} try: r = subprocess.run([aapt, "dump", "badging", path], capture_output=True, text=True, timeout=15) if r.returncode == 0: lines = r.stdout.split("\n") info = {"valid": True, "path": path, "size": os.path.getsize(path)} for l in lines: if "package:" in l: for p in l.split(): if "name=" in p: info["package"] = p.split("=")[1].strip("'") if "versionCode=" in p: info["version_code"] = p.split("=")[1].strip("'") if "versionName=" in p: info["version_name"] = p.split("=")[1].strip("'") if "launchable-activity:" in l: info["activity"] = l.split("name=")[1].split()[0].strip("'") if "sdkVersion:" in l: info["min_sdk"] = l.split(":")[1].strip() if "targetSdkVersion:" in l: info["target_sdk"] = l.split(":")[1].strip() return info else: return {"valid": False, "error": r.stderr[:200]} except Exception as e: return {"valid": False, "error": str(e)} def install_apk(self, apk_path=None, serial=None): """Install APK via adb.""" path = apk_path or self._find_apk() if not path or not os.path.exists(path): return {"success": False, "error": "APK not found"} adb = shutil.which("adb") if not adb: return {"success": False, "error": "adb not found — install Android platform-tools"} cmd = [adb] if serial: cmd += ["-s", serial] cmd += ["install", "-r", path] try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=60) if "Success" in r.stdout: return {"success": True, "output": r.stdout.strip()} else: return {"success": False, "error": r.stdout[-200:] or r.stderr[-200:]} except Exception as e: return {"success": False, "error": str(e)} def list_devices(self): """List connected ADB devices.""" adb = shutil.which("adb") if not adb: return [] try: r = subprocess.run([adb, "devices"], capture_output=True, text=True, timeout=5) lines = r.stdout.strip().split("\n")[1:] devices = [] for l in lines: if l.strip() and "device" in l and "offline" not in l: devices.append(l.split()[0]) return devices except: return [] def _find_apk(self): if self.project_dir: apks = glob.glob(self.project_dir + "/**/*.apk", recursive=True) if apks: return apks[0] standard = os.path.join(self.project_dir, "app/build/outputs/apk/debug/app-debug.apk") if os.path.exists(standard): return standard return None def _ensure_gradle(self): """Check if gradle is available.""" if shutil.which("gradle"): return True # Try to install via apt try: subprocess.run(["apt-get", "install", "-y", "gradle"], capture_output=True, timeout=60) return shutil.which("gradle") is not None except: return False def generate_project(self, app_name="FSIApp", package="com.fsi.app", activity_name="MainActivity"): """Generate a complete Android project.""" self.app_name = app_name self.package = package self.project_dir = f"/tmp/fsi_felon/android_projects/{app_name.lower().replace(' ', '_')}" print(f"\n 🔨 GENERATING: {app_name}") print(f" 📦 Package: {package}") if os.path.exists(self.project_dir): shutil.rmtree(self.project_dir) # Create structure pkg_path = package.replace('.', '/') dirs = [ f"app/src/main/java/{pkg_path}", "app/src/main/res/layout", "app/src/main/res/values", "app/src/main/res/drawable", "gradle/wrapper" ] for d in dirs: os.makedirs(os.path.join(self.project_dir, d), exist_ok=True) # Write all files self._write_build_gradle() self._write_manifest(activity_name) self._write_main_activity(activity_name, pkg_path) self._write_layout() self._write_resources() self._write_gradle_props() print(f" ✅ Generated: {len(dirs)} directories, 10+ files") return self.project_dir def _write_build_gradle(self): content = f"""plugins {{ id 'com.android.application' }} android {{ namespace '{self.package}' compileSdk 34 defaultConfig {{ applicationId "{self.package}" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" }} buildTypes {{ release {{ minifyEnabled false }} }} }} dependencies {{ implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' }} """ with open(os.path.join(self.project_dir, "app", "build.gradle"), 'w') as f: f.write(content) def _write_manifest(self, activity_name): content = f""" """ with open(os.path.join(self.project_dir, "app", "src", "main", "AndroidManifest.xml"), 'w') as f: f.write(content) def _write_main_activity(self, activity_name, pkg_path): content = f"""package {self.package}; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; public class {activity_name} extends AppCompatActivity {{ @Override protected void onCreate(Bundle savedInstanceState) {{ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }} }} """ path = os.path.join(self.project_dir, f"app/src/main/java/{pkg_path}/{activity_name}.java") with open(path, 'w') as f: f.write(content) def _write_layout(self): content = """ """ with open(os.path.join(self.project_dir, "app/src/main/res/layout/activity_main.xml"), 'w') as f: f.write(content) def _write_resources(self): with open(os.path.join(self.project_dir, "app/src/main/res/values/strings.xml"), 'w') as f: f.write(f""" {self.app_name} """) with open(os.path.join(self.project_dir, "app/src/main/res/values/themes.xml"), 'w') as f: f.write(""" """) def _write_gradle_props(self): with open(os.path.join(self.project_dir, "gradle.properties"), 'w') as f: f.write("""org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true """) with open(os.path.join(self.project_dir, "settings.gradle"), 'w') as f: f.write(f"""pluginManagement {{ repositories {{ google(); mavenCentral(); gradlePluginPortal() }} }} dependencyResolutionManagement {{ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {{ google(); mavenCentral() }} }} rootProject.name = "{self.app_name}" include ':app' """) def compile(self): """Compile using system gradle or gradlew.""" if not self.project_dir: print(" ❌ No project. Generate first.") return None if not self._ensure_gradle(): print(" ❌ Gradle not found. Install: apt-get install gradle") return None print(f"\n 🔨 COMPILING...") print(f" 📁 {self.project_dir}") # Try gradle directly gradle = shutil.which("gradle") cmd = [gradle, "assembleDebug", "--no-daemon", "-p", self.project_dir] print(f" ⚙️ {' '.join(cmd)}") print(f" ⏳ First build takes 3-5 minutes (downloads dependencies)...") try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) if result.returncode == 0: apk = os.path.join(self.project_dir, "app/build/outputs/apk/debug/app-debug.apk") if os.path.exists(apk): size = os.path.getsize(apk) print(f"\n ✅ APK BUILT SUCCESSFULLY") print(f" 📦 {apk}") print(f" 📊 {size:,} bytes ({size/1024/1024:.2f} MB)") print(f"\n 🚀 INSTALL: adb install {apk}") print(f" 📤 SHARE: cp {apk} ~/Downloads/") return apk else: # Find APK apks = glob.glob(self.project_dir + "/**/*.apk", recursive=True) if apks: print(f" ✅ APK found: {apks[0]}") return apks[0] print(" ⚠️ Build succeeded but APK not found") return None else: print(f" ❌ BUILD FAILED") err = result.stderr[-500:] if result.stderr else result.stdout[-500:] print(f" {err}") return None except subprocess.TimeoutExpired: print(" ⏱️ Timeout. Try again with more memory.") return None except Exception as e: print(f" ❌ Error: {e}") return None def main(): compiler = AndroidCompiler() project = compiler.generate_project("FSI_Demo", "com.fsi.demo", "MainActivity") apk = compiler.compile() if apk: print(f"\n 🎉 DONE. APK ready at: {apk}") if __name__ == '__main__': main()