diff --git a/v3/pkg/application/application_android.go b/v3/pkg/application/application_android.go index ce620a603..e2883206b 100644 --- a/v3/pkg/application/application_android.go +++ b/v3/pkg/application/application_android.go @@ -139,8 +139,11 @@ static void executeJavaScriptOnBridge(const char* js) { import "C" import ( + "context" + "encoding/json" "fmt" "io" + "log/slog" "net/http" "net/http/httptest" "strings" @@ -148,8 +151,6 @@ import ( "time" "unsafe" - "encoding/json" - "github.com/wailsapp/wails/v3/internal/runtime" ) @@ -221,6 +222,12 @@ func (a *App) platformRun() { 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() @@ -259,6 +266,9 @@ func newPlatformApp(app *App) *androidApp { 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") @@ -433,7 +443,7 @@ func Java_com_wails_app_WailsBridge_nativeOnPageFinished(env *C.JNIEnv, obj C.jo if win != nil { androidLogf("info", "🤖 [JNI] Injecting runtime.Core() into window %d", id) // Get the runtime core JavaScript - runtimeJS := runtime.Core() + 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. @@ -621,16 +631,60 @@ func serveAssetForAndroid(app *App, path string) ([]byte, error) { return body, nil } +// Global MessageProcessor for Android JNI callbacks +var ( + globalMessageProc *MessageProcessor + globalMessageProcLock sync.RWMutex +) + func handleMessageForAndroid(app *App, message string) string { - // Parse the message - var msg map[string]interface{} - if err := json.Unmarshal([]byte(message), &msg); err != nil { + // 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()) } - // TODO: Route to appropriate handler based on message type - // For now, return success - return `{"success":true}` + // 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 { diff --git a/v3/pkg/application/application_android_nocgo.go b/v3/pkg/application/application_android_nocgo.go index 045c5ae3f..556aecace 100644 --- a/v3/pkg/application/application_android_nocgo.go +++ b/v3/pkg/application/application_android_nocgo.go @@ -3,11 +3,12 @@ package application import ( + "context" + "encoding/json" "fmt" + "log/slog" "sync" "unsafe" - - "encoding/json" ) var ( @@ -38,6 +39,12 @@ func (a *App) platformRun() { globalApp = a globalAppLock.Unlock() + // Create MessageProcessor so message callbacks can route runtime calls + messageProc := NewMessageProcessor(slog.Default()) + globalMessageProcLock.Lock() + globalMessageProc = messageProc + globalMessageProcLock.Unlock() + androidLogf("info", "🤖 [application_android.go] Waiting for Android lifecycle...") // Block forever - Android manages the app lifecycle via JNI callbacks @@ -171,16 +178,58 @@ func serveAssetForAndroid(app *App, path string) ([]byte, error) { return nil, fmt.Errorf("asset serving not yet implemented: %s", path) } +// Global MessageProcessor for message callbacks +var ( + globalMessageProc *MessageProcessor + globalMessageProcLock sync.RWMutex +) + func handleMessageForAndroid(app *App, message string) string { - // Parse the message - var msg map[string]interface{} - if err := json.Unmarshal([]byte(message), &msg); err != nil { + // Some messages are plain strings (e.g. "wails:runtime:ready"), not JSON. + 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()) } - // TODO: Route to appropriate handler based on message type - // For now, return success - return `{"success":true}` + 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 { diff --git a/v3/pkg/events/events.go b/v3/pkg/events/events.go index 20bfcbeaa..bd75b862b 100644 --- a/v3/pkg/events/events.go +++ b/v3/pkg/events/events.go @@ -523,6 +523,40 @@ func newIOSEvents() iosEvents { } } +var Android = newAndroidEvents() + +type androidEvents struct { + ActivityCreated ApplicationEventType + ActivityStarted ApplicationEventType + ActivityResumed ApplicationEventType + ActivityPaused ApplicationEventType + ActivityStopped ApplicationEventType + ActivityDestroyed ApplicationEventType + ApplicationLowMemory ApplicationEventType + ApplicationConfigChanged ApplicationEventType + WebViewDidStartNavigation WindowEventType + WebViewDidFinishNavigation WindowEventType + WebViewDidFailNavigation WindowEventType + WebViewReceivedError WindowEventType +} + +func newAndroidEvents() androidEvents { + return androidEvents{ + ActivityCreated: 1259, + ActivityStarted: 1260, + ActivityResumed: 1261, + ActivityPaused: 1262, + ActivityStopped: 1263, + ActivityDestroyed: 1264, + ApplicationLowMemory: 1265, + ApplicationConfigChanged: 1266, + WebViewDidStartNavigation: 1267, + WebViewDidFinishNavigation: 1268, + WebViewDidFailNavigation: 1269, + WebViewReceivedError: 1270, + } +} + func JSEvent(event uint) string { return eventToJS[event] } @@ -763,4 +797,16 @@ var eventToJS = map[uint]string{ 1256: "ios:WebViewDidFinishNavigation", 1257: "ios:WebViewDidFailNavigation", 1258: "ios:WebViewDecidePolicyForNavigationAction", + 1259: "android:ActivityCreated", + 1260: "android:ActivityStarted", + 1261: "android:ActivityResumed", + 1262: "android:ActivityPaused", + 1263: "android:ActivityStopped", + 1264: "android:ActivityDestroyed", + 1265: "android:ApplicationLowMemory", + 1266: "android:ApplicationConfigChanged", + 1267: "android:WebViewDidStartNavigation", + 1268: "android:WebViewDidFinishNavigation", + 1269: "android:WebViewDidFailNavigation", + 1270: "android:WebViewReceivedError", }