| 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 """<!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} |
| button{background:#e94560;color:#fff;border:none;padding:12px 30px;border-radius:8px;font-size:1em;cursor:pointer} |
| button:hover{background:#c73652} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="icon">🔌</div> |
| <h1>No Internet Connection</h1> |
| <p>Please check your network settings and try again.</p> |
| <button onclick="location.reload()">Retry</button> |
| </div> |
| </body> |
| </html>""" |
|
|
|
|
| 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_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 <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" />') |
|
|
| 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="false" |
| android:hardwareAccelerated="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) |
| 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<String> 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<Uri[]> 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<Uri[]> 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) |
|
|
| |
| 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: |
| |
| 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 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.") |
|
|
| |
| config["actual_firebase_enabled"] = actual_firebase_enabled |
|
|
| create_build_gradle_root(project_dir, actual_firebase_enabled, use_onesignal) |
| create_settings_gradle(project_dir, config.get("app_name", "WebApp")) |
| create_gradle_properties(project_dir) |
| create_gradle_wrapper(project_dir) |
|
|
| create_app_build_gradle(app_dir, config) |
| create_proguard_rules(app_dir, config) |
|
|
| create_android_manifest(src_main, config) |
|
|
| create_main_activity(java_dir, config) |
|
|
| if config.get("splash_enabled"): |
| create_splash_activity(java_dir, config) |
|
|
| icon_b64 = config.get("app_icon_b64", "") |
| if icon_b64: |
| save_app_icon(res_dir, icon_b64) |
| else: |
| for folder in ["mipmap-mdpi", "mipmap-hdpi", "mipmap-xhdpi", "mipmap-xxhdpi", "mipmap-xxxhdpi"]: |
| mipmap_dir = os.path.join(res_dir, folder) |
| os.makedirs(mipmap_dir, exist_ok=True) |
| create_default_icon(os.path.join(mipmap_dir, "ic_launcher.png")) |
|
|
| splash_media_b64 = config.get("splash_media_b64", "") |
| if splash_media_b64 and config.get("splash_enabled"): |
| try: |
| if "," in splash_media_b64: |
| splash_media_b64 = splash_media_b64.split(",")[1] |
| media_bytes = base64.b64decode(splash_media_b64) |
| with open(os.path.join(assets_dir, "splash_media"), "wb") as f: |
| f.write(media_bytes) |
| config["has_splash_image"] = True |
| create_splash_activity(java_dir, config) |
| except Exception as e: |
| config["has_splash_image"] = False |
|
|
| if config.get("offline_mode", True): |
| with open(os.path.join(assets_dir, "offline.html"), "w") as f: |
| f.write(generate_offline_html()) |
|
|
| strings_xml = """<?xml version="1.0" encoding="utf-8"?> |
| <resources> |
| <string name="app_name">{app_name}</string> |
| </resources> |
| """.format(app_name=config.get("app_name", "WebApp").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)) |
| with open(os.path.join(res_dir, "values", "strings.xml"), "w") as f: |
| f.write(strings_xml) |
|
|
| colors_xml = """<?xml version="1.0" encoding="utf-8"?> |
| <resources> |
| <color name="colorPrimary">{status_color}</color> |
| <color name="colorPrimaryDark">{status_color}</color> |
| <color name="colorAccent">#e94560</color> |
| </resources> |
| """.format(status_color=config.get("status_bar_color", "#1a1a2e")) |
| with open(os.path.join(res_dir, "values", "colors.xml"), "w") as f: |
| f.write(colors_xml) |
|
|
| styles_xml = """<?xml version="1.0" encoding="utf-8"?> |
| <resources> |
| <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> |
| <item name="colorPrimary">@color/colorPrimary</item> |
| <item name="colorPrimaryDark">@color/colorPrimaryDark</item> |
| <item name="colorAccent">@color/colorAccent</item> |
| </style> |
| </resources> |
| """ |
| with open(os.path.join(res_dir, "values", "styles.xml"), "w") as f: |
| f.write(styles_xml) |
|
|
| 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/<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) |
|
|