When the main Android app's WebSocket connection is down, messages are
now delivered via Android broadcast intent (am broadcast) so the app can
save them to DB and show notifications. WebSocket sessions now carry a
client_type parameter ("main" for the Android app) to distinguish
between different client types for future Google Assistant replacement
clients. Heartbeat always targets the last "main" session.
- Add LastMainChannel to state for heartbeat targeting
- Add clientTypes map to WebSocketChannel (retained after disconnect)
- Add broadcast fallback in WebSocket Send for "main" clients
- Add pkg/broadcast package using am broadcast IPC
- Add AgentMessageReceiver + NotificationHelper on Android side
- Request POST_NOTIFICATIONS runtime permission on Android 13+
- Share state.Manager between AgentLoop and HeartbeatService
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
51 lines
1.2 KiB
Go
51 lines
1.2 KiB
Go
package broadcast
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
)
|
|
|
|
const (
|
|
// Action is the intent action the Android app listens for.
|
|
Action = "io.picoclaw.android.AGENT_MESSAGE"
|
|
// Package is the Android app package name.
|
|
Package = "io.picoclaw.android"
|
|
)
|
|
|
|
// Message represents a message to send via Android broadcast.
|
|
type Message struct {
|
|
Content string `json:"content"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
// Send sends a message to the Android app via am broadcast.
|
|
// This works because the Go server runs inside Termux on the same device.
|
|
func Send(msg Message) error {
|
|
data, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal broadcast message: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command("am", "broadcast",
|
|
"-a", Action,
|
|
"-p", Package,
|
|
"--es", "message", string(data),
|
|
)
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
logger.ErrorCF("broadcast", "am broadcast failed", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"output": string(output),
|
|
})
|
|
return fmt.Errorf("am broadcast failed: %w (%s)", err, string(output))
|
|
}
|
|
|
|
logger.InfoCF("broadcast", "Broadcast sent", map[string]interface{}{
|
|
"content_len": len(msg.Content),
|
|
})
|
|
return nil
|
|
}
|