mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 22:55:48 +01:00
Fixes five regressions in Android support: 1. Restore events.Android struct (IDs 1259-1270) and JS event mappings that were removed from events.go, breaking events_common_android.go 2. Pass flags to runtime.Core() in nativeOnPageFinished to match the updated API signature used by all other platforms 3. Call setupCommonEvents() in the CGO build's run() method (the non-CGO build already did this) 4. Replace the stub handleMessageForAndroid() with real MessageProcessor routing so JS-to-Go runtime calls (bound methods, clipboard, etc.) actually work 5. Handle non-JSON string messages (e.g. "wails:runtime:ready") that the JS bridge sends before attempting JSON parse Fixes #5020 Co-authored-by: Varun Chawla <varun_6april@hotmail.com>
741 lines
21 KiB
Go
741 lines
21 KiB
Go
//go:build android && cgo && !server
|
|
|
|
package application
|
|
|
|
/*
|
|
#include <jni.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// Global JavaVM reference for thread attachment
|
|
static JavaVM* g_jvm = NULL;
|
|
|
|
// Global reference to bridge object (must be a global ref, not local)
|
|
static jobject g_bridge = NULL;
|
|
|
|
// Cached method ID for executeJavaScript
|
|
static jmethodID g_executeJsMethod = NULL;
|
|
|
|
// Helper function to convert Java String to C string
|
|
static const char* jstringToC(JNIEnv *env, jstring jstr) {
|
|
if (jstr == NULL) return NULL;
|
|
return (*env)->GetStringUTFChars(env, jstr, NULL);
|
|
}
|
|
|
|
// Helper function to release Java String
|
|
static void releaseJString(JNIEnv *env, jstring jstr, const char* cstr) {
|
|
if (jstr != NULL && cstr != NULL) {
|
|
(*env)->ReleaseStringUTFChars(env, jstr, cstr);
|
|
}
|
|
}
|
|
|
|
// Helper function to create Java byte array from C data
|
|
static jbyteArray createByteArray(JNIEnv *env, const void* data, int len) {
|
|
if (data == NULL || len <= 0) return NULL;
|
|
jbyteArray arr = (*env)->NewByteArray(env, len);
|
|
if (arr != NULL) {
|
|
(*env)->SetByteArrayRegion(env, arr, 0, len, (const jbyte*)data);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Helper function to create Java String from C string
|
|
static jstring createJString(JNIEnv *env, const char* str) {
|
|
if (str == NULL) return NULL;
|
|
return (*env)->NewStringUTF(env, str);
|
|
}
|
|
|
|
// Store JavaVM and create global reference to bridge
|
|
static void storeBridgeRef(JNIEnv *env, jobject bridge) {
|
|
// Get JavaVM
|
|
if ((*env)->GetJavaVM(env, &g_jvm) != 0) {
|
|
return;
|
|
}
|
|
|
|
// Create global reference to bridge object
|
|
g_bridge = (*env)->NewGlobalRef(env, bridge);
|
|
if (g_bridge == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Cache the executeJavaScript method ID
|
|
jclass bridgeClass = (*env)->GetObjectClass(env, g_bridge);
|
|
if (bridgeClass != NULL) {
|
|
g_executeJsMethod = (*env)->GetMethodID(env, bridgeClass, "executeJavaScript", "(Ljava/lang/String;)V");
|
|
(*env)->DeleteLocalRef(env, bridgeClass);
|
|
}
|
|
}
|
|
|
|
// Android logging via __android_log_print
|
|
#include <android/log.h>
|
|
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "WailsNative", __VA_ARGS__)
|
|
|
|
// Execute JavaScript via the bridge - can be called from any thread
|
|
static void executeJavaScriptOnBridge(const char* js) {
|
|
LOGD("executeJavaScriptOnBridge called, js length: %d", js ? (int)strlen(js) : -1);
|
|
|
|
if (g_jvm == NULL) {
|
|
LOGD("executeJavaScriptOnBridge: g_jvm is NULL");
|
|
return;
|
|
}
|
|
if (g_bridge == NULL) {
|
|
LOGD("executeJavaScriptOnBridge: g_bridge is NULL");
|
|
return;
|
|
}
|
|
if (g_executeJsMethod == NULL) {
|
|
LOGD("executeJavaScriptOnBridge: g_executeJsMethod is NULL");
|
|
return;
|
|
}
|
|
if (js == NULL) {
|
|
LOGD("executeJavaScriptOnBridge: js is NULL");
|
|
return;
|
|
}
|
|
|
|
JNIEnv *env = NULL;
|
|
int needsDetach = 0;
|
|
|
|
// Get JNIEnv for current thread
|
|
jint result = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6);
|
|
LOGD("executeJavaScriptOnBridge: GetEnv result = %d", result);
|
|
if (result == JNI_EDETACHED) {
|
|
// Attach current thread to JVM
|
|
LOGD("executeJavaScriptOnBridge: Attaching thread");
|
|
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != 0) {
|
|
LOGD("executeJavaScriptOnBridge: AttachCurrentThread failed");
|
|
return;
|
|
}
|
|
needsDetach = 1;
|
|
} else if (result != JNI_OK) {
|
|
LOGD("executeJavaScriptOnBridge: GetEnv failed with %d", result);
|
|
return;
|
|
}
|
|
|
|
// Create Java string and call method
|
|
jstring jJs = (*env)->NewStringUTF(env, js);
|
|
LOGD("executeJavaScriptOnBridge: jJs created: %p", jJs);
|
|
if (jJs != NULL) {
|
|
LOGD("executeJavaScriptOnBridge: Calling Java method");
|
|
(*env)->CallVoidMethod(env, g_bridge, g_executeJsMethod, jJs);
|
|
LOGD("executeJavaScriptOnBridge: Java method called");
|
|
(*env)->DeleteLocalRef(env, jJs);
|
|
}
|
|
|
|
// Check for exceptions
|
|
if ((*env)->ExceptionCheck(env)) {
|
|
LOGD("executeJavaScriptOnBridge: Exception occurred!");
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
|
|
// Detach if we attached
|
|
if (needsDetach) {
|
|
LOGD("executeJavaScriptOnBridge: Detaching thread");
|
|
(*g_jvm)->DetachCurrentThread(g_jvm);
|
|
}
|
|
|
|
LOGD("executeJavaScriptOnBridge: Done");
|
|
}
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/runtime"
|
|
)
|
|
|
|
var (
|
|
// Global reference to the app for JNI callbacks
|
|
globalApp *App
|
|
globalAppLock sync.RWMutex
|
|
|
|
// JNI environment and class references
|
|
javaVM unsafe.Pointer
|
|
bridgeObject unsafe.Pointer
|
|
|
|
// Android main function registration
|
|
androidMainFunc func()
|
|
androidMainLock sync.Mutex
|
|
|
|
// App ready signal
|
|
appReady = make(chan struct{})
|
|
appReadyOnce sync.Once
|
|
)
|
|
|
|
func init() {
|
|
androidLogf("info", "🤖 [application_android.go] init() called")
|
|
}
|
|
|
|
// RegisterAndroidMain registers the main function to be called when the Android app starts.
|
|
// This should be called from init() in your main.go file for Android builds.
|
|
// Example:
|
|
//
|
|
// func init() {
|
|
// application.RegisterAndroidMain(main)
|
|
// }
|
|
func RegisterAndroidMain(mainFunc func()) {
|
|
androidMainLock.Lock()
|
|
defer androidMainLock.Unlock()
|
|
androidMainFunc = mainFunc
|
|
androidLogf("info", "🤖 [application_android.go] Android main function registered")
|
|
}
|
|
|
|
// signalAppReady signals that the app is ready to serve requests
|
|
func signalAppReady() {
|
|
appReadyOnce.Do(func() {
|
|
close(appReady)
|
|
androidLogf("info", "🤖 [application_android.go] App ready signal sent")
|
|
})
|
|
}
|
|
|
|
// waitForAppReady waits for the app to be ready, with a timeout
|
|
func waitForAppReady(timeout time.Duration) bool {
|
|
select {
|
|
case <-appReady:
|
|
return true
|
|
case <-time.After(timeout):
|
|
return false
|
|
}
|
|
}
|
|
|
|
func androidLogf(level string, format string, a ...interface{}) {
|
|
msg := fmt.Sprintf(format, a...)
|
|
// For now, just use println - we'll connect to Android's Log.* later
|
|
println(fmt.Sprintf("[Android/%s] %s", level, msg))
|
|
}
|
|
|
|
func (a *App) platformRun() {
|
|
androidLogf("info", "🤖 [application_android.go] platformRun() called")
|
|
|
|
// Store global reference for JNI callbacks
|
|
globalAppLock.Lock()
|
|
globalApp = a
|
|
globalAppLock.Unlock()
|
|
|
|
// Create MessageProcessor so JNI message callbacks can route runtime calls
|
|
messageProc := NewMessageProcessor(slog.Default())
|
|
globalMessageProcLock.Lock()
|
|
globalMessageProc = messageProc
|
|
globalMessageProcLock.Unlock()
|
|
|
|
// Signal that the app is ready to serve requests
|
|
signalAppReady()
|
|
|
|
androidLogf("info", "🤖 [application_android.go] App ready, waiting for Android lifecycle...")
|
|
|
|
// Block forever - Android manages the app lifecycle via JNI callbacks
|
|
select {}
|
|
}
|
|
|
|
func (a *App) platformQuit() {
|
|
androidLogf("info", "🤖 [application_android.go] platformQuit() called")
|
|
// Android will handle app termination
|
|
}
|
|
|
|
func (a *App) isDarkMode() bool {
|
|
// TODO: Query Android for dark mode status
|
|
return false
|
|
}
|
|
|
|
func (a *App) isWindows() bool {
|
|
return false
|
|
}
|
|
|
|
// Platform-specific app implementation for Android
|
|
type androidApp struct {
|
|
parent *App
|
|
}
|
|
|
|
func newPlatformApp(app *App) *androidApp {
|
|
androidLogf("info", "🤖 [application_android.go] newPlatformApp() called")
|
|
return &androidApp{
|
|
parent: app,
|
|
}
|
|
}
|
|
|
|
func (a *androidApp) run() error {
|
|
androidLogf("info", "🤖 [application_android.go] androidApp.run() called")
|
|
|
|
// Wire platform events → common events (same as non-CGO build)
|
|
a.setupCommonEvents()
|
|
|
|
// Emit application started event
|
|
a.parent.Event.Emit("ApplicationStarted")
|
|
|
|
a.parent.platformRun()
|
|
return nil
|
|
}
|
|
|
|
func (a *androidApp) destroy() {
|
|
androidLogf("info", "🤖 [application_android.go] androidApp.destroy() called")
|
|
}
|
|
|
|
func (a *androidApp) setIcon(_ []byte) {
|
|
// Android app icon is set through AndroidManifest.xml
|
|
}
|
|
|
|
func (a *androidApp) name() string {
|
|
return a.parent.options.Name
|
|
}
|
|
|
|
func (a *androidApp) GetFlags(options Options) map[string]any {
|
|
return nil
|
|
}
|
|
|
|
func (a *androidApp) getAccentColor() string {
|
|
return ""
|
|
}
|
|
|
|
func (a *androidApp) getCurrentWindowID() uint {
|
|
return 0
|
|
}
|
|
|
|
func (a *androidApp) hide() {
|
|
// Android manages app visibility
|
|
}
|
|
|
|
func (a *androidApp) isDarkMode() bool {
|
|
return a.parent.isDarkMode()
|
|
}
|
|
|
|
func (a *androidApp) on(_ uint) {
|
|
// Android event handling
|
|
}
|
|
|
|
func (a *androidApp) setApplicationMenu(_ *Menu) {
|
|
// Android doesn't have application menus in the same way
|
|
}
|
|
|
|
func (a *androidApp) show() {
|
|
// Android manages app visibility
|
|
}
|
|
|
|
func (a *androidApp) showAboutDialog(_ string, _ string, _ []byte) {
|
|
// TODO: Implement Android about dialog
|
|
}
|
|
|
|
func (a *androidApp) getPrimaryScreen() (*Screen, error) {
|
|
screens, err := getScreens()
|
|
if err != nil || len(screens) == 0 {
|
|
return nil, err
|
|
}
|
|
return screens[0], nil
|
|
}
|
|
|
|
func (a *androidApp) getScreens() ([]*Screen, error) {
|
|
return getScreens()
|
|
}
|
|
|
|
func (a *App) logPlatformInfo() {
|
|
// Log Android platform info
|
|
androidLogf("info", "Platform: Android")
|
|
}
|
|
|
|
func (a *App) platformEnvironment() map[string]any {
|
|
return map[string]any{
|
|
"platform": "android",
|
|
}
|
|
}
|
|
|
|
func fatalHandler(errFunc func(error)) {
|
|
// Android fatal handler
|
|
}
|
|
|
|
// JNI Export Functions - Called from Java
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeInit
|
|
func Java_com_wails_app_WailsBridge_nativeInit(env *C.JNIEnv, obj C.jobject, bridge C.jobject) {
|
|
androidLogf("info", "🤖 [JNI] nativeInit called")
|
|
|
|
// Store references for later use (legacy - keeping for compatibility)
|
|
javaVM = unsafe.Pointer(env)
|
|
bridgeObject = unsafe.Pointer(bridge)
|
|
|
|
// Store JavaVM and bridge global reference for JNI callbacks
|
|
C.storeBridgeRef(env, bridge)
|
|
androidLogf("info", "🤖 [JNI] Bridge reference stored for JNI callbacks")
|
|
|
|
// Start the registered main function in a goroutine
|
|
androidMainLock.Lock()
|
|
mainFunc := androidMainFunc
|
|
androidMainLock.Unlock()
|
|
|
|
if mainFunc != nil {
|
|
androidLogf("info", "🤖 [JNI] Starting registered main function in goroutine")
|
|
go mainFunc()
|
|
} else {
|
|
androidLogf("warn", "🤖 [JNI] No main function registered! Call application.RegisterAndroidMain(main) in init()")
|
|
}
|
|
|
|
androidLogf("info", "🤖 [JNI] nativeInit complete")
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeShutdown
|
|
func Java_com_wails_app_WailsBridge_nativeShutdown(env *C.JNIEnv, obj C.jobject) {
|
|
androidLogf("info", "🤖 [JNI] nativeShutdown called")
|
|
|
|
globalAppLock.Lock()
|
|
if globalApp != nil {
|
|
globalApp.Quit()
|
|
}
|
|
globalAppLock.Unlock()
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeOnResume
|
|
func Java_com_wails_app_WailsBridge_nativeOnResume(env *C.JNIEnv, obj C.jobject) {
|
|
androidLogf("info", "🤖 [JNI] nativeOnResume called")
|
|
|
|
globalAppLock.RLock()
|
|
app := globalApp
|
|
globalAppLock.RUnlock()
|
|
|
|
if app != nil {
|
|
app.Event.Emit("ApplicationResumed")
|
|
}
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeOnPause
|
|
func Java_com_wails_app_WailsBridge_nativeOnPause(env *C.JNIEnv, obj C.jobject) {
|
|
androidLogf("info", "🤖 [JNI] nativeOnPause called")
|
|
|
|
globalAppLock.RLock()
|
|
app := globalApp
|
|
globalAppLock.RUnlock()
|
|
|
|
if app != nil {
|
|
app.Event.Emit("ApplicationPaused")
|
|
}
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeOnPageFinished
|
|
func Java_com_wails_app_WailsBridge_nativeOnPageFinished(env *C.JNIEnv, obj C.jobject, jurl C.jstring) {
|
|
cUrl := C.jstringToC(env, jurl)
|
|
defer C.releaseJString(env, jurl, cUrl)
|
|
url := C.GoString(cUrl)
|
|
|
|
androidLogf("info", "🤖 [JNI] nativeOnPageFinished called: %s", url)
|
|
|
|
globalAppLock.RLock()
|
|
app := globalApp
|
|
globalAppLock.RUnlock()
|
|
|
|
if app == nil {
|
|
androidLogf("error", "🤖 [JNI] nativeOnPageFinished: app is nil")
|
|
return
|
|
}
|
|
|
|
// Inject the runtime into the first window (with proper locking)
|
|
app.windowsLock.RLock()
|
|
windowCount := len(app.windows)
|
|
androidLogf("info", "🤖 [JNI] nativeOnPageFinished: window count = %d", windowCount)
|
|
for id, win := range app.windows {
|
|
androidLogf("info", "🤖 [JNI] Found window ID: %d", id)
|
|
if win != nil {
|
|
androidLogf("info", "🤖 [JNI] Injecting runtime.Core() into window %d", id)
|
|
// Get the runtime core JavaScript
|
|
runtimeJS := runtime.Core(app.impl.GetFlags(app.options))
|
|
androidLogf("info", "🤖 [JNI] Runtime JS length: %d bytes", len(runtimeJS))
|
|
app.windowsLock.RUnlock()
|
|
// IMPORTANT: We must bypass win.ExecJS because it queues if runtimeLoaded is false.
|
|
// On Android, we need to inject the runtime directly since the runtime hasn't been loaded yet.
|
|
// This is the bootstrap injection that enables the runtime to load.
|
|
androidLogf("info", "🤖 [JNI] Calling executeJavaScript directly (bypassing queue)")
|
|
executeJavaScript(runtimeJS)
|
|
// Emit event
|
|
app.Event.Emit("PageFinished", url)
|
|
return
|
|
}
|
|
}
|
|
app.windowsLock.RUnlock()
|
|
|
|
androidLogf("warn", "🤖 [JNI] nativeOnPageFinished: no windows found to inject runtime")
|
|
// Emit event even if no windows
|
|
app.Event.Emit("PageFinished", url)
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeServeAsset
|
|
func Java_com_wails_app_WailsBridge_nativeServeAsset(env *C.JNIEnv, obj C.jobject, jpath C.jstring, jmethod C.jstring, jheaders C.jstring) C.jbyteArray {
|
|
// Convert Java strings to Go strings
|
|
cPath := C.jstringToC(env, jpath)
|
|
cMethod := C.jstringToC(env, jmethod)
|
|
defer C.releaseJString(env, jpath, cPath)
|
|
defer C.releaseJString(env, jmethod, cMethod)
|
|
|
|
goPath := C.GoString(cPath)
|
|
goMethod := C.GoString(cMethod)
|
|
|
|
androidLogf("debug", "🤖 [JNI] nativeServeAsset: %s %s", goMethod, goPath)
|
|
|
|
// Wait for the app to be ready (timeout after 10 seconds)
|
|
if !waitForAppReady(10 * time.Second) {
|
|
androidLogf("error", "🤖 [JNI] Timeout waiting for app to be ready")
|
|
return C.createByteArray(env, nil, 0)
|
|
}
|
|
|
|
globalAppLock.RLock()
|
|
app := globalApp
|
|
globalAppLock.RUnlock()
|
|
|
|
if app == nil || app.assets == nil {
|
|
androidLogf("error", "🤖 [JNI] App or assets not initialized after ready signal")
|
|
return C.createByteArray(env, nil, 0)
|
|
}
|
|
|
|
// Serve the asset through the asset server
|
|
data, err := serveAssetForAndroid(app, goPath)
|
|
if err != nil {
|
|
androidLogf("error", "🤖 [JNI] Error serving asset %s: %v", goPath, err)
|
|
return C.createByteArray(env, nil, 0)
|
|
}
|
|
|
|
androidLogf("debug", "🤖 [JNI] Serving asset %s (%d bytes)", goPath, len(data))
|
|
|
|
// Create Java byte array from the data
|
|
// Handle empty data case to avoid index out of range panic
|
|
if len(data) == 0 {
|
|
return C.createByteArray(env, nil, 0)
|
|
}
|
|
return C.createByteArray(env, unsafe.Pointer(&data[0]), C.int(len(data)))
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeHandleMessage
|
|
func Java_com_wails_app_WailsBridge_nativeHandleMessage(env *C.JNIEnv, obj C.jobject, jmessage C.jstring) C.jstring {
|
|
// Convert Java string to Go string
|
|
cMessage := C.jstringToC(env, jmessage)
|
|
defer C.releaseJString(env, jmessage, cMessage)
|
|
|
|
goMessage := C.GoString(cMessage)
|
|
|
|
androidLogf("debug", "🤖 [JNI] nativeHandleMessage: %s", goMessage)
|
|
|
|
globalAppLock.RLock()
|
|
app := globalApp
|
|
globalAppLock.RUnlock()
|
|
|
|
if app == nil {
|
|
errorResponse := `{"error":"App not initialized"}`
|
|
return C.createJString(env, C.CString(errorResponse))
|
|
}
|
|
|
|
// Parse and handle the message
|
|
response := handleMessageForAndroid(app, goMessage)
|
|
return C.createJString(env, C.CString(response))
|
|
}
|
|
|
|
//export Java_com_wails_app_WailsBridge_nativeGetAssetMimeType
|
|
func Java_com_wails_app_WailsBridge_nativeGetAssetMimeType(env *C.JNIEnv, obj C.jobject, jpath C.jstring) C.jstring {
|
|
// Convert Java string to Go string
|
|
cPath := C.jstringToC(env, jpath)
|
|
defer C.releaseJString(env, jpath, cPath)
|
|
|
|
goPath := C.GoString(cPath)
|
|
mimeType := getMimeTypeForPath(goPath)
|
|
return C.createJString(env, C.CString(mimeType))
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func serveAssetForAndroid(app *App, path string) ([]byte, error) {
|
|
// Check if this is a runtime call (includes query string)
|
|
isRuntimeCall := strings.HasPrefix(path, "/wails/runtime")
|
|
|
|
// Normalize path for regular assets (not runtime calls)
|
|
if !isRuntimeCall {
|
|
if path == "" || path == "/" {
|
|
path = "/index.html"
|
|
}
|
|
}
|
|
|
|
// Ensure path starts with /
|
|
if len(path) > 0 && path[0] != '/' {
|
|
path = "/" + path
|
|
}
|
|
|
|
// Check if asset server is available
|
|
if app.assets == nil {
|
|
return nil, fmt.Errorf("asset server not initialized")
|
|
}
|
|
|
|
// Create a fake HTTP request
|
|
fullURL := "https://wails.localhost" + path
|
|
androidLogf("debug", "🤖 [serveAssetForAndroid] Creating request for: %s", fullURL)
|
|
|
|
req, err := http.NewRequest("GET", fullURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// For runtime calls (/wails/runtime), we need to add the window ID header
|
|
// This is required by the MessageProcessor to route the call correctly
|
|
if isRuntimeCall {
|
|
// Get the first window (on Android, there's typically only one)
|
|
windows := app.Window.GetAll()
|
|
androidLogf("debug", "🤖 [serveAssetForAndroid] Runtime call, found %d windows", len(windows))
|
|
if len(windows) > 0 {
|
|
// Use the first window's ID
|
|
windowID := windows[0].ID()
|
|
req.Header.Set("x-wails-window-id", fmt.Sprintf("%d", windowID))
|
|
androidLogf("debug", "🤖 [serveAssetForAndroid] Added window ID header: %d", windowID)
|
|
} else {
|
|
androidLogf("warn", "🤖 [serveAssetForAndroid] No windows available for runtime call")
|
|
}
|
|
}
|
|
|
|
// Use httptest.ResponseRecorder to capture the response
|
|
recorder := httptest.NewRecorder()
|
|
|
|
// Serve the request through the asset server
|
|
app.assets.ServeHTTP(recorder, req)
|
|
|
|
// Check response status
|
|
result := recorder.Result()
|
|
defer result.Body.Close()
|
|
|
|
// Read the response body
|
|
body, err := io.ReadAll(result.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
androidLogf("debug", "🤖 [serveAssetForAndroid] Response status: %d, body length: %d", result.StatusCode, len(body))
|
|
|
|
// For runtime calls, we need to return the body even for error responses
|
|
// so the JavaScript can see the error message
|
|
if isRuntimeCall {
|
|
if result.StatusCode != http.StatusOK {
|
|
androidLogf("warn", "🤖 [serveAssetForAndroid] Runtime call returned status %d: %s", result.StatusCode, string(body))
|
|
}
|
|
// Return the body regardless of status - the JS will handle errors
|
|
return body, nil
|
|
}
|
|
|
|
// For regular assets, check status codes
|
|
if result.StatusCode == http.StatusNotFound {
|
|
return nil, fmt.Errorf("asset not found: %s", path)
|
|
}
|
|
|
|
if result.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("asset server error: status %d for %s", result.StatusCode, path)
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
// Global MessageProcessor for Android JNI callbacks
|
|
var (
|
|
globalMessageProc *MessageProcessor
|
|
globalMessageProcLock sync.RWMutex
|
|
)
|
|
|
|
func handleMessageForAndroid(app *App, message string) string {
|
|
// Some messages are plain strings (e.g. "wails:runtime:ready"), not JSON.
|
|
// The JS bridge sends: typeof m==='string' ? m : JSON.stringify(m)
|
|
if len(message) == 0 || message[0] != '{' {
|
|
androidLogf("debug", "🤖 [handleMessageForAndroid] Non-JSON message: %s", message)
|
|
return `{"success":true}`
|
|
}
|
|
|
|
var req RuntimeRequest
|
|
if err := json.Unmarshal([]byte(message), &req); err != nil {
|
|
androidLogf("error", "🤖 [handleMessageForAndroid] Failed to parse: %v", err)
|
|
return fmt.Sprintf(`{"error":"%s"}`, err.Error())
|
|
}
|
|
|
|
// Fill in a window ID if none was provided (Android typically has one window)
|
|
if req.WebviewWindowID == 0 && req.WebviewWindowName == "" {
|
|
windows := app.Window.GetAll()
|
|
if len(windows) > 0 {
|
|
req.WebviewWindowID = uint32(windows[0].ID())
|
|
}
|
|
}
|
|
|
|
globalMessageProcLock.RLock()
|
|
messageProc := globalMessageProc
|
|
globalMessageProcLock.RUnlock()
|
|
|
|
if messageProc == nil {
|
|
androidLogf("error", "🤖 [handleMessageForAndroid] MessageProcessor not initialized")
|
|
return `{"error":"MessageProcessor not initialized"}`
|
|
}
|
|
|
|
ctx := context.Background()
|
|
result, err := messageProc.HandleRuntimeCallWithIDs(ctx, &req)
|
|
if err != nil {
|
|
androidLogf("error", "🤖 [handleMessageForAndroid] Error: %v", err)
|
|
return fmt.Sprintf(`{"error":"%s"}`, err.Error())
|
|
}
|
|
|
|
if result == nil {
|
|
return `{"success":true}`
|
|
}
|
|
|
|
resp, err := json.Marshal(result)
|
|
if err != nil {
|
|
androidLogf("error", "🤖 [handleMessageForAndroid] Marshal error: %v", err)
|
|
return fmt.Sprintf(`{"error":"%s"}`, err.Error())
|
|
}
|
|
return string(resp)
|
|
}
|
|
|
|
func getMimeTypeForPath(path string) string {
|
|
// Simple MIME type detection based on extension
|
|
switch {
|
|
case endsWith(path, ".html"), endsWith(path, ".htm"):
|
|
return "text/html"
|
|
case endsWith(path, ".js"), endsWith(path, ".mjs"):
|
|
return "application/javascript"
|
|
case endsWith(path, ".css"):
|
|
return "text/css"
|
|
case endsWith(path, ".json"):
|
|
return "application/json"
|
|
case endsWith(path, ".png"):
|
|
return "image/png"
|
|
case endsWith(path, ".jpg"), endsWith(path, ".jpeg"):
|
|
return "image/jpeg"
|
|
case endsWith(path, ".gif"):
|
|
return "image/gif"
|
|
case endsWith(path, ".svg"):
|
|
return "image/svg+xml"
|
|
case endsWith(path, ".ico"):
|
|
return "image/x-icon"
|
|
case endsWith(path, ".woff"):
|
|
return "font/woff"
|
|
case endsWith(path, ".woff2"):
|
|
return "font/woff2"
|
|
case endsWith(path, ".ttf"):
|
|
return "font/ttf"
|
|
default:
|
|
return "application/octet-stream"
|
|
}
|
|
}
|
|
|
|
func endsWith(s, suffix string) bool {
|
|
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
|
}
|
|
|
|
// executeJavaScript executes JavaScript code in the Android WebView via JNI callback
|
|
func executeJavaScript(js string) {
|
|
androidLogf("info", "🤖 executeJavaScript called, length: %d", len(js))
|
|
if js == "" {
|
|
androidLogf("warn", "🤖 executeJavaScript: empty JS string")
|
|
return
|
|
}
|
|
|
|
// Convert Go string to C string and call the JNI bridge
|
|
androidLogf("info", "🤖 executeJavaScript: calling C.executeJavaScriptOnBridge")
|
|
cJs := C.CString(js)
|
|
defer C.free(unsafe.Pointer(cJs))
|
|
|
|
C.executeJavaScriptOnBridge(cJs)
|
|
androidLogf("info", "🤖 executeJavaScript: done")
|
|
}
|