wails/v3/pkg/application/application_android_nocgo.go
Lea Anthony 75eaaf29fb fix(v3): restore Android build and runtime message handling
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>
2026-02-27 21:54:57 +11:00

274 lines
6.6 KiB
Go

//go:build android && !cgo && !server
package application
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"sync"
"unsafe"
)
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
)
func init() {
androidLogf("info", "🤖 [application_android.go] init() called")
}
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 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
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 common events
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
}
// Helper functions
func serveAssetForAndroid(app *App, path string) ([]byte, error) {
// Normalize path
if path == "" || path == "/" {
path = "/index.html"
}
// TODO: Use the actual asset server to serve the file
// For now, return a placeholder
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 {
// 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())
}
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 is a stub for non-cgo builds
func executeJavaScript(js string) {
androidLogf("warn", "executeJavaScript called but cgo is not enabled")
}