diff --git a/resources/androidstudio/app/src/main/cpp/php_bridge.c b/resources/androidstudio/app/src/main/cpp/php_bridge.c index 35887e5..8992519 100644 --- a/resources/androidstudio/app/src/main/cpp/php_bridge.c +++ b/resources/androidstudio/app/src/main/cpp/php_bridge.c @@ -666,6 +666,12 @@ JNIEXPORT jstring JNICALL native_run_artisan_command(JNIEnv *env, jobject thiz, const char *command = (*env)->GetStringUTFChars(env, jcommand, NULL); LOGI("runArtisanCommand: %s", command); + // Lock ephemeral mutex to prevent background workers from starting + // ephemeral runtime while artisan commands are running (and vice versa). + // runArtisanCommand does php_embed_init/shutdown which destroys global + // state that ephemeral hot path would be using. + pthread_mutex_lock(&g_ephemeral_mutex); + clear_collected_output(); // Get Laravel path @@ -679,6 +685,7 @@ JNIEXPORT jstring JNICALL native_run_artisan_command(JNIEnv *env, jobject thiz, php_embed_module.ini_entries = "display_errors=1\nimplicit_flush=1\noutput_buffering=0\n"; if (php_embed_init(0, NULL) != SUCCESS) { LOGE("Failed to initialize PHP for artisan"); + pthread_mutex_unlock(&g_ephemeral_mutex); (*env)->ReleaseStringUTFChars(env, jcommand, command); (*env)->ReleaseStringUTFChars(env, jLaravelPath, cLaravelPath); (*env)->DeleteLocalRef(env, jLaravelPath); @@ -736,6 +743,8 @@ JNIEXPORT jstring JNICALL native_run_artisan_command(JNIEnv *env, jobject thiz, safe_php_embed_shutdown(); php_initialized = 0; + pthread_mutex_unlock(&g_ephemeral_mutex); + (*env)->ReleaseStringUTFChars(env, jcommand, command); (*env)->ReleaseStringUTFChars(env, jLaravelPath, cLaravelPath); (*env)->DeleteLocalRef(env, jLaravelPath); diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/LaravelEnvironment.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/LaravelEnvironment.kt index 75b167f..66a787d 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/LaravelEnvironment.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/LaravelEnvironment.kt @@ -674,6 +674,7 @@ class LaravelEnvironment(private val context: Context) { return outFile } + @Synchronized private fun copyAssetFile(assetName: String, outFile: File) { try { context.assets.open(assetName).use { input -> diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt index 500641b..2284b8c 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt @@ -11,7 +11,7 @@ import com.nativephp.mobile.network.PHPRequest import com.nativephp.mobile.security.LaravelCookieStore class PHPBridge(private val context: Context) { - private var lastPostData: String? = null + private val postDataByPath = ConcurrentHashMap>() private val requestDataMap = ConcurrentHashMap() private val phpExecutor = java.util.concurrent.Executors.newSingleThreadExecutor() @@ -248,50 +248,37 @@ class PHPBridge(private val context: Context) { return result } - // New function to store request data with a key - fun storeRequestData(key: String, data: String) { - requestDataMap[key] = data - Log.d(TAG, "Stored request data with key: $key (length=${data.length})") - - // Also update last post data for backward compatibility - lastPostData = data - - // Clean up old requests occasionally - if (requestDataMap.size > 10) { - cleanupOldRequests() - } + fun storePostData(url: String, data: String) { + val path = android.net.Uri.parse(url).path ?: url + val queue = postDataByPath.getOrPut(path) { java.util.concurrent.ConcurrentLinkedQueue() } + queue.add(data) + Log.d(TAG, "Queued POST data for $path (length=${data.length}, queue size=${queue.size})") } - // Clean up old request data - private fun cleanupOldRequests() { - val now = System.currentTimeMillis() - val keysToRemove = mutableListOf() - - // Find keys with timestamps older than MAX_REQUEST_AGE - requestDataMap.keys.forEach { key -> - if (key.contains("-")) { - val timestampStr = key.substringAfterLast("-") - try { - val timestamp = timestampStr.toLong() - if (now - timestamp > MAX_REQUEST_AGE) { - keysToRemove.add(key) - } - } catch (e: NumberFormatException) { - // Key doesn't have a valid timestamp format, ignore + fun consumePostData(url: String): String? { + val path = android.net.Uri.parse(url).path ?: url + val queue = postDataByPath[path] + + // Try immediate poll + var data = queue?.poll() + + // If empty, the JS bridge may not have fired yet — wait briefly + if (data == null) { + for (i in 1..10) { + Thread.sleep(5) + data = (postDataByPath[path] ?: queue)?.poll() + if (data != null) { + Log.d(TAG, "POST data for $path arrived after ${i * 5}ms wait") + break } } } - // Remove old entries - keysToRemove.forEach { requestDataMap.remove(it) } - if (keysToRemove.isNotEmpty()) { - Log.d(TAG, "Cleaned up ${keysToRemove.size} old request entries") + if (data != null) { + Log.d(TAG, "Consumed POST data for $path (length=${data.length})") + } else { + Log.w(TAG, "No POST data for $path after 50ms — request may have no body") } - } - - fun getLastPostData(): String? { - val data = lastPostData - lastPostData = null // Clear after consumption to prevent stale data return data } diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/network/WebViewManager.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/network/WebViewManager.kt index e10787b..4aa3098 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/network/WebViewManager.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/network/WebViewManager.kt @@ -255,7 +255,12 @@ class WebViewManager( // Regular PHP requests url.contains("127.0.0.1") -> { Log.d(TAG, "🌐 Handling PHP request") - phpHandler.handlePHPRequest(request, phpBridge.getLastPostData()) + val postData = if (request.method.equals("POST", ignoreCase = true) || + request.method.equals("PUT", ignoreCase = true) || + request.method.equals("PATCH", ignoreCase = true)) { + phpBridge.consumePostData(url) + } else null + phpHandler.handlePHPRequest(request, postData) } else -> { Log.d(TAG, "↪️ Delegating to system handler: $url") @@ -492,16 +497,10 @@ class WebViewManager( class JSBridge(private val phpBridge: PHPBridge, private val TAG: String) { @JavascriptInterface fun logPostData(data: String, url: String, headers: String) { + Log.d("$TAG-JS", "📦 POST data captured for: $url (length=${data.length})") - // Create a unique key for this request - val requestKey = "$url-${System.currentTimeMillis()}" - Log.d("$TAG-JS", "📦 RequestKey: $data") - - // Store in phpBridge with the key - phpBridge.storeRequestData(requestKey, data) - - // Set as current request -// phpBridge.storeCurrentRequestKey(requestKey) + // Queue the POST data keyed by URL path for consumption by shouldInterceptRequest + phpBridge.storePostData(url, data) // Try to extract CSRF token LaravelSecurity.extractFromPostBody(data) diff --git a/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub b/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub index ed52365..bba01f8 100644 --- a/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub +++ b/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub @@ -14,4 +14,15 @@ fun registerPluginBridgeFunctions(activity: FragmentActivity, context: Context) {{ INIT_FUNCTIONS }} {{ REGISTRATIONS }} +} + +/** + * Register only bridge functions that require Context (not Activity). + * Used by WorkManager workers for cold-boot background execution + * when no Activity is available. + */ +fun registerContextOnlyBridgeFunctions(context: Context) { + val registry = BridgeFunctionRegistry.shared + +{{ CONTEXT_REGISTRATIONS }} } \ No newline at end of file diff --git a/resources/xcode/NativePHP/Bridge/Functions/EdgeFunctions.swift b/resources/xcode/NativePHP/Bridge/Functions/EdgeFunctions.swift index 21593eb..7259533 100644 --- a/resources/xcode/NativePHP/Bridge/Functions/EdgeFunctions.swift +++ b/resources/xcode/NativePHP/Bridge/Functions/EdgeFunctions.swift @@ -27,7 +27,6 @@ enum EdgeFunctions { } print("🎨 Edge.Set called with \(components.count) component(s)") - print("🎨 Edge.Set components: \(components)") // Convert components back to JSON string for NativeUIState do { diff --git a/src/Plugins/Compilers/AndroidPluginCompiler.php b/src/Plugins/Compilers/AndroidPluginCompiler.php index 07ea682..9d71cb0 100644 --- a/src/Plugins/Compilers/AndroidPluginCompiler.php +++ b/src/Plugins/Compilers/AndroidPluginCompiler.php @@ -339,6 +339,20 @@ protected function renderRegistrationTemplate(array $registrations, array $initF }) ->implode("\n\n"); + // Generate context-only registrations for cold-boot WorkManager execution + $contextRegisterCalls = collect($registrations) + ->filter(function ($reg) { + $params = $reg['params'] ?? ['activity']; + + return ! in_array('activity', $params) && in_array('context', $params); + }) + ->map(function ($reg) { + $className = $this->extractClassName($reg['class']); + + return " // Plugin: {$reg['plugin']}\n registry.register(\"{$reg['name']}\", {$className}(context))"; + }) + ->implode("\n\n"); + $initCalls = collect($initFunctions) ->map(function ($init) { // Extract just the function name from the full path @@ -354,6 +368,7 @@ protected function renderRegistrationTemplate(array $registrations, array $initF 'IMPORTS' => $imports, 'INIT_FUNCTIONS' => $initCalls, 'REGISTRATIONS' => $registerCalls, + 'CONTEXT_REGISTRATIONS' => $contextRegisterCalls ?: ' // No context-only bridge functions registered', ]) ->render(); }