Spaces:
Paused
Paused
| import os | |
| import sys | |
| import json | |
| import uuid | |
| import time | |
| import shutil | |
| import base64 | |
| import threading | |
| import subprocess | |
| import re | |
| from datetime import datetime | |
| from flask import Flask, request, jsonify, send_file, Response | |
| app = Flask(__name__) | |
| BUILD_DIR_BASE = "/tmp/builds" | |
| ROOT_DIR = os.getcwd() | |
| KEYSTORE_PATH = os.path.join(ROOT_DIR, "release-key.jks") | |
| LOG_STORE = {} | |
| BUILD_STATUS = {} | |
| BUILD_ARTIFACTS = {} | |
| os.makedirs(BUILD_DIR_BASE, exist_ok=True) | |
| def init_keystore(): | |
| if not os.path.exists(KEYSTORE_PATH): | |
| keytool_cmd = [ | |
| "keytool", "-genkeypair", | |
| "-v", | |
| "-keystore", KEYSTORE_PATH, | |
| "-keyalg", "RSA", | |
| "-keysize", "2048", | |
| "-validity", "10000", | |
| "-alias", "releasekey", | |
| "-storepass", "android123", | |
| "-keypass", "android123", | |
| "-dname", "CN=SkyData, OU=Mobile, O=Company, L=City, S=State, C=US" | |
| ] | |
| subprocess.run(keytool_cmd, capture_output=True, timeout=60) | |
| init_keystore() | |
| def get_sha1(): | |
| if not os.path.exists(KEYSTORE_PATH): | |
| return None | |
| cmd = [ | |
| "keytool", "-list", "-v", | |
| "-keystore", KEYSTORE_PATH, | |
| "-storepass", "android123", | |
| "-alias", "releasekey" | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| match = re.search(r"SHA1:\s+([A-F0-9:]+)", result.stdout) | |
| if match: | |
| return match.group(1) | |
| return None | |
| def analyze_logs_ai(log_text): | |
| diagnosis_lines = [] | |
| if "Could not resolve" in log_text or "Could not find" in log_text: | |
| diagnosis_lines.append("خطأ في التبعيات: لم يتم العثور على مكتبة مطلوبة. تأكد من أن جميع المكتبات المحددة متوفرة في مستودعات Maven/Google.") | |
| if "FirebaseApp" in log_text or "google-services" in log_text or "com.google.gms" in log_text: | |
| diagnosis_lines.append("خطأ في Firebase: ملف google-services.json غير صالح أو غير متوافق مع اسم الحزمة. تحقق من إعدادات Firebase الخاصة بك.") | |
| if "AAPT" in log_text or "resource" in log_text.lower() and "not found" in log_text.lower(): | |
| diagnosis_lines.append("خطأ في الموارد: مورد XML أو صورة مفقودة. تأكد من صحة جميع ملفات الموارد.") | |
| if "Syntax error" in log_text or "Unexpected token" in log_text or "illegal start" in log_text.lower(): | |
| diagnosis_lines.append("خطأ في بناء الجملة: يوجد خطأ نحوي في كود Java. راجع ملفات المصدر بعناية.") | |
| if "OutOfMemoryError" in log_text or "heap" in log_text.lower(): | |
| diagnosis_lines.append("خطأ في الذاكرة: نفدت ذاكرة JVM أثناء البناء. حاول زيادة حجم الذاكرة المخصصة.") | |
| if "SDK location not found" in log_text or "ANDROID_HOME" in log_text: | |
| diagnosis_lines.append("خطأ في SDK: لم يتم العثور على Android SDK. تحقق من متغيرات البيئة.") | |
| if "Manifest merger failed" in log_text: | |
| diagnosis_lines.append("خطأ في دمج الـ Manifest: تعارض بين إعدادات AndroidManifest.xml. راجع الأذونات والإعدادات.") | |
| if "Execution failed for task" in log_text: | |
| task_match = re.search(r"Execution failed for task '([^']+)'", log_text) | |
| if task_match: | |
| diagnosis_lines.append("فشل تنفيذ المهمة: {} - تحقق من إعدادات هذه المهمة والتبعيات المرتبطة بها.".format(task_match.group(1))) | |
| if "minSdkVersion" in log_text: | |
| diagnosis_lines.append("خطأ في إصدار SDK الأدنى: إصدار SDK الأدنى المحدد غير متوافق مع إحدى المكتبات.") | |
| if "Duplicate class" in log_text: | |
| diagnosis_lines.append("خطأ تكرار: يوجد تكرار في الفئات بين المكتبات. استخدم exclude لحل التعارضات.") | |
| if "R8" in log_text and ("error" in log_text.lower() or "failed" in log_text.lower()): | |
| diagnosis_lines.append("خطأ في R8/ProGuard: فشل تقليص الكود. تحقق من قواعد ProGuard وتأكد من عدم حذف فئات مطلوبة.") | |
| if not diagnosis_lines: | |
| if "BUILD SUCCESSFUL" in log_text: | |
| diagnosis_lines.append("تم البناء بنجاح! لا توجد أخطاء.") | |
| else: | |
| diagnosis_lines.append("لم يتم التعرف على خطأ محدد. راجع سجل البناء الكامل يدوياً للبحث عن المشكلة.") | |
| return "\n".join(diagnosis_lines) | |
| def generate_offline_html(): | |
| return """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Offline</title> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| body{font-family:'Segoe UI',sans-serif;background:#1a1a2e;color:#e0e0e0;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center;padding:20px} | |
| .container{max-width:400px} | |
| h1{font-size:2em;margin-bottom:10px;color:#e94560} | |
| p{font-size:1.1em;color:#aaa;margin-bottom:20px} | |
| .icon{font-size:4em;margin-bottom:20px} | |
| .loader { border: 4px solid #1a1a3e; border-top: 4px solid #e94560; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 20px; display: none; } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| button{background:#e94560;color:#fff;border:none;padding:12px 30px;border-radius:8px;font-size:1em;cursor:pointer} | |
| button:hover{background:#c73652} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="icon">🔌</div> | |
| <h1>لا يوجد اتصال بالإنترنت</h1> | |
| <p>التطبيق بانتظار عودة الشبكة لإعادة الاتصال تلقائياً...</p> | |
| <div class="loader" id="loader"></div> | |
| <button id="retryBtn" onclick="retry()">إعادة المحاولة</button> | |
| </div> | |
| <script> | |
| function retry() { | |
| document.getElementById('retryBtn').style.display = 'none'; | |
| document.getElementById('loader').style.display = 'block'; | |
| setTimeout(() => location.reload(), 1000); | |
| } | |
| window.addEventListener('online', () => { retry(); }); | |
| </script> | |
| </body> | |
| </html>""" | |
| def create_build_gradle_root(project_dir, use_firebase, use_onesignal): | |
| firebase_classpath = "" | |
| onesignal_classpath = "" | |
| if use_firebase: | |
| firebase_classpath = " classpath 'com.google.gms:google-services:4.4.0'" | |
| if use_onesignal: | |
| onesignal_classpath = " classpath 'gradle.plugin.com.onesignal:onesignal-gradle-plugin:0.14.0'" | |
| content = """buildscript {{ | |
| repositories {{ | |
| google() | |
| mavenCentral() | |
| gradlePluginPortal() | |
| }} | |
| dependencies {{ | |
| classpath 'com.android.tools.build:gradle:7.4.2' | |
| {firebase_cp} | |
| {onesignal_cp} | |
| }} | |
| }} | |
| allprojects {{ | |
| repositories {{ | |
| google() | |
| mavenCentral() | |
| gradlePluginPortal() | |
| }} | |
| }} | |
| task clean(type: Delete) {{ | |
| delete rootProject.buildDir | |
| }} | |
| """.format(firebase_cp=firebase_classpath, onesignal_cp=onesignal_classpath) | |
| with open(os.path.join(project_dir, "build.gradle"), "w") as f: | |
| f.write(content) | |
| def create_settings_gradle(project_dir, app_name): | |
| content = """rootProject.name = "{app_name}" | |
| include ':app' | |
| """.format(app_name=app_name.replace('"', '\\"')) | |
| with open(os.path.join(project_dir, "settings.gradle"), "w") as f: | |
| f.write(content) | |
| def create_gradle_properties(project_dir): | |
| content = """org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 | |
| org.gradle.daemon=false | |
| org.gradle.parallel=true | |
| org.gradle.caching=true | |
| android.useAndroidX=true | |
| android.enableJetifier=true | |
| """ | |
| with open(os.path.join(project_dir, "gradle.properties"), "w") as f: | |
| f.write(content) | |
| def create_gradle_wrapper(project_dir): | |
| wrapper_dir = os.path.join(project_dir, "gradle", "wrapper") | |
| os.makedirs(wrapper_dir, exist_ok=True) | |
| props_content = """distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| distributionUrl=https\\://services.gradle.org/distributions/gradle-7.6-bin.zip | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| """ | |
| with open(os.path.join(wrapper_dir, "gradle-wrapper.properties"), "w") as f: | |
| f.write(props_content) | |
| def create_app_build_gradle(app_dir, config): | |
| package_name = config.get("package_name", "com.example.webapp") | |
| version_name = config.get("version_name", "1.0.0") | |
| version_code = config.get("version_code", "1") | |
| minify_enabled = "true" if config.get("proguard_enabled") else "false" | |
| split_apk = config.get("split_apk", False) | |
| use_firebase = config.get("actual_firebase_enabled", False) | |
| use_onesignal = bool(config.get("onesignal_app_id", "").strip()) | |
| use_admob = config.get("admob_enabled", False) | |
| dependencies = [] | |
| dependencies.append(" implementation 'androidx.appcompat:appcompat:1.6.1'") | |
| dependencies.append(" implementation 'androidx.webkit:webkit:1.8.0'") | |
| dependencies.append(" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'") | |
| dependencies.append(" implementation 'com.google.android.material:material:1.10.0'") | |
| dependencies.append(" implementation 'com.google.android.gms:play-services-auth:20.7.0'") | |
| if use_firebase: | |
| dependencies.append(" implementation platform('com.google.firebase:firebase-bom:32.7.0')") | |
| dependencies.append(" implementation 'com.google.firebase:firebase-analytics'") | |
| dependencies.append(" implementation 'com.google.firebase:firebase-messaging'") | |
| if use_onesignal: | |
| dependencies.append(" implementation 'com.onesignal:OneSignal:4.114.0'") | |
| if use_admob: | |
| dependencies.append(" implementation 'com.google.android.gms:play-services-ads:22.6.0'") | |
| deps_str = "\n".join(dependencies) | |
| plugins_str = " id 'com.android.application'\n" | |
| if use_firebase: | |
| plugins_str += " id 'com.google.gms.google-services'\n" | |
| if use_onesignal: | |
| plugins_str += " id 'com.onesignal.androidsdk.onesignal-gradle-plugin'\n" | |
| proguard_rules = "" | |
| if config.get("proguard_enabled"): | |
| proguard_rules = """ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'""" | |
| splits_block = "" | |
| if split_apk: | |
| splits_block = """ | |
| splits { | |
| abi { | |
| enable true | |
| reset() | |
| include 'armeabi-v7a', 'arm64-v8a' | |
| universalApk true | |
| } | |
| }""" | |
| content = """plugins {{ | |
| {plugins} | |
| }} | |
| android {{ | |
| namespace '{package_name}' | |
| compileSdk 34 | |
| defaultConfig {{ | |
| applicationId "{package_name}" | |
| minSdk 24 | |
| targetSdk 34 | |
| versionCode {version_code} | |
| versionName "{version_name}" | |
| }} | |
| signingConfigs {{ | |
| release {{ | |
| storeFile file("../release-key.jks") | |
| storePassword "android123" | |
| keyAlias "releasekey" | |
| keyPassword "android123" | |
| }} | |
| }} | |
| buildTypes {{ | |
| release {{ | |
| minifyEnabled {minify_enabled} | |
| shrinkResources {minify_enabled} | |
| {proguard_rules} | |
| signingConfig signingConfigs.release | |
| }} | |
| }} | |
| {splits} | |
| compileOptions {{ | |
| sourceCompatibility JavaVersion.VERSION_17 | |
| targetCompatibility JavaVersion.VERSION_17 | |
| }} | |
| lint {{ | |
| abortOnError false | |
| checkReleaseBuilds false | |
| }} | |
| }} | |
| dependencies {{ | |
| {deps_str} | |
| }} | |
| """.format( | |
| plugins=plugins_str, | |
| package_name=package_name, | |
| version_code=version_code, | |
| version_name=version_name, | |
| minify_enabled=minify_enabled, | |
| proguard_rules=proguard_rules, | |
| splits=splits_block, | |
| deps_str=deps_str | |
| ) | |
| with open(os.path.join(app_dir, "build.gradle"), "w") as f: | |
| f.write(content) | |
| def create_proguard_rules(app_dir, config): | |
| rules = """-keepattributes Signature | |
| -keepattributes *Annotation* | |
| -keep class * extends android.app.Activity | |
| -keep class * extends android.app.Application | |
| -keep class * extends android.app.Service | |
| -keep class * extends android.content.BroadcastReceiver | |
| -keep class * extends android.content.ContentProvider | |
| -dontwarn okhttp3.** | |
| -dontwarn okio.** | |
| -dontwarn javax.annotation.** | |
| -keep class com.google.android.gms.** { *; } | |
| -keep class com.google.firebase.** { *; } | |
| -keep class androidx.** { *; } | |
| -keep class com.onesignal.** { *; } | |
| -dontwarn com.onesignal.** | |
| -keepclassmembers class * { | |
| @android.webkit.JavascriptInterface <methods>; | |
| } | |
| """ | |
| with open(os.path.join(app_dir, "proguard-rules.pro"), "w") as f: | |
| f.write(rules) | |
| def create_android_manifest(manifest_dir, config): | |
| package_name = config.get("package_name", "com.example.webapp") | |
| orientation = config.get("screen_orientation", "unspecified") | |
| if orientation == "portrait": | |
| orientation_str = "portrait" | |
| elif orientation == "landscape": | |
| orientation_str = "landscape" | |
| else: | |
| orientation_str = "unspecified" | |
| permissions = [] | |
| permissions.append(' <uses-permission android:name="android.permission.INTERNET" />') | |
| permissions.append(' <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />') | |
| permissions.append(' <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />') | |
| if config.get("perm_camera"): | |
| permissions.append(' <uses-permission android:name="android.permission.CAMERA" />') | |
| permissions.append(' <uses-feature android:name="android.hardware.camera" android:required="false" />') | |
| if config.get("perm_microphone"): | |
| permissions.append(' <uses-permission android:name="android.permission.RECORD_AUDIO" />') | |
| if config.get("perm_location"): | |
| permissions.append(' <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />') | |
| permissions.append(' <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />') | |
| if config.get("perm_storage"): | |
| permissions.append(' <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />') | |
| permissions.append(' <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />') | |
| if config.get("perm_vibrate"): | |
| permissions.append(' <uses-permission android:name="android.permission.VIBRATE" />') | |
| if config.get("perm_contacts"): | |
| permissions.append(' <uses-permission android:name="android.permission.READ_CONTACTS" />') | |
| if config.get("perm_notifications"): | |
| permissions.append(' <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />') | |
| permissions_str = "\n".join(permissions) | |
| fullscreen_theme = "" | |
| if config.get("fullscreen_mode"): | |
| fullscreen_theme = 'android:theme="@style/Theme.AppCompat.NoActionBar"' | |
| else: | |
| fullscreen_theme = 'android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"' | |
| splash_activity = "" | |
| main_activity_intent = "" | |
| if config.get("splash_enabled"): | |
| splash_activity = """ | |
| <activity | |
| android:name=".SplashActivity" | |
| android:exported="true" | |
| android:screenOrientation="{orientation}" | |
| {fullscreen_theme_attr}> | |
| <intent-filter> | |
| <action android:name="android.intent.action.MAIN" /> | |
| <category android:name="android.intent.category.LAUNCHER" /> | |
| </intent-filter> | |
| </activity>""".format(orientation=orientation_str, fullscreen_theme_attr=fullscreen_theme) | |
| main_activity_intent = "" | |
| else: | |
| main_activity_intent = """ | |
| <intent-filter> | |
| <action android:name="android.intent.action.MAIN" /> | |
| <category android:name="android.intent.category.LAUNCHER" /> | |
| </intent-filter>""" | |
| meta_data = "" | |
| if config.get("admob_enabled"): | |
| meta_data += """ | |
| <meta-data | |
| android:name="com.google.android.gms.ads.APPLICATION_ID" | |
| android:value="ca-app-pub-3940256099942544~3347511713" />""" | |
| content = """<?xml version="1.0" encoding="utf-8"?> | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |
| {permissions} | |
| <application | |
| android:allowBackup="true" | |
| android:icon="@mipmap/ic_launcher" | |
| android:label="{app_name}" | |
| android:supportsRtl="true" | |
| android:usesCleartextTraffic="true" | |
| android:hardwareAccelerated="true" | |
| android:largeHeap="true" | |
| {fullscreen_theme}> | |
| {splash_activity} | |
| <activity | |
| android:name=".MainActivity" | |
| android:exported="true" | |
| android:screenOrientation="{orientation}" | |
| android:configChanges="orientation|screenSize|keyboardHidden" | |
| android:windowSoftInputMode="adjustResize"> | |
| {main_activity_intent} | |
| </activity> | |
| {meta_data} | |
| </application> | |
| </manifest> | |
| """.format( | |
| permissions=permissions_str, | |
| app_name=config.get("app_name", "WebApp").replace('"', '\\"'), | |
| fullscreen_theme=fullscreen_theme, | |
| splash_activity=splash_activity, | |
| orientation=orientation_str, | |
| main_activity_intent=main_activity_intent, | |
| meta_data=meta_data | |
| ) | |
| with open(os.path.join(manifest_dir, "AndroidManifest.xml"), "w") as f: | |
| f.write(content) | |
| def create_main_activity(java_dir, config): | |
| package_name = config.get("package_name", "com.example.webapp") | |
| target_url = config.get("target_url", "https://www.google.com") | |
| enable_js = config.get("enable_javascript", True) | |
| enable_dom = config.get("enable_dom_storage", True) | |
| allow_file_access = config.get("allow_file_access", False) | |
| media_autoplay = config.get("media_autoplay", False) | |
| enable_zoom = config.get("enable_zoom", False) | |
| custom_ua = config.get("custom_user_agent", "") | |
| pull_to_refresh = config.get("pull_to_refresh", False) | |
| multi_windows = config.get("multi_windows", False) | |
| deep_link_intercept = config.get("deep_link_intercept", True) | |
| prevent_screenshots = config.get("prevent_screenshots", False) | |
| clear_cache_exit = config.get("clear_cache_exit", False) | |
| clear_cookies_exit = config.get("clear_cookies_exit", False) | |
| keep_screen_on = config.get("keep_screen_on", False) | |
| fullscreen_mode = config.get("fullscreen_mode", False) | |
| enforce_https = config.get("enforce_https", True) | |
| dev_tools_f12 = config.get("dev_tools_f12", True) | |
| root_detection = config.get("root_detection", True) | |
| anti_frida = config.get("anti_frida", True) | |
| tamper_protection = config.get("tamper_protection", True) | |
| prevent_memory_edit = config.get("prevent_memory_edit", True) | |
| smart_cache = config.get("smart_cache", True) | |
| status_bar_color = config.get("status_bar_color", "#1a1a2e") | |
| bottom_nav = config.get("bottom_nav_enabled", False) | |
| offline_mode = config.get("offline_mode", True) | |
| mixed_content = "WebSettings.MIXED_CONTENT_NEVER_ALLOW" if enforce_https else "WebSettings.MIXED_CONTENT_ALWAYS_ALLOW" | |
| use_admob = config.get("admob_enabled", False) | |
| onesignal_id = config.get("onesignal_app_id", "").strip() | |
| use_onesignal = bool(onesignal_id) | |
| google_web_client_id = config.get("google_web_client_id", "") | |
| supabase_url = config.get("supabase_url", "") | |
| supabase_anon_key = config.get("supabase_anon_key", "") | |
| imports = [] | |
| imports.append("package {};".format(package_name)) | |
| imports.append("") | |
| imports.append("import android.app.Activity;") | |
| imports.append("import android.content.Context;") | |
| imports.append("import android.content.Intent;") | |
| imports.append("import android.content.pm.PackageManager;") | |
| imports.append("import android.graphics.Bitmap;") | |
| imports.append("import android.graphics.Color;") | |
| imports.append("import android.net.ConnectivityManager;") | |
| imports.append("import android.net.Network;") | |
| imports.append("import android.net.NetworkCapabilities;") | |
| imports.append("import android.net.NetworkInfo;") | |
| imports.append("import android.net.NetworkRequest;") | |
| imports.append("import android.net.Uri;") | |
| imports.append("import android.os.Build;") | |
| imports.append("import android.os.Bundle;") | |
| imports.append("import android.util.Log;") | |
| imports.append("import android.view.View;") | |
| imports.append("import android.view.Window;") | |
| imports.append("import android.view.WindowManager;") | |
| imports.append("import android.webkit.ConsoleMessage;") | |
| imports.append("import android.webkit.CookieManager;") | |
| imports.append("import android.webkit.GeolocationPermissions;") | |
| imports.append("import android.webkit.JavascriptInterface;") | |
| imports.append("import android.webkit.PermissionRequest;") | |
| imports.append("import android.webkit.ValueCallback;") | |
| imports.append("import android.webkit.WebChromeClient;") | |
| imports.append("import android.webkit.WebResourceRequest;") | |
| imports.append("import android.webkit.WebSettings;") | |
| imports.append("import android.webkit.WebView;") | |
| imports.append("import android.webkit.WebViewClient;") | |
| imports.append("import android.widget.FrameLayout;") | |
| imports.append("import android.widget.LinearLayout;") | |
| imports.append("import android.widget.ImageButton;") | |
| imports.append("import android.widget.ProgressBar;") | |
| imports.append("import android.widget.Toast;") | |
| imports.append("import androidx.appcompat.app.AppCompatActivity;") | |
| imports.append("import androidx.core.app.ActivityCompat;") | |
| imports.append("import androidx.core.content.ContextCompat;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignIn;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignInAccount;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignInClient;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignInOptions;") | |
| imports.append("import com.google.android.gms.common.api.ApiException;") | |
| imports.append("import com.google.android.gms.tasks.Task;") | |
| if use_onesignal: | |
| imports.append("import com.onesignal.OneSignal;") | |
| if pull_to_refresh: | |
| imports.append("import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;") | |
| imports.append("import java.io.File;") | |
| imports.append("import java.io.BufferedReader;") | |
| imports.append("import java.io.FileReader;") | |
| imports.append("import java.util.ArrayList;") | |
| imports.append("import java.util.List;") | |
| imports.append("") | |
| root_detection_code = "" | |
| root_detection_method = "" | |
| if root_detection: | |
| root_detection_method = """ | |
| private boolean isRootedOrEmulator() { | |
| String[] rootPaths = {"/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su"}; | |
| for (String path : rootPaths) { | |
| if (new File(path).exists()) return true; | |
| } | |
| String buildTags = android.os.Build.TAGS; | |
| if (buildTags != null && buildTags.contains("test-keys")) return true; | |
| if (Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion")) return true; | |
| return false; | |
| }""" | |
| root_detection_code = """ | |
| if (isRootedOrEmulator()) { | |
| Toast.makeText(this, "Security Alert: Device not secure.", Toast.LENGTH_LONG).show(); | |
| finish(); | |
| return; | |
| }""" | |
| frida_method = "" | |
| if anti_frida: | |
| frida_method = """ | |
| private boolean detectFridaAndXposed() { | |
| try { | |
| java.net.Socket socket = new java.net.Socket("127.0.0.1", 27042); | |
| socket.close(); | |
| return true; | |
| } catch (Exception e) { } | |
| try { | |
| throw new Exception("Stack trace check"); | |
| } catch (Exception e) { | |
| for (StackTraceElement element : e.getStackTrace()) { | |
| if (element.getClassName().contains("de.robv.android.xposed.") || element.getClassName().contains("com.saurik.substrate.")) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| }""" | |
| root_detection_code += """ | |
| if (detectFridaAndXposed()) { | |
| Toast.makeText(this, "Security Alert: Hooking framework detected.", Toast.LENGTH_LONG).show(); | |
| finish(); | |
| return; | |
| }""" | |
| tamper_method = "" | |
| if tamper_protection: | |
| tamper_method = """ | |
| private boolean verifyAppSignature() { | |
| try { | |
| android.content.pm.PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), android.content.pm.PackageManager.GET_SIGNATURES); | |
| for (android.content.pm.Signature signature : pInfo.signatures) { | |
| java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA"); | |
| md.update(signature.toByteArray()); | |
| String currentSignature = android.util.Base64.encodeToString(md.digest(), android.util.Base64.DEFAULT).trim(); | |
| if (currentSignature != null && currentSignature.length() > 0) return true; | |
| } | |
| } catch (Exception e) { e.printStackTrace(); } | |
| return false; | |
| }""" | |
| root_detection_code += """ | |
| if (!verifyAppSignature()) { | |
| Toast.makeText(this, "Security Alert: App integrity compromised.", Toast.LENGTH_LONG).show(); | |
| finish(); | |
| return; | |
| }""" | |
| memory_method = "" | |
| if prevent_memory_edit: | |
| memory_method = """ | |
| private boolean detectMemoryEditors() { | |
| String[] suspiciousPackages = {"com.catchdroid.gameguardian", "org.sbtools.gamehack", "com.cih.gamecih", "com.silead.guardian"}; | |
| PackageManager pm = getPackageManager(); | |
| for (String pkg : suspiciousPackages) { | |
| try { | |
| pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES); | |
| return true; | |
| } catch (PackageManager.NameNotFoundException e) { } | |
| } | |
| return false; | |
| }""" | |
| root_detection_code += """ | |
| if (detectMemoryEditors()) { | |
| Toast.makeText(this, "Security Alert: Memory Editor detected.", Toast.LENGTH_LONG).show(); | |
| finish(); | |
| return; | |
| }""" | |
| eruda_injection = "" | |
| if dev_tools_f12: | |
| eruda_injection = """ | |
| view.evaluateJavascript("javascript:(function() { " + | |
| "if(document.getElementById('eruda-plugin')) return; " + | |
| "var script = document.createElement('script'); " + | |
| "script.id = 'eruda-plugin'; " + | |
| "script.src = 'https://cdn.jsdelivr.net/npm/eruda'; " + | |
| "document.body.appendChild(script); " + | |
| "script.onload = function() { eruda.init(); }; " + | |
| "})();", null);""" | |
| cache_mode = "WebSettings.LOAD_CACHE_ELSE_NETWORK" if smart_cache else "WebSettings.LOAD_DEFAULT" | |
| network_reconnect = """ | |
| private void setupNetworkListener() { | |
| ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
| cm.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { | |
| @Override | |
| public void onAvailable(Network network) { | |
| runOnUiThread(() -> { | |
| if (webView.getUrl() != null && webView.getUrl().contains("offline.html")) { | |
| webView.loadUrl(TARGET_URL); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| }""" | |
| onesignal_setup = "" | |
| if use_onesignal: | |
| onesignal_setup = """ | |
| OneSignal.setLogLevel(OneSignal.LOG_LEVEL.VERBOSE, OneSignal.LOG_LEVEL.NONE); | |
| OneSignal.initWithContext(this); | |
| OneSignal.setAppId("{os_id}"); | |
| OneSignal.promptForPushNotifications(); | |
| """.format(os_id=onesignal_id) | |
| content = """{imports_str} | |
| public class MainActivity extends AppCompatActivity {{ | |
| private WebView webView; | |
| private static final String TARGET_URL = "{target_url}"; | |
| private ValueCallback<Uri[]> fileUploadCallback; | |
| private static final int FILE_CHOOSER_RESULT = 1002; | |
| private GoogleSignInClient mGoogleSignInClient; | |
| private static final int RC_SIGN_IN = 9001; | |
| @Override | |
| protected void onCreate(Bundle savedInstanceState) {{ | |
| super.onCreate(savedInstanceState); | |
| {root_det_c} | |
| {screenshot_c} | |
| {os_setup} | |
| webView = new WebView(this); | |
| webView.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); | |
| webView.setBackgroundColor(Color.parseColor("#1a1a2e")); | |
| WebSettings settings = webView.getSettings(); | |
| settings.setJavaScriptEnabled(true); | |
| settings.setDomStorageEnabled(true); | |
| settings.setAllowFileAccess(true); | |
| settings.setCacheMode({cache_mode}); | |
| settings.setDatabaseEnabled(true); | |
| settings.setMixedContentMode({mixed_content}); | |
| CookieManager cookieManager = CookieManager.getInstance(); | |
| cookieManager.setAcceptCookie(true); | |
| cookieManager.setAcceptThirdPartyCookies(webView, true); | |
| GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) | |
| .requestIdToken("{client_id}") | |
| .requestEmail() | |
| .build(); | |
| mGoogleSignInClient = GoogleSignIn.getClient(this, gso); | |
| webView.addJavascriptInterface(new WebAppInterface(this), "AndroidBridge"); | |
| setupNetworkListener(); | |
| webView.setWebViewClient(new WebViewClient() {{ | |
| @Override | |
| public void onPageFinished(WebView view, String url) {{ | |
| super.onPageFinished(view, url); | |
| {eruda_inj} | |
| }} | |
| @Override | |
| public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {{ | |
| super.onReceivedError(view, errorCode, description, failingUrl); | |
| if (!isNetworkAvailable()) {{ | |
| view.loadUrl("file:///android_asset/offline.html"); | |
| }} | |
| }} | |
| }}); | |
| webView.setWebChromeClient(new WebChromeClient() {{ | |
| @Override | |
| public boolean onConsoleMessage(ConsoleMessage consoleMessage) {{ | |
| Log.d("SkyData-WebView", consoleMessage.message() + " -- From line " | |
| + consoleMessage.lineNumber() + " of " | |
| + consoleMessage.sourceId()); | |
| return super.onConsoleMessage(consoleMessage); | |
| }} | |
| @Override | |
| public boolean onShowFileChooser(WebView wv, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {{ | |
| fileUploadCallback = filePathCallback; | |
| Intent intent = fileChooserParams.createIntent(); | |
| try {{ | |
| startActivityForResult(intent, FILE_CHOOSER_RESULT); | |
| }} catch (Exception e) {{ return false; }} | |
| return true; | |
| }} | |
| }}); | |
| setContentView(webView); | |
| if (!isNetworkAvailable()) {{ | |
| webView.loadUrl("file:///android_asset/offline.html"); | |
| }} else {{ | |
| webView.loadUrl(TARGET_URL); | |
| }} | |
| }} | |
| public class WebAppInterface {{ | |
| Context mContext; | |
| WebAppInterface(Context c) {{ mContext = c; }} | |
| @JavascriptInterface | |
| public void startGoogleSignIn() {{ | |
| Intent signInIntent = mGoogleSignInClient.getSignInIntent(); | |
| startActivityForResult(signInIntent, RC_SIGN_IN); | |
| }} | |
| @JavascriptInterface | |
| public String getSupabaseUrl() {{ return "{supa_url}"; }} | |
| @JavascriptInterface | |
| public String getSupabaseKey() {{ return "{supa_key}"; }} | |
| }} | |
| @Override | |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) {{ | |
| super.onActivityResult(requestCode, resultCode, data); | |
| if (requestCode == RC_SIGN_IN) {{ | |
| Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data); | |
| try {{ | |
| GoogleSignInAccount account = task.getResult(ApiException.class); | |
| String idToken = account.getIdToken(); | |
| webView.post(() -> {{ | |
| webView.evaluateJavascript("javascript:handleGoogleLoginSuccess('" + idToken + "')", null); | |
| }}); | |
| }} catch (ApiException e) {{ | |
| Log.e("SkyData-Auth", "Login Failed Code: " + e.getStatusCode()); | |
| webView.post(() -> {{ | |
| webView.evaluateJavascript("javascript:handleGoogleLoginError('" + e.getStatusCode() + "')", null); | |
| }}); | |
| }} | |
| return; | |
| }} | |
| }} | |
| @Override | |
| public void onBackPressed() {{ | |
| if (webView.canGoBack()) {{ webView.goBack(); }} | |
| else {{ super.onBackPressed(); }} | |
| }} | |
| private boolean isNetworkAvailable() {{ | |
| ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); | |
| if (cm != null) {{ | |
| NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); | |
| return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); | |
| }} | |
| return false; | |
| }} | |
| {root_method} | |
| {frida_method} | |
| {tamper_method} | |
| {memory_method} | |
| {network_reconnect} | |
| }} | |
| """.format( | |
| imports_str="\n".join(imports), | |
| target_url=target_url.replace('"', '\\"'), | |
| screenshot_c= "getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);" if prevent_screenshots else "", | |
| cache_mode=cache_mode, | |
| mixed_content=mixed_content, | |
| client_id=google_web_client_id, | |
| eruda_inj=eruda_injection, | |
| supa_url=supabase_url, | |
| supa_key=supabase_anon_key, | |
| root_det_c=root_detection_code, | |
| root_method=root_detection_method, | |
| frida_method=frida_method, | |
| tamper_method=tamper_method, | |
| memory_method=memory_method, | |
| network_reconnect=network_reconnect, | |
| os_setup=onesignal_setup | |
| ) | |
| with open(os.path.join(java_dir, "MainActivity.java"), "w") as f: | |
| f.write(content) | |
| def create_splash_activity(java_dir, config): | |
| package_name = config.get("package_name", "com.example.webapp") | |
| splash_text = config.get("splash_text", "Loading...") | |
| splash_text_color = config.get("splash_text_color", "#FFFFFF") | |
| splash_bg_color = config.get("splash_bg_color", "#1a1a2e") | |
| has_splash_image = config.get("has_splash_image", False) | |
| image_code = "" | |
| if has_splash_image: | |
| image_code = """ | |
| ImageView splashImage = new ImageView(this); | |
| try { | |
| java.io.InputStream is = getAssets().open("splash_media"); | |
| android.graphics.drawable.Drawable d = android.graphics.drawable.Drawable.createFromStream(is, null); | |
| splashImage.setImageDrawable(d); | |
| splashImage.setScaleType(ImageView.ScaleType.FIT_CENTER); | |
| LinearLayout.LayoutParams imgParams = new LinearLayout.LayoutParams(600, 600); | |
| imgParams.gravity = android.view.Gravity.CENTER; | |
| splashImage.setLayoutParams(imgParams); | |
| layout.addView(splashImage); | |
| is.close(); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| }""" | |
| content = """package {package_name}; | |
| import android.content.Intent; | |
| import android.graphics.Color; | |
| import android.os.Bundle; | |
| import android.os.Handler; | |
| import android.view.Gravity; | |
| import android.view.WindowManager; | |
| import android.widget.ImageView; | |
| import android.widget.LinearLayout; | |
| import android.widget.TextView; | |
| import androidx.appcompat.app.AppCompatActivity; | |
| public class SplashActivity extends AppCompatActivity {{ | |
| @Override | |
| protected void onCreate(Bundle savedInstanceState) {{ | |
| super.onCreate(savedInstanceState); | |
| getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); | |
| LinearLayout layout = new LinearLayout(this); | |
| layout.setOrientation(LinearLayout.VERTICAL); | |
| layout.setGravity(Gravity.CENTER); | |
| layout.setBackgroundColor(Color.parseColor("{bg_color}")); | |
| layout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); | |
| {image_code} | |
| TextView textView = new TextView(this); | |
| textView.setText("{splash_text}"); | |
| textView.setTextColor(Color.parseColor("{text_color}")); | |
| textView.setTextSize(24); | |
| textView.setGravity(Gravity.CENTER); | |
| LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); | |
| textParams.topMargin = 40; | |
| textView.setLayoutParams(textParams); | |
| layout.addView(textView); | |
| setContentView(layout); | |
| new Handler().postDelayed(new Runnable() {{ | |
| @Override | |
| public void run() {{ | |
| Intent intent = new Intent(SplashActivity.this, MainActivity.class); | |
| startActivity(intent); | |
| finish(); | |
| }} | |
| }}, 3000); | |
| }} | |
| }} | |
| """.format( | |
| package_name=package_name, | |
| bg_color=splash_bg_color if splash_bg_color else "#1a1a2e", | |
| image_code=image_code, | |
| splash_text=splash_text.replace('"', '\\"') if splash_text else "Loading...", | |
| text_color=splash_text_color if splash_text_color else "#FFFFFF" | |
| ) | |
| with open(os.path.join(java_dir, "SplashActivity.java"), "w") as f: | |
| f.write(content) | |
| def save_app_icon(res_dir, icon_b64): | |
| sizes = {"mipmap-mdpi": 48, "mipmap-hdpi": 72, "mipmap-xhdpi": 96, "mipmap-xxhdpi": 144, "mipmap-xxxhdpi": 192} | |
| try: | |
| if "," in icon_b64: | |
| icon_b64 = icon_b64.split(",")[1] | |
| icon_bytes = base64.b64decode(icon_b64) | |
| for folder, size in sizes.items(): | |
| mipmap_dir = os.path.join(res_dir, folder) | |
| os.makedirs(mipmap_dir, exist_ok=True) | |
| icon_path = os.path.join(mipmap_dir, "ic_launcher.png") | |
| with open(icon_path, "wb") as f: | |
| f.write(icon_bytes) | |
| except Exception as e: | |
| for folder, size in sizes.items(): | |
| mipmap_dir = os.path.join(res_dir, folder) | |
| os.makedirs(mipmap_dir, exist_ok=True) | |
| create_default_icon(os.path.join(mipmap_dir, "ic_launcher.png")) | |
| def create_default_icon(path): | |
| import struct, zlib | |
| width = 48 | |
| height = 48 | |
| def create_png(w, h, r, g, b): | |
| def chunk(chunk_type, data): | |
| c = chunk_type + data | |
| crc = zlib.crc32(c) & 0xFFFFFFFF | |
| return struct.pack(">I", len(data)) + c + struct.pack(">I", crc) | |
| header = b'\x89PNG\r\n\x1a\n' | |
| ihdr = chunk(b'IHDR', struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) | |
| raw_data = b'' | |
| for y in range(h): | |
| raw_data += b'\x00' | |
| for x in range(w): | |
| raw_data += bytes([r, g, b]) | |
| compressed = zlib.compress(raw_data) | |
| idat = chunk(b'IDAT', compressed) | |
| iend = chunk(b'IEND', b'') | |
| return header + ihdr + idat + iend | |
| png_data = create_png(width, height, 0x4e, 0x54, 0xc8) | |
| with open(path, "wb") as f: | |
| f.write(png_data) | |
| def setup_project(build_id, config): | |
| project_dir = os.path.join(BUILD_DIR_BASE, build_id, "project") | |
| os.makedirs(project_dir, exist_ok=True) | |
| package_name = config.get("package_name", "com.example.webapp") | |
| package_path = package_name.replace(".", "/") | |
| app_dir = os.path.join(project_dir, "app") | |
| src_main = os.path.join(app_dir, "src", "main") | |
| java_dir = os.path.join(src_main, "java", package_path) | |
| res_dir = os.path.join(src_main, "res") | |
| assets_dir = os.path.join(src_main, "assets") | |
| os.makedirs(java_dir, exist_ok=True) | |
| os.makedirs(res_dir, exist_ok=True) | |
| os.makedirs(assets_dir, exist_ok=True) | |
| os.makedirs(os.path.join(res_dir, "values"), exist_ok=True) | |
| use_firebase = config.get("firebase_enabled", False) | |
| use_onesignal = bool(config.get("onesignal_app_id", "").strip()) | |
| firebase_json = config.get("firebase_json", "") | |
| actual_firebase_enabled = False | |
| if use_firebase and firebase_json.strip(): | |
| try: | |
| parsed = json.loads(firebase_json) | |
| if "project_info" in parsed: | |
| os.makedirs(app_dir, exist_ok=True) | |
| with open(os.path.join(app_dir, "google-services.json"), "w") as f: | |
| json.dump(parsed, f, indent=2) | |
| actual_firebase_enabled = True | |
| else: | |
| LOG_STORE[build_id].append("⚠️ WARNING: Firebase JSON is missing 'project_info'. Firebase integration skipped.") | |
| except json.JSONDecodeError: | |
| LOG_STORE[build_id].append("⚠️ WARNING: Firebase JSON is invalid. Firebase integration skipped.") | |
| config["actual_firebase_enabled"] = actual_firebase_enabled | |
| create_build_gradle_root(project_dir, actual_firebase_enabled, use_onesignal) | |
| create_settings_gradle(project_dir, config.get("app_name", "WebApp")) | |
| create_gradle_properties(project_dir) | |
| create_gradle_wrapper(project_dir) | |
| create_app_build_gradle(app_dir, config) | |
| create_proguard_rules(app_dir, config) | |
| create_android_manifest(src_main, config) | |
| create_main_activity(java_dir, config) | |
| if config.get("splash_enabled"): | |
| create_splash_activity(java_dir, config) | |
| icon_b64 = config.get("app_icon_b64", "") | |
| if icon_b64: | |
| save_app_icon(res_dir, icon_b64) | |
| else: | |
| for folder in ["mipmap-mdpi", "mipmap-hdpi", "mipmap-xhdpi", "mipmap-xxhdpi", "mipmap-xxxhdpi"]: | |
| mipmap_dir = os.path.join(res_dir, folder) | |
| os.makedirs(mipmap_dir, exist_ok=True) | |
| create_default_icon(os.path.join(mipmap_dir, "ic_launcher.png")) | |
| splash_media_b64 = config.get("splash_media_b64", "") | |
| if splash_media_b64 and config.get("splash_enabled"): | |
| try: | |
| if "," in splash_media_b64: | |
| splash_media_b64 = splash_media_b64.split(",")[1] | |
| media_bytes = base64.b64decode(splash_media_b64) | |
| with open(os.path.join(assets_dir, "splash_media"), "wb") as f: | |
| f.write(media_bytes) | |
| config["has_splash_image"] = True | |
| create_splash_activity(java_dir, config) | |
| except Exception as e: | |
| config["has_splash_image"] = False | |
| if config.get("offline_mode", True): | |
| with open(os.path.join(assets_dir, "offline.html"), "w") as f: | |
| f.write(generate_offline_html()) | |
| strings_xml = """<?xml version="1.0" encoding="utf-8"?> | |
| <resources> | |
| <string name="app_name">{app_name}</string> | |
| </resources> | |
| """.format(app_name=config.get("app_name", "WebApp").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)) | |
| with open(os.path.join(res_dir, "values", "strings.xml"), "w") as f: | |
| f.write(strings_xml) | |
| colors_xml = """<?xml version="1.0" encoding="utf-8"?> | |
| <resources> | |
| <color name="colorPrimary">{status_color}</color> | |
| <color name="colorPrimaryDark">{status_color}</color> | |
| <color name="colorAccent">#e94560</color> | |
| </resources> | |
| """.format(status_color=config.get("status_bar_color", "#1a1a2e")) | |
| with open(os.path.join(res_dir, "values", "colors.xml"), "w") as f: | |
| f.write(colors_xml) | |
| styles_xml = """<?xml version="1.0" encoding="utf-8"?> | |
| <resources> | |
| <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> | |
| <item name="colorPrimary">@color/colorPrimary</item> | |
| <item name="colorPrimaryDark">@color/colorPrimaryDark</item> | |
| <item name="colorAccent">@color/colorAccent</item> | |
| </style> | |
| </resources> | |
| """ | |
| with open(os.path.join(res_dir, "values", "styles.xml"), "w") as f: | |
| f.write(styles_xml) | |
| if os.path.exists(KEYSTORE_PATH): | |
| shutil.copy2(KEYSTORE_PATH, os.path.join(project_dir, "release-key.jks")) | |
| local_props = "sdk.dir={}\n".format(os.environ.get("ANDROID_SDK_ROOT", "/opt/android-sdk")) | |
| with open(os.path.join(project_dir, "local.properties"), "w") as f: | |
| f.write(local_props) | |
| return project_dir | |
| def run_build(build_id, config): | |
| LOG_STORE[build_id] = [] | |
| BUILD_STATUS[build_id] = "running" | |
| def log(msg): | |
| timestamp = datetime.now().strftime("%H:%M:%S") | |
| LOG_STORE[build_id].append("[{}] {}".format(timestamp, msg)) | |
| try: | |
| log("=== Build Started ===") | |
| log("App Name: {}".format(config.get("app_name", "WebApp"))) | |
| log("Package: {}".format(config.get("package_name", "com.example.webapp"))) | |
| log("Setting up project structure...") | |
| project_dir = setup_project(build_id, config) | |
| log("Project structure created successfully.") | |
| log("Starting Gradle build...") | |
| build_format = config.get("build_format", "apk").lower() | |
| if build_format == "aab": | |
| gradle_task = "bundleRelease" | |
| else: | |
| gradle_task = "assembleRelease" | |
| gradle_bin = os.path.join(os.environ.get("GRADLE_HOME", "/opt/gradle/gradle-7.6"), "bin", "gradle") | |
| cmd = [ | |
| gradle_bin, | |
| gradle_task, | |
| "--parallel", | |
| "--build-cache", | |
| "--no-daemon", | |
| "--stacktrace" | |
| ] | |
| log("Running: {}".format(" ".join(cmd))) | |
| env = os.environ.copy() | |
| env["ANDROID_SDK_ROOT"] = os.environ.get("ANDROID_SDK_ROOT", "/opt/android-sdk") | |
| env["ANDROID_HOME"] = os.environ.get("ANDROID_HOME", "/opt/android-sdk") | |
| env["JAVA_HOME"] = os.environ.get("JAVA_HOME", "/usr/lib/jvm/java-17-openjdk-amd64") | |
| process = subprocess.Popen( | |
| cmd, | |
| cwd=project_dir, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| env=env, | |
| bufsize=1 | |
| ) | |
| for line in iter(process.stdout.readline, ""): | |
| stripped = line.rstrip() | |
| if stripped: | |
| log(stripped) | |
| process.wait() | |
| if process.returncode == 0: | |
| log("=== BUILD SUCCESSFUL ===") | |
| if build_format == "aab": | |
| output_path = os.path.join(project_dir, "app", "build", "outputs", "bundle", "release", "app-release.aab") | |
| else: | |
| base_apk_dir = os.path.join(project_dir, "app", "build", "outputs", "apk", "release") | |
| output_path = os.path.join(base_apk_dir, "app-universal-release.apk") | |
| if not os.path.exists(output_path): | |
| output_path = os.path.join(base_apk_dir, "app-release.apk") | |
| if not os.path.exists(output_path): | |
| output_path = os.path.join(base_apk_dir, "app-release-unsigned.apk") | |
| if os.path.exists(output_path): | |
| artifact_name = "{}.{}".format(config.get("app_name", "app").replace(" ", "_"), "aab" if build_format == "aab" else "apk") | |
| final_path = os.path.join(BUILD_DIR_BASE, build_id, artifact_name) | |
| shutil.copy2(output_path, final_path) | |
| BUILD_ARTIFACTS[build_id] = final_path | |
| log("Artifact saved: {}".format(artifact_name)) | |
| BUILD_STATUS[build_id] = "success" | |
| else: | |
| log("ERROR: Build output file not found at expected path.") | |
| BUILD_STATUS[build_id] = "failed" | |
| else: | |
| log("=== BUILD FAILED (exit code: {}) ===".format(process.returncode)) | |
| BUILD_STATUS[build_id] = "failed" | |
| full_log = "\n".join(LOG_STORE[build_id]) | |
| ai_analysis = analyze_logs_ai(full_log) | |
| log("") | |
| log("=== AI Log Analysis (Arabic) ===") | |
| for analysis_line in ai_analysis.split("\n"): | |
| log(analysis_line) | |
| except Exception as e: | |
| log("CRITICAL ERROR: {}".format(str(e))) | |
| BUILD_STATUS[build_id] = "failed" | |
| # ========================================== | |
| # دمج الواجهة (HTML) لكي لا تحتاج لملف index.html أبداً! | |
| # ========================================== | |
| HTML_CONTENT = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Android APK/AAB Builder SaaS</title> | |
| <style> | |
| *,*::before,*::after{margin:0;padding:0;box-sizing:border-box} | |
| :root{--bg:#0a0a1a;--surface:#111128;--surface2:#1a1a3e;--surface3:#222255;--border:#2a2a5a;--accent:#e94560;--accent2:#ff6b81;--text:#e0e0f0;--text2:#a0a0c0;--success:#00e676;--warning:#ffc107;--error:#ff5252;--radius:12px;--font:'Segoe UI',system-ui,-apple-system,sans-serif;} | |
| html{scroll-behavior:smooth} | |
| body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;line-height:1.6;overflow-x:hidden;} | |
| .container{max-width:800px;margin:0 auto;padding:16px;} | |
| header{text-align:center;padding:30px 16px 20px;background:linear-gradient(135deg,#0f0f2e 0%,#1a1a4e 50%,#0f0f2e 100%);border-bottom:2px solid var(--accent);margin-bottom:20px;} | |
| header h1{font-size:1.6em;background:linear-gradient(135deg,var(--accent),var(--accent2),#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:6px;font-weight:800;letter-spacing:-0.5px;} | |
| header p{color:var(--text2);font-size:0.9em;} | |
| .badge{display:inline-block;background:var(--accent);color:#fff;padding:2px 10px;border-radius:20px;font-size:0.7em;margin-top:8px;font-weight:600;} | |
| .sha1-card{background:linear-gradient(135deg,#1a1a3e,#2a2a5a);border:1px solid var(--accent);border-radius:var(--radius);padding:16px;margin-bottom:20px;text-align:center;} | |
| .sha1-card h3{font-size:0.9em;color:var(--accent2);margin-bottom:8px;} | |
| .sha1-value{font-family:'Courier New',monospace;font-size:1.1em;color:var(--success);background:rgba(0,230,118,0.1);padding:8px 12px;border-radius:8px;display:inline-block;word-break:break-all;margin-bottom:10px;} | |
| .copy-btn{background:var(--accent);color:#fff;border:none;padding:8px 20px;border-radius:6px;font-size:0.85em;cursor:pointer;transition:all 0.3s; text-decoration:none; display:inline-block;} | |
| .copy-btn:hover{background:#c73652;transform:translateY(-1px);} | |
| .copy-btn.copied{background:var(--success);color:#000;} | |
| .accordion{margin-bottom:12px;border-radius:var(--radius);overflow:hidden;border:1px solid var(--border);background:var(--surface);transition:all 0.3s ease;} | |
| .accordion:hover{border-color:var(--accent);} | |
| .accordion-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;cursor:pointer;user-select:none;background:var(--surface);transition:background 0.3s;} | |
| .accordion-header:hover{background:var(--surface2);} | |
| .accordion-header .section-icon{font-size:1.3em;margin-right:12px;flex-shrink:0;} | |
| .accordion-header .section-title{flex:1;font-weight:600;font-size:0.95em;} | |
| .accordion-header .section-count{background:var(--surface3);color:var(--text2);padding:2px 8px;border-radius:10px;font-size:0.75em;margin-right:10px;} | |
| .accordion-arrow{font-size:0.8em;transition:transform 0.3s;color:var(--text2);} | |
| .accordion.open .accordion-arrow{transform:rotate(180deg);} | |
| .accordion-body{max-height:0;overflow:hidden;transition:max-height 0.4s ease;background:var(--surface2);} | |
| .accordion.open .accordion-body{max-height:5000px;} | |
| .accordion-content{padding:16px 18px;} | |
| .form-group{margin-bottom:16px;} | |
| .form-group label{display:block;font-size:0.85em;color:var(--text2);margin-bottom:6px;font-weight:500;} | |
| .form-group label .required{color:var(--accent);} | |
| input[type="text"],input[type="number"],input[type="url"],input[type="email"],input[type="color"],input[type="password"],select,textarea{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:0.9em;font-family:var(--font);outline:none;transition:border-color 0.3s,box-shadow 0.3s;} | |
| input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(233,69,96,0.15);} | |
| textarea{resize:vertical;min-height:80px;font-family:'Courier New',monospace;font-size:0.8em;} | |
| input[type="color"]{padding:4px;height:42px;cursor:pointer;} | |
| input[type="password"]{font-family:'Courier New',monospace;} | |
| select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a0a0c0' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px;} | |
| .toggle-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.05);} | |
| .toggle-row:last-child{border-bottom:none;} | |
| .toggle-label{font-size:0.88em;color:var(--text);flex:1;padding-right:10px;} | |
| .toggle-label small{display:block;color:var(--text2);font-size:0.82em;margin-top:2px;} | |
| .switch{position:relative;width:48px;height:26px;flex-shrink:0;} | |
| .switch input{opacity:0;width:0;height:0;} | |
| .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:var(--surface3);border-radius:26px;transition:0.3s;} | |
| .slider:before{content:"";position:absolute;height:20px;width:20px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:0.3s;} | |
| .switch input:checked + .slider{background:var(--accent);} | |
| .switch input:checked + .slider:before{transform:translateX(22px);} | |
| .file-upload-zone{border:2px dashed var(--border);border-radius:var(--radius);padding:20px;text-align:center;cursor:pointer;transition:all 0.3s;background:var(--bg);} | |
| .file-upload-zone:hover{border-color:var(--accent);background:rgba(233,69,96,0.05);} | |
| .file-upload-zone p{color:var(--text2);font-size:0.85em;margin-top:6px;} | |
| .file-upload-zone .upload-icon{font-size:2em;margin-bottom:4px;} | |
| .file-upload-zone img{max-width:80px;max-height:80px;border-radius:8px;margin-top:8px;} | |
| .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px;} | |
| @media(max-width:500px){.grid-2{grid-template-columns:1fr}} | |
| .build-btn{display:block;width:100%;padding:16px;background:linear-gradient(135deg,var(--accent),#a855f7);color:#fff;border:none;border-radius:var(--radius);font-size:1.1em;font-weight:700;cursor:pointer;transition:all 0.3s;letter-spacing:0.5px;margin-top:20px;position:relative;overflow:hidden;} | |
| .build-btn:hover{transform:translateY(-2px);box-shadow:0 8px 30px rgba(233,69,96,0.3);} | |
| .build-btn:disabled{opacity:0.6;cursor:not-allowed;transform:none;} | |
| .build-btn .spinner{display:none;width:20px;height:20px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;margin-right:10px;vertical-align:middle;} | |
| .build-btn.loading .spinner{display:inline-block;} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| #log-panel{display:none;margin-top:20px;border-radius:var(--radius);overflow:hidden;border:1px solid var(--border);background:var(--surface);} | |
| #log-panel.active{display:block;} | |
| .log-header{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:var(--surface2);border-bottom:1px solid var(--border);} | |
| .log-header h3{font-size:0.95em;font-weight:600;} | |
| .log-status{padding:3px 12px;border-radius:20px;font-size:0.75em;font-weight:600;} | |
| .log-status.running{background:var(--warning);color:#000} | |
| .log-status.success{background:var(--success);color:#000} | |
| .log-status.failed{background:var(--error);color:#fff} | |
| #log-output{padding:14px;max-height:500px;overflow-y:auto;font-family:'Courier New',monospace;font-size:0.78em;line-height:1.7;background:#050510;color:#88ff88;white-space:pre-wrap;word-break:break-all;} | |
| #log-output .error-line{color:var(--error)} | |
| #log-output .success-line{color:var(--success)} | |
| #log-output .ai-line{color:#ffd700;font-weight:bold} | |
| #download-section{display:none;margin-top:16px;text-align:center;} | |
| #download-section.active{display:block;} | |
| .download-btn{display:inline-block;padding:14px 40px;background:linear-gradient(135deg,var(--success),#00c853);color:#000;border:none;border-radius:var(--radius);font-size:1em;font-weight:700;cursor:pointer;text-decoration:none;transition:all 0.3s;} | |
| .download-btn:hover{transform:translateY(-2px);box-shadow:0 8px 30px rgba(0,230,118,0.3);} | |
| footer{text-align:center;padding:30px 16px;color:var(--text2);font-size:0.8em;border-top:1px solid var(--border);margin-top:30px;} | |
| .perm-grid{display:grid;grid-template-columns:1fr;gap:0;} | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Android APK/AAB Builder</h1> | |
| <p>Build production-ready Android apps from any URL</p> | |
| <span class="badge">AAA Studio Features • Cloud Build</span> | |
| </header> | |
| <div class="container"> | |
| <form id="buildForm" onsubmit="handleBuild(event)" autocomplete="off"> | |
| <div class="sha1-card" id="sha1Card" style="display:none"> | |
| <h3>🔐 Keystore SHA-1 Fingerprint</h3> | |
| <div class="sha1-value" id="sha1Value">Loading...</div> | |
| <br> | |
| <button type="button" class="copy-btn" id="copySha1Btn" onclick="copySha1()">Copy to Clipboard</button> | |
| <a href="/download_keystore" class="copy-btn" style="background:#a855f7; margin-left:10px;">📥 Download JKS</a> | |
| <p style="font-size:0.75em;color:var(--text2);margin-top:8px">لثبات البصمة للأبد: قم بتحميل ملف JKS ورفعه بجوار app.py ولن يتغير الـ SHA-1 أبداً.</p> | |
| </div> | |
| <div class="accordion open" id="acc-security-keys"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-security-keys')"> | |
| <span class="section-icon">🔐</span><span class="section-title">Security Keys (OAuth & Supabase)</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="form-group"><label>Supabase Project URL <span class="required">*</span></label><input type="url" name="supabase_url" placeholder="https://abcdefgh12345678.supabase.co" required></div> | |
| <div class="form-group"><label>Supabase Anon Key <span class="required">*</span></label><input type="password" name="supabase_anon_key" placeholder="eyJhbGciOiJIUzI1NiIs..." required></div> | |
| <div class="form-group"><label>Google Web Client ID <span class="required">*</span></label><input type="text" name="google_web_client_id" placeholder="123456789012-abcdef.apps.googleusercontent.com" required></div> | |
| </div></div></div> | |
| <div class="accordion open" id="acc-core"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-core')"> | |
| <span class="section-icon">⚙</span><span class="section-title">Core Basics</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="form-group"><label>App Name <span class="required">*</span></label><input type="text" name="app_name" value="MyApp" required></div> | |
| <div class="form-group"><label>Package Name <span class="required">*</span></label><input type="text" name="package_name" value="com.example.myapp" required pattern="^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$"></div> | |
| <div class="form-group"><label>Target Web URL <span class="required">*</span></label><input type="url" name="target_url" value="https://skydata.bond/login.html" required></div> | |
| <div class="grid-2"> | |
| <div class="form-group"><label>Build Format</label><select name="build_format"><option value="apk" selected>APK</option><option value="aab">AAB (App Bundle)</option></select></div> | |
| <div class="form-group"><label>Screen Orientation</label><select name="screen_orientation"><option value="unspecified" selected>Auto</option><option value="portrait">Portrait Only</option><option value="landscape">Landscape Only</option></select></div> | |
| </div> | |
| <div class="toggle-row"><div class="toggle-label">Split APK (Performance)<small>Build separate lightweight APKs for ARM64 & ARMv7</small></div><label class="switch"><input type="checkbox" name="split_apk"><span class="slider"></span></label></div> | |
| <div class="grid-2"> | |
| <div class="form-group"><label>Version Name</label><input type="text" name="version_name" value="1.0.0"></div> | |
| <div class="form-group"><label>Version Code</label><input type="number" name="version_code" value="1" min="1"></div> | |
| </div> | |
| <div class="grid-2"> | |
| <div class="form-group"><label>Developer Name</label><input type="text" name="dev_name" value="Developer"></div> | |
| <div class="form-group"><label>Developer Email</label><input type="email" name="dev_email" value="dev@example.com"></div> | |
| </div> | |
| </div></div></div> | |
| <div class="accordion" id="acc-branding"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-branding')"> | |
| <span class="section-icon">🎨</span><span class="section-title">Branding & UI</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="form-group"><label>App Icon (PNG)</label> | |
| <div class="file-upload-zone" id="iconUploadZone" onclick="document.getElementById('iconFileInput').click()"><div class="upload-icon">📷</div><p>Click to upload icon</p><img id="iconPreview" style="display:none" alt="icon preview"></div> | |
| <input type="file" id="iconFileInput" accept="image/png,image/jpeg,image/webp" style="display:none" onchange="handleIconUpload(event)"> | |
| <input type="hidden" name="app_icon_b64" id="app_icon_b64"></div> | |
| <div class="toggle-row"><div class="toggle-label">Splash Screen</div><label class="switch"><input type="checkbox" name="splash_enabled" id="splashToggle" onchange="toggleSplashOptions()"><span class="slider"></span></label></div> | |
| <div id="splashOptions" style="display:none"> | |
| <div class="form-group"><label>Splash Media</label> | |
| <div class="file-upload-zone" id="splashUploadZone" onclick="document.getElementById('splashFileInput').click()"><div class="upload-icon">🌌</div><p>Click to upload splash image/GIF</p><img id="splashPreview" style="display:none" alt="splash preview"></div> | |
| <input type="file" id="splashFileInput" accept="image/png,image/jpeg,image/gif,image/webp" style="display:none" onchange="handleSplashUpload(event)"> | |
| <input type="hidden" name="splash_media_b64" id="splash_media_b64"></div> | |
| <div class="form-group"><label>Splash Text</label><input type="text" name="splash_text" value="Loading..."></div> | |
| <div class="grid-2"> | |
| <div class="form-group"><label>Splash Text Color</label><input type="color" name="splash_text_color" value="#FFFFFF"></div> | |
| <div class="form-group"><label>Splash Background Color</label><input type="color" name="splash_bg_color" value="#1a1a2e"></div> | |
| </div> | |
| </div> | |
| <div class="grid-2"><div class="form-group"><label>Status Bar Color</label><input type="color" name="status_bar_color" value="#1a1a2e"></div></div> | |
| <div class="toggle-row"><div class="toggle-label">Bottom Navigation Bar</div><label class="switch"><input type="checkbox" name="bottom_nav_enabled"><span class="slider"></span></label></div> | |
| </div></div></div> | |
| <div class="accordion" id="acc-webengine"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-webengine')"> | |
| <span class="section-icon">⚡</span><span class="section-title">Advanced Web Engine & UX</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="toggle-row"><div class="toggle-label">DevTools F12 (Eruda)</div><label class="switch"><input type="checkbox" name="dev_tools_f12" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Smart Cache</div><label class="switch"><input type="checkbox" name="smart_cache" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Enable JavaScript</div><label class="switch"><input type="checkbox" name="enable_javascript" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Enable DOM Storage</div><label class="switch"><input type="checkbox" name="enable_dom_storage" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Allow File Access</div><label class="switch"><input type="checkbox" name="allow_file_access" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Media Auto-play</div><label class="switch"><input type="checkbox" name="media_autoplay"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Enable Zoom Controls</div><label class="switch"><input type="checkbox" name="enable_zoom"><span class="slider"></span></label></div> | |
| <div class="form-group" style="margin-top:12px"><label>Custom User Agent</label><input type="text" name="custom_user_agent" placeholder="Mozilla/5.0 ..."></div> | |
| <div class="toggle-row"><div class="toggle-label">Pull-to-Refresh</div><label class="switch"><input type="checkbox" name="pull_to_refresh" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Multiple Windows Support</div><label class="switch"><input type="checkbox" name="multi_windows"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Deep Link & Intent Interception</div><label class="switch"><input type="checkbox" name="deep_link_intercept" checked><span class="slider"></span></label></div> | |
| </div></div></div> | |
| <div class="accordion" id="acc-security"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-security')"> | |
| <span class="section-icon">🔒</span><span class="section-title">Security & Protection</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="toggle-row"><div class="toggle-label">Anti-Frida & Xposed</div><label class="switch"><input type="checkbox" name="anti_frida" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Prevent Memory Editing</div><label class="switch"><input type="checkbox" name="prevent_memory_edit" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">ProGuard / R8 Obfuscation</div><label class="switch"><input type="checkbox" name="proguard_enabled" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Root & Emulator Detection</div><label class="switch"><input type="checkbox" name="root_detection"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Prevent Screenshots</div><label class="switch"><input type="checkbox" name="prevent_screenshots"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Clear Cache on Exit</div><label class="switch"><input type="checkbox" name="clear_cache_exit"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Clear Cookies on Exit</div><label class="switch"><input type="checkbox" name="clear_cookies_exit"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Keep Screen On</div><label class="switch"><input type="checkbox" name="keep_screen_on"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Fullscreen Immersive Mode</div><label class="switch"><input type="checkbox" name="fullscreen_mode"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Enforce HTTPS Only</div><label class="switch"><input type="checkbox" name="enforce_https" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">Tamper Protection</div><label class="switch"><input type="checkbox" name="tamper_protection" checked><span class="slider"></span></label></div> | |
| </div></div></div> | |
| <div class="accordion" id="acc-monetize"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-monetize')"> | |
| <span class="section-icon">💰</span><span class="section-title">Monetization & Services</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="toggle-row"><div class="toggle-label">Firebase Integration</div><label class="switch"><input type="checkbox" name="firebase_enabled" id="firebaseToggle" onchange="toggleFirebaseOptions()"><span class="slider"></span></label></div> | |
| <div id="firebaseOptions" style="display:none"><div class="form-group"><label>google-services.json content</label><textarea name="firebase_json" placeholder='{"project_info":...}'></textarea></div></div> | |
| <div class="form-group" style="margin-top:12px"><label>OneSignal App ID</label><input type="text" name="onesignal_app_id" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></div> | |
| <div class="toggle-row"><div class="toggle-label">AdMob Ads</div><label class="switch"><input type="checkbox" name="admob_enabled" id="admobToggle" onchange="toggleAdmobOptions()"><span class="slider"></span></label></div> | |
| <div id="admobOptions" style="display:none"> | |
| <div class="form-group"><label>AdMob Banner ID</label><input type="text" name="admob_banner_id" placeholder="ca-app-pub-xxxxx/xxxxx"></div> | |
| <div class="form-group"><label>AdMob Interstitial ID</label><input type="text" name="admob_interstitial_id" placeholder="ca-app-pub-xxxxx/xxxxx"></div> | |
| </div> | |
| </div></div></div> | |
| <div class="accordion" id="acc-perms"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-perms')"> | |
| <span class="section-icon">🔐</span><span class="section-title">Dynamic Permissions</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"><div class="perm-grid"> | |
| <div class="toggle-row"><div class="toggle-label">INTERNET</div><label class="switch"><input type="checkbox" checked disabled><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">CAMERA</div><label class="switch"><input type="checkbox" name="perm_camera"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">RECORD_AUDIO</div><label class="switch"><input type="checkbox" name="perm_microphone"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">LOCATION</div><label class="switch"><input type="checkbox" name="perm_location"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">STORAGE</div><label class="switch"><input type="checkbox" name="perm_storage"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">VIBRATE</div><label class="switch"><input type="checkbox" name="perm_vibrate"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">READ_CONTACTS</div><label class="switch"><input type="checkbox" name="perm_contacts"><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">POST_NOTIFICATIONS</div><label class="switch"><input type="checkbox" name="perm_notifications" checked><span class="slider"></span></label></div> | |
| </div></div></div></div> | |
| <div class="accordion" id="acc-resilience"> | |
| <div class="accordion-header" onclick="toggleAccordion('acc-resilience')"> | |
| <span class="section-icon">🤖</span><span class="section-title">Resilience & AI</span><span class="accordion-arrow">▼</span> | |
| </div> | |
| <div class="accordion-body"><div class="accordion-content"> | |
| <div class="toggle-row"><div class="toggle-label">Offline Mode</div><label class="switch"><input type="checkbox" name="offline_mode" checked><span class="slider"></span></label></div> | |
| <div class="toggle-row"><div class="toggle-label">AI Log Analyzer</div><label class="switch"><input type="checkbox" checked disabled><span class="slider"></span></label></div> | |
| </div></div></div> | |
| <button type="submit" class="build-btn" id="buildBtn"> | |
| <span class="spinner" id="buildSpinner"></span> | |
| <span id="buildBtnText">Build Android App</span> | |
| </button> | |
| </form> | |
| <div id="log-panel"><div class="log-header"><h3>Build Log</h3><span class="log-status" id="logStatus">RUNNING</span></div><div id="log-output"></div></div> | |
| <div id="download-section"><a class="download-btn" id="downloadBtn" href="#">Download Build Artifact</a></div> | |
| </div> | |
| <footer><p>Android APK/AAB Builder SaaS • Powered by Gradle & Flask</p></footer> | |
| <script> | |
| window.onerror = function(m, u, l) { console.log("JS Error: " + m + " at " + l); }; | |
| document.addEventListener("DOMContentLoaded", () => { | |
| const inputs = document.querySelectorAll("#buildForm input, #buildForm select, #buildForm textarea"); | |
| inputs.forEach(input => { | |
| if (input.type !== "file" && input.type !== "submit" && input.type !== "button") { | |
| const savedValue = localStorage.getItem("skydata_" + input.name); | |
| if (savedValue !== null) { | |
| if (input.type === "checkbox" || input.type === "radio") input.checked = savedValue === "true"; | |
| else input.value = savedValue; | |
| } | |
| input.addEventListener("change", () => { | |
| if (input.type === "checkbox" || input.type === "radio") localStorage.setItem("skydata_" + input.name, input.checked); | |
| else localStorage.setItem("skydata_" + input.name, input.value); | |
| }); | |
| input.addEventListener("input", () => { | |
| if (input.type !== "checkbox" && input.type !== "radio") localStorage.setItem("skydata_" + input.name, input.value); | |
| }); | |
| } | |
| }); | |
| toggleSplashOptions(); toggleFirebaseOptions(); toggleAdmobOptions(); | |
| }); | |
| function toggleAccordion(id){var el=document.getElementById(id);if(el.classList.contains('open'))el.classList.remove('open');else el.classList.add('open');} | |
| function toggleSplashOptions(){var cb=document.getElementById('splashToggle');var opts=document.getElementById('splashOptions');opts.style.display=cb.checked?'block':'none';} | |
| function toggleFirebaseOptions(){var cb=document.getElementById('firebaseToggle');var opts=document.getElementById('firebaseOptions');opts.style.display=cb.checked?'block':'none';} | |
| function toggleAdmobOptions(){var cb=document.getElementById('admobToggle');var opts=document.getElementById('admobOptions');opts.style.display=cb.checked?'block':'none';} | |
| function handleIconUpload(e){var f=e.target.files[0];if(!f)return;var r=new FileReader();r.onload=function(ev){var b64=ev.target.result;document.getElementById('app_icon_b64').value=b64;var p=document.getElementById('iconPreview');p.src=b64;p.style.display='block';document.querySelector('#iconUploadZone p').textContent=f.name;};r.readAsDataURL(f);} | |
| function handleSplashUpload(e){var f=e.target.files[0];if(!f)return;var r=new FileReader();r.onload=function(ev){var b64=ev.target.result;document.getElementById('splash_media_b64').value=b64;var p=document.getElementById('splashPreview');p.src=b64;p.style.display='block';document.querySelector('#splashUploadZone p').textContent=f.name;};r.readAsDataURL(f);} | |
| function loadSha1(){fetch('/sha1').then(r=>r.json()).then(data=>{if(data.sha1){document.getElementById('sha1Card').style.display='block';document.getElementById('sha1Value').textContent=data.formatted;}}).catch(err=>console.log(err));} | |
| function copySha1(){var v=document.getElementById('sha1Value').textContent;navigator.clipboard.writeText(v).then(()=>{var b=document.getElementById('copySha1Btn');b.textContent='Copied!';b.classList.add('copied');setTimeout(()=>{b.textContent='Copy to Clipboard';b.classList.remove('copied');},2000);});} | |
| window.addEventListener('load',loadSha1); | |
| var currentBuildId=null;var eventSource=null; | |
| window.handleBuild = function(e){ | |
| e.preventDefault(); | |
| var btn=document.getElementById('buildBtn');var btnText=document.getElementById('buildBtnText'); | |
| btn.disabled=true;btn.classList.add('loading');btnText.textContent='Building...'; | |
| var logPanel=document.getElementById('log-panel');var logOutput=document.getElementById('log-output');var logStatusEl=document.getElementById('logStatus');var downloadSection=document.getElementById('download-section'); | |
| logPanel.classList.add('active');logOutput.innerHTML='';logStatusEl.textContent='RUNNING';logStatusEl.className='log-status running';downloadSection.classList.remove('active'); | |
| if(eventSource){eventSource.close();eventSource=null;} | |
| var formData=new FormData(document.getElementById('buildForm')); | |
| fetch('/build',{method:'POST',body:formData}).then(r=>r.json()).then(data=>{ | |
| if(data.status==='started'){currentBuildId=data.build_id;startLogStream(data.build_id);} | |
| else{btn.disabled=false;btn.classList.remove('loading');btnText.textContent='Build Android App';logOutput.innerHTML='<span class="error-line">Error: '+(data.message||'Unknown')+'</span>\\n';logStatusEl.textContent='FAILED';logStatusEl.className='log-status failed';} | |
| }).catch(err=>{btn.disabled=false;btn.classList.remove('loading');btnText.textContent='Build Android App';logOutput.innerHTML='<span class="error-line">Network Error: '+err.message+'</span>\\n';logStatusEl.textContent='FAILED';logStatusEl.className='log-status failed';}); | |
| }; | |
| function startLogStream(buildId){ | |
| var logOutput=document.getElementById('log-output');var logStatusEl=document.getElementById('logStatus');var btn=document.getElementById('buildBtn');var btnText=document.getElementById('buildBtnText');var downloadSection=document.getElementById('download-section');var downloadBtn=document.getElementById('downloadBtn'); | |
| eventSource=new EventSource('/logs/'+buildId); | |
| eventSource.onmessage=function(event){ | |
| try{var data=JSON.parse(event.data);if(data.type==='log'){var msg=data.message;var span=document.createElement('span');if(msg.indexOf('ERROR')!==-1||msg.indexOf('FAILED')!==-1||msg.indexOf('error')!==-1||msg.indexOf('Exception')!==-1)span.className='error-line';else if(msg.indexOf('BUILD SUCCESSFUL')!==-1||msg.indexOf('success')!==-1)span.className='success-line';else if(msg.indexOf('AI Log Analysis')!==-1)span.className='ai-line';span.textContent=msg+'\\n';logOutput.appendChild(span);logOutput.scrollTop=logOutput.scrollHeight;}else if(data.type==='status'){eventSource.close();eventSource=null;btn.disabled=false;btn.classList.remove('loading');btnText.textContent='Build Android App';if(data.status==='success'){logStatusEl.textContent='SUCCESS';logStatusEl.className='log-status success';downloadSection.classList.add('active');downloadBtn.href='/download/'+buildId;downloadBtn.download='';}else{logStatusEl.textContent='FAILED';logStatusEl.className='log-status failed';}}}catch(err){var fs=document.createElement('span');fs.textContent=event.data+'\\n';logOutput.appendChild(fs);logOutput.scrollTop=logOutput.scrollHeight;} | |
| }; | |
| eventSource.onerror=function(err){ | |
| if(eventSource){eventSource.close();eventSource=null;} | |
| btn.disabled=false;btn.classList.remove('loading');btnText.textContent='Build Android App'; | |
| fetch('/status/'+buildId).then(r=>r.json()).then(d=>{if(d.status==='success'){logStatusEl.textContent='SUCCESS';logStatusEl.className='log-status success';downloadSection.classList.add('active');downloadBtn.href='/download/'+buildId;}else if(d.status==='failed'){logStatusEl.textContent='FAILED';logStatusEl.className='log-status failed';}else{logStatusEl.textContent='UNKNOWN';logStatusEl.className='log-status failed';}}).catch(()=>{logStatusEl.textContent='CONNECTION LOST';logStatusEl.className='log-status failed';}); | |
| }; | |
| } | |
| </script> | |
| </body> | |
| </html>""" | |
| def index(): | |
| return Response(HTML_CONTENT, mimetype="text/html") | |
| def get_sha1_route(): | |
| sha1 = get_sha1() | |
| if sha1: | |
| return jsonify({"sha1": sha1, "formatted": sha1}) | |
| return jsonify({"error": "Keystore not found"}), 404 | |
| def download_keystore(): | |
| if os.path.exists(KEYSTORE_PATH): | |
| return send_file(KEYSTORE_PATH, as_attachment=True, download_name="release-key.jks") | |
| return jsonify({"error": "Keystore not found"}), 404 | |
| def start_build(): | |
| try: | |
| data = request.form.to_dict() | |
| config = {} | |
| config["supabase_url"] = data.get("supabase_url", "") | |
| config["supabase_anon_key"] = data.get("supabase_anon_key", "") | |
| raw_google_id = data.get("google_web_client_id", "") | |
| google_id_match = re.search(r'([0-9]+-[a-zA-Z0-9_]+\.apps\.googleusercontent\.com)', raw_google_id) | |
| if google_id_match: | |
| config["google_web_client_id"] = google_id_match.group(1) | |
| else: | |
| config["google_web_client_id"] = raw_google_id.strip() | |
| config["app_name"] = data.get("app_name", "MyApp") | |
| config["package_name"] = data.get("package_name", "com.example.myapp") | |
| config["target_url"] = data.get("target_url", "https://www.google.com") | |
| config["build_format"] = data.get("build_format", "apk") | |
| config["version_name"] = data.get("version_name", "1.0.0") | |
| config["version_code"] = data.get("version_code", "1") | |
| config["screen_orientation"] = data.get("screen_orientation", "unspecified") | |
| config["dev_tools_f12"] = data.get("dev_tools_f12", "on") == "on" | |
| config["anti_frida"] = data.get("anti_frida") == "on" | |
| config["smart_cache"] = data.get("smart_cache") == "on" | |
| config["prevent_memory_edit"] = data.get("prevent_memory_edit") == "on" | |
| config["split_apk"] = data.get("split_apk") == "on" | |
| config["splash_enabled"] = data.get("splash_enabled") == "on" | |
| config["splash_text"] = data.get("splash_text", "Loading...") | |
| config["splash_text_color"] = data.get("splash_text_color", "#FFFFFF") | |
| config["splash_bg_color"] = data.get("splash_bg_color", "#1a1a2e") | |
| config["status_bar_color"] = data.get("status_bar_color", "#1a1a2e") | |
| config["bottom_nav_enabled"] = data.get("bottom_nav_enabled") == "on" | |
| config["enable_javascript"] = data.get("enable_javascript") == "on" | |
| config["enable_dom_storage"] = data.get("enable_dom_storage") == "on" | |
| config["allow_file_access"] = data.get("allow_file_access") == "on" | |
| config["media_autoplay"] = data.get("media_autoplay") == "on" | |
| config["enable_zoom"] = data.get("enable_zoom") == "on" | |
| config["custom_user_agent"] = data.get("custom_user_agent", "") | |
| config["pull_to_refresh"] = data.get("pull_to_refresh") == "on" | |
| config["multi_windows"] = data.get("multi_windows") == "on" | |
| config["deep_link_intercept"] = data.get("deep_link_intercept") == "on" | |
| config["proguard_enabled"] = data.get("proguard_enabled") == "on" | |
| config["root_detection"] = data.get("root_detection") == "on" | |
| config["prevent_screenshots"] = data.get("prevent_screenshots") == "on" | |
| config["clear_cache_exit"] = data.get("clear_cache_exit") == "on" | |
| config["clear_cookies_exit"] = data.get("clear_cookies_exit") == "on" | |
| config["keep_screen_on"] = data.get("keep_screen_on") == "on" | |
| config["fullscreen_mode"] = data.get("fullscreen_mode") == "on" | |
| config["enforce_https"] = data.get("enforce_https") == "on" | |
| config["tamper_protection"] = data.get("tamper_protection") == "on" | |
| config["firebase_enabled"] = data.get("firebase_enabled") == "on" | |
| config["firebase_json"] = data.get("firebase_json", "") | |
| config["onesignal_enabled"] = bool(data.get("onesignal_app_id", "").strip()) | |
| config["onesignal_app_id"] = data.get("onesignal_app_id", "") | |
| config["admob_enabled"] = data.get("admob_enabled") == "on" | |
| config["admob_banner_id"] = data.get("admob_banner_id", "") | |
| config["admob_interstitial_id"] = data.get("admob_interstitial_id", "") | |
| config["perm_camera"] = data.get("perm_camera") == "on" | |
| config["perm_microphone"] = data.get("perm_microphone") == "on" | |
| config["perm_location"] = data.get("perm_location") == "on" | |
| config["perm_storage"] = data.get("perm_storage") == "on" | |
| config["perm_vibrate"] = data.get("perm_vibrate") == "on" | |
| config["perm_contacts"] = data.get("perm_contacts") == "on" | |
| config["perm_notifications"] = data.get("perm_notifications") == "on" | |
| config["offline_mode"] = data.get("offline_mode") == "on" | |
| config["app_icon_b64"] = data.get("app_icon_b64", "") | |
| config["splash_media_b64"] = data.get("splash_media_b64", "") | |
| config["has_splash_image"] = bool(data.get("splash_media_b64", "").strip()) | |
| build_id = str(uuid.uuid4())[:12] | |
| os.makedirs(os.path.join(BUILD_DIR_BASE, build_id), exist_ok=True) | |
| thread = threading.Thread(target=run_build, args=(build_id, config), daemon=True) | |
| thread.start() | |
| return jsonify({"status": "started", "build_id": build_id}) | |
| except Exception as e: | |
| return jsonify({"status": "error", "message": str(e)}), 500 | |
| def get_logs(build_id): | |
| def generate(): | |
| last_index = 0 | |
| while True: | |
| logs = LOG_STORE.get(build_id, []) | |
| status = BUILD_STATUS.get(build_id, "unknown") | |
| if last_index < len(logs): | |
| new_logs = logs[last_index:] | |
| last_index = len(logs) | |
| for log_line in new_logs: | |
| yield "data: {}\n\n".format(json.dumps({"type": "log", "message": log_line})) | |
| if status in ("success", "failed"): | |
| yield "data: {}\n\n".format(json.dumps({"type": "status", "status": status, "build_id": build_id})) | |
| break | |
| time.sleep(0.5) | |
| return Response(generate(), mimetype="text/event-stream", headers={ | |
| "Cache-Control": "no-cache", | |
| "X-Accel-Buffering": "no", | |
| "Connection": "keep-alive" | |
| }) | |
| def download_artifact(build_id): | |
| artifact_path = BUILD_ARTIFACTS.get(build_id) | |
| if artifact_path and os.path.exists(artifact_path): | |
| return send_file(artifact_path, as_attachment=True, download_name=os.path.basename(artifact_path)) | |
| else: | |
| return jsonify({"error": "Artifact not found"}), 404 | |
| def get_status(build_id): | |
| status = BUILD_STATUS.get(build_id, "unknown") | |
| return jsonify({"status": status, "build_id": build_id}) | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=7860, debug=False, threaded=True) |