wails/v3/pkg/application/application_android.go
Lea Anthony a06d55804c
perf(v3): optimize JSON processing and reduce allocations in hot paths (#4843)
* perf(v3): optimize JSON processing and reduce allocations in hot paths

- Switch to goccy/go-json for method binding, events, and HTTP transport
  (21-63% faster, 40-60% less memory for method calls)
- Optimize BoundMethod struct layout to reduce padding (144 -> 136 bytes)
- Cache isVariadic flag at registration to avoid reflect call per invocation
- Use stack-allocated buffer for method arguments (<=8 args)
- Optimize result collection to avoid slice allocation for single return values
- Use sync.Map for MIME cache to improve concurrent read performance
- Use buffer pool for HTTP transport request body reading
- Lazily allocate CloseNotify channel in content type sniffer
- Remove debug CSS logging from asset server
- Add comprehensive benchmark tests (build tag: bench)

Performance improvements for BoundMethod.Call:
- SimpleCall:   1290ns -> 930ns (28% faster), 240B -> 80B (67% less memory)
- ComplexCall:  10500ns -> 3900ns (63% faster), 1192B -> 1020B (14% less)
- VariadicCall: 3460ns -> 1600ns (54% faster), 512B -> 289B (44% less)

* perf(v3): add max size limit to buffer pool to prevent memory bloat

Buffers larger than 512KB are not returned to the pool, allowing GC
to reclaim memory after large requests (e.g., base64 encoded images).

* perf(v3): remove mimetype library dependency, saving ~208KB binary size

- Replace github.com/wailsapp/mimetype with expanded extension map + stdlib
- Expand MIME type map from 16 to 50+ common web formats (fonts, audio, video, etc.)
- Add comprehensive test suite validating MIME detection for all web formats
- Use http.DetectContentType as fallback for unknown extensions
- Actual binary size reduction: 1.2MB (11MB -> 9.8MB in test app)

* perf(v3): migrate all runtime code to goccy/go-json

Migrate remaining encoding/json usages to goccy/go-json in:
- pkg/application (android, darwin, ios, single_instance, webview_window)
- pkg/services (kvstore, notifications on all platforms)
- internal/assetserver/webview (request/response handling)
- internal/runtime and internal/capabilities

Note: encoding/json (110KB) remains in binary because:
1. goccy/go-json imports it for interface compatibility (json.Marshaler, etc.)
2. log/slog (stdlib) uses it for JSON output

The performance benefit is in the hot paths which now use the faster library.

* perf(v3): replace gopkg.in/ini.v1 with minimal .desktop file parser

Replace the gopkg.in/ini.v1 dependency with a purpose-built minimal parser
for Linux .desktop files.

The new parser:
- Only extracts the Exec key from [Desktop Entry] section (all we need)
- Follows the Desktop Entry Specification
- Has comprehensive test coverage (40 tests) including:
  - All major file managers (Nautilus, Dolphin, Thunar, PCManFM, Caja, Nemo)
  - Edge cases (UTF-8, special chars, comments, empty files, etc.)
  - Buffer limit handling

Binary size reduction: 45KB (10.22MB -> 10.18MB)

* perf(v3): remove samber/lo from runtime code, saving ~310KB binary size

Replace samber/lo with Go 1.21+ stdlib slices package and minimal internal
helpers in all runtime code paths. This removes 80 transitive dependencies
from the production binary.

Changes:
- Create internal/sliceutil package with Unique and FindMapKey helpers
- Replace lo.Without with slices.DeleteFunc in event handling
- Replace lo.Ternary with inline if/else in Windows code
- Replace lo.Uniq with sliceutil.Unique for feature flags
- Replace lo.FindKey with sliceutil.FindMapKey for method aliases
- Replace lo.Filter with slices.DeleteFunc in event listeners
- Replace lo.Must with inline panic in w32 package

Binary size: 10.18MB -> 9.87MB (~310KB / 3% reduction)

Note: CLI tools still use samber/lo since they don't affect
production binary size. The application_debug.go file also
retains lo usage as it has //go:build !production tag.

* fix: address CodeRabbit review comments

- Use application/x-typescript MIME type (not IANA-registered text/typescript)
- Fix potential panic in mimetype_stdlib_test.go for short MIME strings
- Use cached isVariadic flag in bindings_optimized_bench_test.go

* fix: initialize goccy/go-json decoder early to fix Windows test failure

On Windows, goccy/go-json's type address calculation can fail if the
decoder is first invoked during test execution rather than at init time.
Force early initialization by unmarshaling a []int during package init.

See: https://github.com/goccy/go-json/issues/474

* 📝 Add docstrings to `v3/performance-improvements` (#4844)

* fix: initialize goccy/go-json decoder early to fix Windows test failure

On Windows, goccy/go-json's type address calculation can fail if the
decoder is first invoked during test execution rather than at init time.
Force early initialization by unmarshaling a []int during package init.

See: https://github.com/goccy/go-json/issues/474

* 📝 Add docstrings to `v3/performance-improvements`

Docstrings generation was requested by @leaanthony.

* https://github.com/wailsapp/wails/pull/4843#issuecomment-3703472562

The following files were modified:

* `v3/internal/assetserver/common.go`
* `v3/internal/assetserver/content_type_sniffer.go`
* `v3/internal/assetserver/mimecache.go`
* `v3/internal/fileexplorer/desktopfile.go`
* `v3/internal/fileexplorer/fileexplorer_linux.go`
* `v3/internal/sliceutil/sliceutil.go`
* `v3/pkg/application/application_ios.go`
* `v3/pkg/application/bindings.go`
* `v3/pkg/application/ios_runtime_ios.go`
* `v3/pkg/w32/window.go`

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-02 07:03:36 +11:00

687 lines
20 KiB
Go

//go:build android && cgo
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 (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"time"
"unsafe"
json "github.com/goccy/go-json"
"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()
// 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")
// 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()
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
}
func handleMessageForAndroid(app *App, message string) string {
// Parse the message
var msg map[string]interface{}
if err := json.Unmarshal([]byte(message), &msg); err != nil {
return fmt.Sprintf(`{"error":"%s"}`, err.Error())
}
// TODO: Route to appropriate handler based on message type
// For now, return success
return `{"success":true}`
}
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")
}