Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions resources/androidstudio/app/src/main/cpp/php_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, java.util.concurrent.ConcurrentLinkedQueue<String>>()
private val requestDataMap = ConcurrentHashMap<String, String>()
private val phpExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()

Expand Down Expand Up @@ -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<String>()

// 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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions src/Plugins/Compilers/AndroidPluginCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand Down
Loading