wails/v3/pkg/application/application_android_nocgo.go
Lea Anthony 4d8ec29feb feat: Add Android support for Wails v3
This commit adds comprehensive Android support for Wails v3, enabling
Go applications to run as native Android apps with WebView-based UI.

Key features:
- Android-specific application implementation with JNI bridge
- WebView integration via WebViewAssetLoader for serving assets
- JavaScript runtime injection and execution via JNI callbacks
- Binding call support with async result callbacks
- Event system support for Android platform
- Full example Android app with Gradle build system

Technical details:
- Uses CGO with Android NDK for cross-compilation
- Implements JNI callbacks for Go <-> Java communication
- Supports both ARM64 and x86_64 architectures
- WebView debugging support via Chrome DevTools Protocol
- Handles empty response body case in binding calls to prevent panic

Files added:
- v3/pkg/application/*_android.go - Android platform implementations
- v3/pkg/events/events_android.go - Android event definitions
- v3/internal/*/\*_android.go - Android-specific internal packages
- v3/examples/android/ - Complete example Android application
- v3/ANDROID_ARCHITECTURE.md - Architecture documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:06:59 +11:00

224 lines
5.1 KiB
Go

//go:build android && !cgo
package application
import (
"encoding/json"
"fmt"
"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()
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")
}