wails/v3/pkg/application/application_android_nocgo.go
Lea Anthony 9a363d7be5
feat(v3): add server mode for headless HTTP deployment (#4903)
* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Fix corrupted test file with embedded terminal output
- Fix module name mismatch in gin-routing (was gin-example)
- Fix replace directive version mismatch in gin-service
- Fix placeholder module name in ios example (was changeme)
- Fix Dockerfile COPY path to work from both build contexts
- Fix bare URL in README (MD034 compliance)
- Fix comment accuracy in getScreens (returns error, not empty slice)
- Remove deprecated docker-compose version field
- Add port documentation in Taskfile template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Add note about healthcheck wget not being available in distroless images
- Add !server build constraint to menu_windows.go and menu_darwin.go
- Downgrade window-visibility-test go.mod from 1.25 to 1.24 to match CI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:33:44 +11:00

225 lines
5.1 KiB
Go

//go:build android && !cgo && !server
package application
import (
"fmt"
"sync"
"unsafe"
"encoding/json"
)
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()
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)
}
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 is a stub for non-cgo builds
func executeJavaScript(js string) {
androidLogf("warn", "executeJavaScript called but cgo is not enabled")
}