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, render_template, send_file, Response
app = Flask(__name__)
BUILD_DIR_BASE = "/tmp/builds"
LOG_STORE = {}
BUILD_STATUS = {}
BUILD_ARTIFACTS = {}
os.makedirs(BUILD_DIR_BASE, exist_ok=True)
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 """
Offline
🔌
No Internet Connection
Please check your network settings and try again.
Retry
"""
def generate_keystore(build_dir, dev_name, dev_email):
keystore_path = os.path.join(build_dir, "release-key.jks")
keytool_cmd = [
"keytool", "-genkeypair",
"-v",
"-keystore", keystore_path,
"-keyalg", "RSA",
"-keysize", "2048",
"-validity", "10000",
"-alias", "releasekey",
"-storepass", "android123",
"-keypass", "android123",
"-dname", "CN={}, OU=Mobile, O=Company, L=City, S=State, C=US".format(dev_name if dev_name else "Developer")
]
subprocess.run(keytool_cmd, capture_output=True, timeout=60)
return keystore_path
def create_build_gradle_root(project_dir, use_firebase, use_onesignal):
firebase_classpath = ""
if use_firebase:
firebase_classpath = " classpath 'com.google.gms:google-services:4.4.0'"
content = """buildscript {{
repositories {{
google()
mavenCentral()
}}
dependencies {{
classpath 'com.android.tools.build:gradle:7.4.2'
{firebase_cp}
}}
}}
allprojects {{
repositories {{
google()
mavenCentral()
}}
}}
task clean(type: Delete) {{
delete rootProject.buildDir
}}
""".format(firebase_cp=firebase_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"
# 🔥 Use the securely validated flag from config
use_firebase = config.get("actual_firebase_enabled", False)
use_onesignal = config.get("onesignal_enabled", False)
onesignal_id = config.get("onesignal_app_id", "")
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'")
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_admob:
dependencies.append(" implementation 'com.google.android.gms:play-services-ads:22.6.0'")
deps_str = "\n".join(dependencies)
firebase_plugin = ""
if use_firebase:
firebase_plugin = "apply plugin: 'com.google.gms.google-services'"
proguard_rules = ""
if config.get("proguard_enabled"):
proguard_rules = """ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'"""
content = """plugins {{
id 'com.android.application'
}}
{firebase_plugin}
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
}}
}}
compileOptions {{
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}}
lint {{
abortOnError false
checkReleaseBuilds false
}}
}}
dependencies {{
{deps_str}
}}
""".format(
firebase_plugin=firebase_plugin,
package_name=package_name,
version_code=version_code,
version_name=version_name,
minify_enabled=minify_enabled,
proguard_rules=proguard_rules,
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.** { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface ;
}
"""
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(' ')
permissions.append(' ')
if config.get("perm_camera"):
permissions.append(' ')
permissions.append(' ')
if config.get("perm_microphone"):
permissions.append(' ')
if config.get("perm_location"):
permissions.append(' ')
permissions.append(' ')
if config.get("perm_storage"):
permissions.append(' ')
permissions.append(' ')
if config.get("perm_vibrate"):
permissions.append(' ')
if config.get("perm_contacts"):
permissions.append(' ')
if config.get("perm_notifications"):
permissions.append(' ')
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 = """
""".format(orientation=orientation_str, fullscreen_theme_attr=fullscreen_theme)
main_activity_intent = ""
else:
main_activity_intent = """
"""
meta_data = ""
if config.get("admob_enabled"):
meta_data += """
"""
content = """
{permissions}
{splash_activity}
{main_activity_intent}
{meta_data}
""".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)
root_detection = config.get("root_detection", False)
tamper_protection = config.get("tamper_protection", False)
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"
perm_camera = config.get("perm_camera", False)
perm_microphone = config.get("perm_microphone", False)
perm_location = config.get("perm_location", False)
perm_storage = config.get("perm_storage", False)
perm_notifications = config.get("perm_notifications", False)
use_admob = config.get("admob_enabled", False)
admob_banner_id = config.get("admob_banner_id", "")
admob_interstitial_id = config.get("admob_interstitial_id", "")
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.NetworkInfo;")
imports.append("import android.net.Uri;")
imports.append("import android.os.Build;")
imports.append("import android.os.Bundle;")
imports.append("import android.view.View;")
imports.append("import android.view.Window;")
imports.append("import android.view.WindowManager;")
imports.append("import android.webkit.CookieManager;")
imports.append("import android.webkit.GeolocationPermissions;")
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;")
if pull_to_refresh:
imports.append("import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;")
if use_admob:
imports.append("import com.google.android.gms.ads.AdRequest;")
imports.append("import com.google.android.gms.ads.AdSize;")
imports.append("import com.google.android.gms.ads.AdView;")
imports.append("import com.google.android.gms.ads.MobileAds;")
imports.append("import com.google.android.gms.ads.interstitial.InterstitialAd;")
imports.append("import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback;")
imports.append("import com.google.android.gms.ads.LoadAdError;")
imports.append("import java.io.File;")
imports.append("import java.util.ArrayList;")
imports.append("import java.util.List;")
imports.append("")
runtime_perms_list = []
if perm_camera:
runtime_perms_list.append('"android.permission.CAMERA"')
if perm_microphone:
runtime_perms_list.append('"android.permission.RECORD_AUDIO"')
if perm_location:
runtime_perms_list.append('"android.permission.ACCESS_FINE_LOCATION"')
runtime_perms_list.append('"android.permission.ACCESS_COARSE_LOCATION"')
if perm_storage:
runtime_perms_list.append('"android.permission.READ_EXTERNAL_STORAGE"')
if perm_notifications:
runtime_perms_list.append('"android.permission.POST_NOTIFICATIONS"')
perms_array = ", ".join(runtime_perms_list)
root_detection_code = ""
if root_detection:
root_detection_code = """
if (isRootedOrEmulator()) {
Toast.makeText(this, "This app cannot run on rooted/emulated devices.", Toast.LENGTH_LONG).show();
finish();
return;
}"""
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"};
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") || Build.PRODUCT.contains("sdk_gphone") || Build.PRODUCT.contains("vbox86p") || Build.HARDWARE.contains("goldfish") || Build.HARDWARE.contains("ranchu")) return true;
return false;
}"""
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;
}"""
tamper_check = ""
if tamper_protection:
tamper_check = """
if (!verifyAppSignature()) {
Toast.makeText(this, "App integrity check failed.", Toast.LENGTH_LONG).show();
finish();
return;
}"""
screenshot_code = ""
if prevent_screenshots:
screenshot_code = " getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);"
keep_screen_code = ""
if keep_screen_on:
keep_screen_code = " getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);"
fullscreen_code = ""
if fullscreen_mode:
fullscreen_code = """
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}"""
status_bar_code = """
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {{
Window window = getWindow();
window.setStatusBarColor(Color.parseColor("{color}"));
}}""".format(color=status_bar_color if status_bar_color else "#1a1a2e")
swipe_refresh_setup = ""
swipe_refresh_decl = ""
if pull_to_refresh:
swipe_refresh_decl = " private SwipeRefreshLayout swipeRefreshLayout;"
swipe_refresh_setup = """
swipeRefreshLayout = new SwipeRefreshLayout(this);
swipeRefreshLayout.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
webView.reload();
}
});"""
ua_code = ""
if custom_ua:
ua_code = ' settings.setUserAgentString("{}");'.format(custom_ua.replace('"', '\\"'))
admob_decl = ""
admob_setup = ""
admob_interstitial_setup = ""
admob_interstitial_decl = ""
if use_admob:
admob_decl = " private AdView adView;"
admob_interstitial_decl = " private InterstitialAd mInterstitialAd;"
admob_setup = """
MobileAds.initialize(this, initializationStatus -> {{}});
adView = new AdView(this);
adView.setAdSize(AdSize.BANNER);
adView.setAdUnitId("{banner_id}");
AdRequest adRequest = new AdRequest.Builder().build();
adView.loadAd(adRequest);""".format(banner_id=admob_banner_id if admob_banner_id else "ca-app-pub-3940256099942544/6300978111")
if admob_interstitial_id:
admob_interstitial_setup = """
loadInterstitialAd();"""
admob_interstitial_method = ""
if use_admob and admob_interstitial_id:
admob_interstitial_method = """
private void loadInterstitialAd() {{
AdRequest adRequest = new AdRequest.Builder().build();
InterstitialAd.load(this, "{interstitial_id}", adRequest, new InterstitialAdLoadCallback() {{
@Override
public void onAdLoaded(InterstitialAd interstitialAd) {{
mInterstitialAd = interstitialAd;
}}
@Override
public void onAdFailedToLoad(LoadAdError loadAdError) {{
mInterstitialAd = null;
}}
}});
}}
private void showInterstitialAd() {{
if (mInterstitialAd != null) {{
mInterstitialAd.show(this);
loadInterstitialAd();
}}
}}""".format(interstitial_id=admob_interstitial_id)
deep_link_code = ""
if deep_link_intercept:
deep_link_code = """
String host = Uri.parse(url).getHost();
if (host != null && (host.contains("accounts.google.com") || host.contains("facebook.com") || host.contains("wa.me") || host.contains("whatsapp.com") || host.contains("t.me") || host.contains("twitter.com") || host.contains("x.com") || host.contains("instagram.com"))) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true;
}"""
offline_check_code = ""
if offline_mode:
offline_check_code = """
if (!isNetworkAvailable()) {
webView.loadUrl("file:///android_asset/offline.html");
} else {
webView.loadUrl(TARGET_URL);
}"""
else:
offline_check_code = """
webView.loadUrl(TARGET_URL);"""
offline_method = ""
if offline_mode:
offline_method = """
private boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
}
return false;
}"""
on_destroy_code = ""
destroy_lines = []
if clear_cache_exit:
destroy_lines.append(" if (webView != null) { webView.clearCache(true); }")
if clear_cookies_exit:
destroy_lines.append(" CookieManager.getInstance().removeAllCookies(null);")
destroy_lines.append(" CookieManager.getInstance().flush();")
if destroy_lines:
on_destroy_code = """
@Override
protected void onDestroy() {{
super.onDestroy();
{lines}
}}""".format(lines="\n".join(destroy_lines))
request_perms_code = ""
if runtime_perms_list:
request_perms_code = """
List permissionsNeeded = new ArrayList<>();
String[] allPerms = new String[]{{{perms}}};
for (String perm : allPerms) {{
if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {{
permissionsNeeded.add(perm);
}}
}}
if (!permissionsNeeded.isEmpty()) {{
ActivityCompat.requestPermissions(this, permissionsNeeded.toArray(new String[0]), 1001);
}}""".format(perms=perms_array)
bottom_nav_code_layout = ""
bottom_nav_code_setup = ""
if bottom_nav:
bottom_nav_code_layout = """
LinearLayout bottomNav = new LinearLayout(this);
bottomNav.setOrientation(LinearLayout.HORIZONTAL);
bottomNav.setBackgroundColor(Color.parseColor("#1a1a2e"));
bottomNav.setGravity(android.view.Gravity.CENTER);
LinearLayout.LayoutParams navParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 130);
bottomNav.setLayoutParams(navParams);
ImageButton btnBack = new ImageButton(this);
btnBack.setImageResource(android.R.drawable.ic_media_previous);
btnBack.setBackgroundColor(Color.TRANSPARENT);
btnBack.setColorFilter(Color.WHITE);
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
btnBack.setLayoutParams(btnParams);
btnBack.setOnClickListener(v -> { if (webView.canGoBack()) webView.goBack(); });
ImageButton btnHome = new ImageButton(this);
btnHome.setImageResource(android.R.drawable.ic_menu_revert);
btnHome.setBackgroundColor(Color.TRANSPARENT);
btnHome.setColorFilter(Color.WHITE);
btnHome.setLayoutParams(btnParams);
btnHome.setOnClickListener(v -> webView.loadUrl(TARGET_URL));
ImageButton btnRefresh = new ImageButton(this);
btnRefresh.setImageResource(android.R.drawable.ic_popup_sync);
btnRefresh.setBackgroundColor(Color.TRANSPARENT);
btnRefresh.setColorFilter(Color.WHITE);
btnRefresh.setLayoutParams(btnParams);
btnRefresh.setOnClickListener(v -> webView.reload());
bottomNav.addView(btnBack);
bottomNav.addView(btnHome);
bottomNav.addView(btnRefresh);"""
build_layout = ""
if pull_to_refresh and bottom_nav and use_admob:
build_layout = """
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
FrameLayout.LayoutParams webParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 0);
LinearLayout.LayoutParams webLinearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
swipeRefreshLayout.addView(webView);
swipeRefreshLayout.setLayoutParams(webLinearParams);
rootLayout.addView(swipeRefreshLayout);
rootLayout.addView(adView);
{bottom_nav_layout}
rootLayout.addView(bottomNav);
setContentView(rootLayout);""".format(bottom_nav_layout=bottom_nav_code_layout)
elif pull_to_refresh and bottom_nav:
build_layout = """
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
LinearLayout.LayoutParams webLinearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
swipeRefreshLayout.addView(webView);
swipeRefreshLayout.setLayoutParams(webLinearParams);
rootLayout.addView(swipeRefreshLayout);
{bottom_nav_layout}
rootLayout.addView(bottomNav);
setContentView(rootLayout);""".format(bottom_nav_layout=bottom_nav_code_layout)
elif pull_to_refresh and use_admob:
build_layout = """
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
LinearLayout.LayoutParams webLinearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
swipeRefreshLayout.addView(webView);
swipeRefreshLayout.setLayoutParams(webLinearParams);
rootLayout.addView(swipeRefreshLayout);
rootLayout.addView(adView);
setContentView(rootLayout);"""
elif bottom_nav and use_admob:
build_layout = """
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
LinearLayout.LayoutParams webLinearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
webView.setLayoutParams(webLinearParams);
rootLayout.addView(webView);
rootLayout.addView(adView);
{bottom_nav_layout}
rootLayout.addView(bottomNav);
setContentView(rootLayout);""".format(bottom_nav_layout=bottom_nav_code_layout)
elif pull_to_refresh:
build_layout = """
swipeRefreshLayout.addView(webView);
setContentView(swipeRefreshLayout);"""
elif bottom_nav:
build_layout = """
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
LinearLayout.LayoutParams webLinearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
webView.setLayoutParams(webLinearParams);
rootLayout.addView(webView);
{bottom_nav_layout}
rootLayout.addView(bottomNav);
setContentView(rootLayout);""".format(bottom_nav_layout=bottom_nav_code_layout)
elif use_admob:
build_layout = """
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
LinearLayout.LayoutParams webLinearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
webView.setLayoutParams(webLinearParams);
rootLayout.addView(webView);
rootLayout.addView(adView);
setContentView(rootLayout);"""
else:
build_layout = """
setContentView(webView);"""
swipe_page_finished = ""
if pull_to_refresh:
swipe_page_finished = """
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}"""
content = """{imports_str}
public class MainActivity extends AppCompatActivity {{
private WebView webView;
private static final String TARGET_URL = "{target_url}";
private ValueCallback fileUploadCallback;
private static final int FILE_CHOOSER_RESULT = 1002;
{swipe_decl}
{admob_d}
{admob_interstitial_d}
@Override
protected void onCreate(Bundle savedInstanceState) {{
super.onCreate(savedInstanceState);
{screenshot_c}
{keep_screen_c}
{fullscreen_c}
{status_bar_c}
{root_det_c}
{tamper_c}
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({js_enabled});
settings.setDomStorageEnabled({dom_enabled});
settings.setAllowFileAccess({file_access});
settings.setMediaPlaybackRequiresUserGesture({media_gesture});
settings.setSupportZoom({zoom_enabled});
settings.setBuiltInZoomControls({zoom_enabled});
settings.setDisplayZoomControls(false);
settings.setJavaScriptCanOpenWindowsAutomatically({multi_win});
settings.setSupportMultipleWindows({multi_win});
settings.setMixedContentMode({mixed_content});
settings.setLoadWithOverviewMode(true);
settings.setUseWideViewPort(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setDatabaseEnabled(true);
settings.setAllowContentAccess(true);
{ua_c}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setAcceptThirdPartyCookies(webView, true);
{swipe_setup}
{admob_s}
{admob_inter_s}
{request_perms}
webView.setWebViewClient(new WebViewClient() {{
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {{
String url = request.getUrl().toString();
{deep_link}
if (url.startsWith("tel:") || url.startsWith("mailto:") || url.startsWith("sms:") || url.startsWith("whatsapp:") || url.startsWith("intent:")) {{
try {{
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
}} catch (Exception e) {{
e.printStackTrace();
}}
return true;
}}
return false;
}}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {{
super.onPageStarted(view, url, favicon);
}}
@Override
public void onPageFinished(WebView view, String url) {{
super.onPageFinished(view, url);
{swipe_finished}
}}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {{
super.onReceivedError(view, errorCode, description, failingUrl);
{offline_on_error}
}}
}});
webView.setWebChromeClient(new WebChromeClient() {{
@Override
public void onPermissionRequest(final PermissionRequest request) {{
runOnUiThread(() -> request.grant(request.getResources()));
}}
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {{
callback.invoke(origin, true, false);
}}
@Override
public boolean onShowFileChooser(WebView wv, ValueCallback filePathCallback, FileChooserParams fileChooserParams) {{
if (fileUploadCallback != null) {{
fileUploadCallback.onReceiveValue(null);
}}
fileUploadCallback = filePathCallback;
Intent intent = fileChooserParams.createIntent();
try {{
startActivityForResult(intent, FILE_CHOOSER_RESULT);
}} catch (Exception e) {{
fileUploadCallback = null;
return false;
}}
return true;
}}
}});
{build_layout_code}
{offline_check}
}}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {{
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == FILE_CHOOSER_RESULT) {{
if (fileUploadCallback != null) {{
Uri[] results = null;
if (resultCode == Activity.RESULT_OK && data != null) {{
String dataString = data.getDataString();
if (dataString != null) {{
results = new Uri[]{{Uri.parse(dataString)}};
}}
}}
fileUploadCallback.onReceiveValue(results);
fileUploadCallback = null;
}}
}}
}}
@Override
public void onBackPressed() {{
if (webView.canGoBack()) {{
webView.goBack();
}} else {{
super.onBackPressed();
}}
}}
{offline_method_code}
{root_method}
{tamper_method_code}
{admob_inter_method}
{on_destroy}
}}
""".format(
imports_str="\n".join(imports),
target_url=target_url.replace('"', '\\"'),
swipe_decl=swipe_refresh_decl,
admob_d=admob_decl,
admob_interstitial_d=admob_interstitial_decl,
screenshot_c=screenshot_code,
keep_screen_c=keep_screen_code,
fullscreen_c=fullscreen_code,
status_bar_c=status_bar_code,
root_det_c=root_detection_code,
tamper_c=tamper_check,
js_enabled="true" if enable_js else "false",
dom_enabled="true" if enable_dom else "false",
file_access="true" if allow_file_access else "false",
media_gesture="false" if media_autoplay else "true",
zoom_enabled="true" if enable_zoom else "false",
multi_win="true" if multi_windows else "false",
mixed_content=mixed_content,
ua_c=ua_code,
swipe_setup=swipe_refresh_setup,
admob_s=admob_setup,
admob_inter_s=admob_interstitial_setup,
request_perms=request_perms_code,
deep_link=deep_link_code,
swipe_finished=swipe_page_finished,
offline_on_error=' if (!isNetworkAvailable()) { view.loadUrl("file:///android_asset/offline.html"); }' if offline_mode else "",
build_layout_code=build_layout,
offline_check=offline_check_code,
offline_method_code=offline_method,
root_method=root_detection_method,
tamper_method_code=tamper_method,
admob_inter_method=admob_interstitial_method,
on_destroy=on_destroy_code
)
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
import 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)
# 🔥 FIREBASE VALIDATION LOGIC 🔥
use_firebase = config.get("firebase_enabled", False)
use_onesignal = config.get("onesignal_enabled", False)
firebase_json = config.get("firebase_json", "")
actual_firebase_enabled = False
if use_firebase and firebase_json.strip():
try:
# Check if it's valid JSON AND contains the crucial "project_info" key
parsed = json.loads(firebase_json)
if "project_info" in parsed:
# Only write the file and enable the plugin if it's perfectly valid
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 to prevent build failure.")
except json.JSONDecodeError:
LOG_STORE[build_id].append("⚠️ WARNING: Firebase JSON is invalid. Firebase integration skipped to prevent build failure.")
elif use_firebase:
LOG_STORE[build_id].append("⚠️ WARNING: Firebase was enabled but no JSON was provided. Integration skipped.")
# Save actual state to config so create_app_build_gradle knows what to do
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 = """
{app_name}
""".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 = """
{status_color}
{status_color}
#e94560
""".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 = """
"""
with open(os.path.join(res_dir, "values", "styles.xml"), "w") as f:
f.write(styles_xml)
generate_keystore(os.path.join(BUILD_DIR_BASE, build_id, "project"), config.get("dev_name", "Developer"), config.get("dev_email", "dev@example.com"))
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("Build Format: {}".format(config.get("build_format", "apk").upper()))
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:
output_path = os.path.join(project_dir, "app", "build", "outputs", "apk", "release", "app-release.apk")
if not os.path.exists(output_path):
output_path = os.path.join(project_dir, "app", "build", "outputs", "apk", "release", "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.")
log("Searching for output files...")
for root, dirs, files in os.walk(os.path.join(project_dir, "app", "build", "outputs")):
for fname in files:
log(" Found: {}".format(os.path.join(root, fname)))
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)))
import traceback
for tb_line in traceback.format_exc().split("\n"):
log(tb_line)
BUILD_STATUS[build_id] = "failed"
@app.route("/")
def index():
return render_template("index.html")
@app.route("/build", methods=["POST"])
def start_build():
try:
data = request.form.to_dict()
config = {}
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_name"] = data.get("dev_name", "Developer")
config["dev_email"] = data.get("dev_email", "dev@example.com")
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/")
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/")
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/")
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)