1 / app.py
Poorpoor6976's picture
Update app.py
38aa750 verified
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">&#128268;</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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;"))
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 &bull; 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">&#128272;</span><span class="section-title">Security Keys (OAuth & Supabase)</span><span class="accordion-arrow">&#9660;</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">&#9881;</span><span class="section-title">Core Basics</span><span class="accordion-arrow">&#9660;</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">&#127912;</span><span class="section-title">Branding & UI</span><span class="accordion-arrow">&#9660;</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">&#128247;</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">&#127756;</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">&#9889;</span><span class="section-title">Advanced Web Engine & UX</span><span class="accordion-arrow">&#9660;</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">&#128274;</span><span class="section-title">Security & Protection</span><span class="accordion-arrow">&#9660;</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">&#128176;</span><span class="section-title">Monetization & Services</span><span class="accordion-arrow">&#9660;</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">&#128272;</span><span class="section-title">Dynamic Permissions</span><span class="accordion-arrow">&#9660;</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">&#129302;</span><span class="section-title">Resilience & AI</span><span class="accordion-arrow">&#9660;</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 &bull; 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>"""
@app.route("/")
def index():
return Response(HTML_CONTENT, mimetype="text/html")
@app.route("/sha1")
def get_sha1_route():
sha1 = get_sha1()
if sha1:
return jsonify({"sha1": sha1, "formatted": sha1})
return jsonify({"error": "Keystore not found"}), 404
@app.route("/download_keystore")
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
@app.route("/build", methods=["POST"])
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
@app.route("/logs/<build_id>")
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"
})
@app.route("/download/<build_id>")
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
@app.route("/status/<build_id>")
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)