Spaces:
Paused
Paused
| import os | |
| import sys | |
| import json | |
| import uuid | |
| import time | |
| import shutil | |
| import base64 | |
| import threading | |
| import subprocess | |
| import re | |
| from datetime import datetime | |
| from flask import Flask, request, jsonify, render_template, send_file, Response | |
| app = Flask(__name__) | |
| BUILD_DIR_BASE = "/tmp/builds" | |
| PERSISTENT_DIR = os.path.join(os.getcwd(), "persistent_data") | |
| KEYSTORE_PATH = os.path.join(PERSISTENT_DIR, "release-key.jks") | |
| LOG_STORE = {} | |
| BUILD_STATUS = {} | |
| BUILD_ARTIFACTS = {} | |
| os.makedirs(BUILD_DIR_BASE, exist_ok=True) | |
| os.makedirs(PERSISTENT_DIR, exist_ok=True) | |
| def init_keystore(): | |
| if not os.path.exists(KEYSTORE_PATH): | |
| keytool_cmd = [ | |
| "keytool", "-genkeypair", | |
| "-v", | |
| "-keystore", KEYSTORE_PATH, | |
| "-keyalg", "RSA", | |
| "-keysize", "2048", | |
| "-validity", "10000", | |
| "-alias", "releasekey", | |
| "-storepass", "android123", | |
| "-keypass", "android123", | |
| "-dname", "CN=SkyData, OU=Mobile, O=Company, L=City, S=State, C=US" | |
| ] | |
| subprocess.run(keytool_cmd, capture_output=True, timeout=60) | |
| init_keystore() | |
| def get_sha1(): | |
| if not os.path.exists(KEYSTORE_PATH): | |
| return None | |
| cmd = [ | |
| "keytool", "-list", "-v", | |
| "-keystore", KEYSTORE_PATH, | |
| "-storepass", "android123", | |
| "-alias", "releasekey" | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| match = re.search(r"SHA1:\s+([A-F0-9:]+)", result.stdout) | |
| if match: | |
| return match.group(1) | |
| return None | |
| def analyze_logs_ai(log_text): | |
| diagnosis_lines = [] | |
| if "Could not resolve" in log_text or "Could not find" in log_text: | |
| diagnosis_lines.append("خطأ في التبعيات: لم يتم العثور على مكتبة مطلوبة. تأكد من أن جميع المكتبات المحددة متوفرة في مستودعات Maven/Google.") | |
| if "FirebaseApp" in log_text or "google-services" in log_text or "com.google.gms" in log_text: | |
| diagnosis_lines.append("خطأ في Firebase: ملف google-services.json غير صالح أو غير متوافق مع اسم الحزمة. تحقق من إعدادات Firebase الخاصة بك.") | |
| if "AAPT" in log_text or "resource" in log_text.lower() and "not found" in log_text.lower(): | |
| diagnosis_lines.append("خطأ في الموارد: مورد XML أو صورة مفقودة. تأكد من صحة جميع ملفات الموارد.") | |
| if "Syntax error" in log_text or "Unexpected token" in log_text or "illegal start" in log_text.lower(): | |
| diagnosis_lines.append("خطأ في بناء الجملة: يوجد خطأ نحوي في كود Java. راجع ملفات المصدر بعناية.") | |
| if "OutOfMemoryError" in log_text or "heap" in log_text.lower(): | |
| diagnosis_lines.append("خطأ في الذاكرة: نفدت ذاكرة JVM أثناء البناء. حاول زيادة حجم الذاكرة المخصصة.") | |
| if "SDK location not found" in log_text or "ANDROID_HOME" in log_text: | |
| diagnosis_lines.append("خطأ في SDK: لم يتم العثور على Android SDK. تحقق من متغيرات البيئة.") | |
| if "Manifest merger failed" in log_text: | |
| diagnosis_lines.append("خطأ في دمج الـ Manifest: تعارض بين إعدادات AndroidManifest.xml. راجع الأذونات والإعدادات.") | |
| if "Execution failed for task" in log_text: | |
| task_match = re.search(r"Execution failed for task '([^']+)'", log_text) | |
| if task_match: | |
| diagnosis_lines.append("فشل تنفيذ المهمة: {} - تحقق من إعدادات هذه المهمة والتبعيات المرتبطة بها.".format(task_match.group(1))) | |
| if "minSdkVersion" in log_text: | |
| diagnosis_lines.append("خطأ في إصدار SDK الأدنى: إصدار SDK الأدنى المحدد غير متوافق مع إحدى المكتبات.") | |
| if "Duplicate class" in log_text: | |
| diagnosis_lines.append("خطأ تكرار: يوجد تكرار في الفئات بين المكتبات. استخدم exclude لحل التعارضات.") | |
| if "R8" in log_text and ("error" in log_text.lower() or "failed" in log_text.lower()): | |
| diagnosis_lines.append("خطأ في R8/ProGuard: فشل تقليص الكود. تحقق من قواعد ProGuard وتأكد من عدم حذف فئات مطلوبة.") | |
| if not diagnosis_lines: | |
| if "BUILD SUCCESSFUL" in log_text: | |
| diagnosis_lines.append("تم البناء بنجاح! لا توجد أخطاء.") | |
| else: | |
| diagnosis_lines.append("لم يتم التعرف على خطأ محدد. راجع سجل البناء الكامل يدوياً للبحث عن المشكلة.") | |
| return "\n".join(diagnosis_lines) | |
| def generate_offline_html(): | |
| return """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Offline</title> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| body{font-family:'Segoe UI',sans-serif;background:#1a1a2e;color:#e0e0e0;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center;padding:20px} | |
| .container{max-width:400px} | |
| h1{font-size:2em;margin-bottom:10px;color:#e94560} | |
| p{font-size:1.1em;color:#aaa;margin-bottom:20px} | |
| .icon{font-size:4em;margin-bottom:20px} | |
| 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 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'") | |
| # مكتبة Google Sign-In | |
| dependencies.append(" implementation 'com.google.android.gms:play-services-auth:20.7.0'") | |
| if use_firebase: | |
| dependencies.append(" implementation platform('com.google.firebase:firebase-bom:32.7.0')") | |
| dependencies.append(" implementation 'com.google.firebase:firebase-analytics'") | |
| dependencies.append(" implementation 'com.google.firebase:firebase-messaging'") | |
| if use_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", "") | |
| google_web_client_id = config.get("google_web_client_id", "") | |
| supabase_url = config.get("supabase_url", "") | |
| supabase_anon_key = config.get("supabase_anon_key", "") | |
| imports = [] | |
| imports.append("package {};".format(package_name)) | |
| imports.append("") | |
| imports.append("import android.app.Activity;") | |
| imports.append("import android.content.Context;") | |
| imports.append("import android.content.Intent;") | |
| imports.append("import android.content.pm.PackageManager;") | |
| imports.append("import android.graphics.Bitmap;") | |
| imports.append("import android.graphics.Color;") | |
| imports.append("import android.net.ConnectivityManager;") | |
| imports.append("import android.net.NetworkInfo;") | |
| imports.append("import android.net.Uri;") | |
| imports.append("import android.os.Build;") | |
| imports.append("import android.os.Bundle;") | |
| imports.append("import android.util.Log;") | |
| imports.append("import android.view.View;") | |
| imports.append("import android.view.Window;") | |
| imports.append("import android.view.WindowManager;") | |
| imports.append("import android.webkit.CookieManager;") | |
| imports.append("import android.webkit.GeolocationPermissions;") | |
| imports.append("import android.webkit.JavascriptInterface;") | |
| imports.append("import android.webkit.PermissionRequest;") | |
| imports.append("import android.webkit.ValueCallback;") | |
| imports.append("import android.webkit.WebChromeClient;") | |
| imports.append("import android.webkit.WebResourceRequest;") | |
| imports.append("import android.webkit.WebSettings;") | |
| imports.append("import android.webkit.WebView;") | |
| imports.append("import android.webkit.WebViewClient;") | |
| imports.append("import android.widget.FrameLayout;") | |
| imports.append("import android.widget.LinearLayout;") | |
| imports.append("import android.widget.ImageButton;") | |
| imports.append("import android.widget.ProgressBar;") | |
| imports.append("import android.widget.Toast;") | |
| imports.append("import androidx.appcompat.app.AppCompatActivity;") | |
| imports.append("import androidx.core.app.ActivityCompat;") | |
| imports.append("import androidx.core.content.ContextCompat;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignIn;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignInAccount;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignInClient;") | |
| imports.append("import com.google.android.gms.auth.api.signin.GoogleSignInOptions;") | |
| imports.append("import com.google.android.gms.common.api.ApiException;") | |
| imports.append("import com.google.android.gms.tasks.Task;") | |
| if 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); | |
| }""" | |
| google_signin_decl = """ private GoogleSignInClient mGoogleSignInClient; | |
| private static final int RC_SIGN_IN = 9001;""" | |
| google_signin_setup = """ GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) | |
| .requestIdToken("{client_id}") | |
| .requestEmail() | |
| .build(); | |
| mGoogleSignInClient = GoogleSignIn.getClient(this, gso); | |
| webView.addJavascriptInterface(new WebAppInterface(this), "AndroidBridge");""".format(client_id=google_web_client_id) | |
| js_interface_class = """ | |
| public class WebAppInterface {{ | |
| Context mContext; | |
| WebAppInterface(Context c) {{ mContext = c; }} | |
| @JavascriptInterface | |
| public void startGoogleSignIn() {{ | |
| Intent signInIntent = mGoogleSignInClient.getSignInIntent(); | |
| startActivityForResult(signInIntent, RC_SIGN_IN); | |
| }} | |
| @JavascriptInterface | |
| public String getSupabaseUrl() {{ return "{supa_url}"; }} | |
| @JavascriptInterface | |
| public String getSupabaseKey() {{ return "{supa_key}"; }} | |
| }}""".format(supa_url=supabase_url, supa_key=supabase_anon_key) | |
| on_activity_result_code = """ | |
| if (requestCode == RC_SIGN_IN) { | |
| Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data); | |
| try { | |
| GoogleSignInAccount account = task.getResult(ApiException.class); | |
| String idToken = account.getIdToken(); | |
| webView.post(() -> { | |
| webView.evaluateJavascript("javascript:handleGoogleLoginSuccess('" + idToken + "')", null); | |
| }); | |
| } catch (ApiException e) { | |
| Log.w("GoogleSignIn", "signInResult:failed code=" + e.getStatusCode()); | |
| webView.post(() -> { | |
| webView.evaluateJavascript("javascript:handleGoogleLoginError('" + e.getStatusCode() + "')", null); | |
| }); | |
| } | |
| return; | |
| }""" | |
| 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} | |
| {google_decl} | |
| @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); | |
| {google_setup} | |
| {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} | |
| }} | |
| {js_interface} | |
| @Override | |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) {{ | |
| super.onActivityResult(requestCode, resultCode, data); | |
| {on_act_res} | |
| 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, | |
| google_decl=google_signin_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, | |
| google_setup=google_signin_setup, | |
| 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, | |
| js_interface=js_interface_class, | |
| on_act_res=on_activity_result_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) | |
| shutil.copy2(KEYSTORE_PATH, os.path.join(project_dir, "release-key.jks")) | |
| local_props = "sdk.dir={}\n".format(os.environ.get("ANDROID_SDK_ROOT", "/opt/android-sdk")) | |
| with open(os.path.join(project_dir, "local.properties"), "w") as f: | |
| f.write(local_props) | |
| return project_dir | |
| def run_build(build_id, config): | |
| LOG_STORE[build_id] = [] | |
| BUILD_STATUS[build_id] = "running" | |
| def log(msg): | |
| timestamp = datetime.now().strftime("%H:%M:%S") | |
| LOG_STORE[build_id].append("[{}] {}".format(timestamp, msg)) | |
| try: | |
| log("=== Build Started ===") | |
| log("App Name: {}".format(config.get("app_name", "WebApp"))) | |
| log("Package: {}".format(config.get("package_name", "com.example.webapp"))) | |
| log("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" | |
| def index(): | |
| return render_template("index.html") | |
| def get_sha1_route(): | |
| sha1 = get_sha1() | |
| if sha1: | |
| return jsonify({"sha1": sha1, "formatted": sha1}) | |
| return jsonify({"error": "Keystore not found"}), 404 | |
| def start_build(): | |
| try: | |
| data = request.form.to_dict() | |
| config = {} | |
| config["supabase_url"] = data.get("supabase_url", "") | |
| config["supabase_anon_key"] = data.get("supabase_anon_key", "") | |
| config["google_web_client_id"] = data.get("google_web_client_id", "") | |
| 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 | |
| def get_logs(build_id): | |
| def generate(): | |
| last_index = 0 | |
| while True: | |
| logs = LOG_STORE.get(build_id, []) | |
| status = BUILD_STATUS.get(build_id, "unknown") | |
| if last_index < len(logs): | |
| new_logs = logs[last_index:] | |
| last_index = len(logs) | |
| for log_line in new_logs: | |
| yield "data: {}\n\n".format(json.dumps({"type": "log", "message": log_line})) | |
| if status in ("success", "failed"): | |
| yield "data: {}\n\n".format(json.dumps({"type": "status", "status": status, "build_id": build_id})) | |
| break | |
| time.sleep(0.5) | |
| return Response(generate(), mimetype="text/event-stream", headers={ | |
| "Cache-Control": "no-cache", | |
| "X-Accel-Buffering": "no", | |
| "Connection": "keep-alive" | |
| }) | |
| def download_artifact(build_id): | |
| artifact_path = BUILD_ARTIFACTS.get(build_id) | |
| if artifact_path and os.path.exists(artifact_path): | |
| return send_file(artifact_path, as_attachment=True, download_name=os.path.basename(artifact_path)) | |
| else: | |
| return jsonify({"error": "Artifact not found"}), 404 | |
| def get_status(build_id): | |
| status = BUILD_STATUS.get(build_id, "unknown") | |
| return jsonify({"status": status, "build_id": build_id}) | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=7860, debug=False, threaded=True) | |