Initial commit: Alfred Mobile - AI Assistant Android App
- OAuth authentication via Authentik - WebSocket connection to OpenClaw gateway - Configurable gateway URL with first-run setup - User preferences sync across devices - Multi-user support with custom assistant names - ElevenLabs TTS integration (local + remote) - FCM push notifications for alarms - Voice input via Google Speech API - No hardcoded secrets or internal IPs in tracked files
This commit is contained in:
378
RECONNECTION.md
Normal file
378
RECONNECTION.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# Auto-Reconnection Feature
|
||||
|
||||
**Alfred mobile app now automatically reconnects when the connection is lost!**
|
||||
|
||||
## Overview
|
||||
|
||||
The app now includes intelligent auto-reconnection with exponential backoff to handle network issues gracefully. When the WebSocket connection drops, the app will automatically attempt to reconnect without requiring user intervention.
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Automatic Reconnection
|
||||
- **Triggered on any connection failure:**
|
||||
- Network interruptions
|
||||
- Server disconnections
|
||||
- "Software caused connection abort" errors
|
||||
- WiFi/cellular switching
|
||||
- Proxy restarts
|
||||
|
||||
### 📈 Exponential Backoff
|
||||
- **Smart retry timing:**
|
||||
- Attempt 1: 1 second delay
|
||||
- Attempt 2: 2 seconds delay
|
||||
- Attempt 3: 4 seconds delay
|
||||
- Attempt 4: 8 seconds delay
|
||||
- Attempt 5: 16 seconds delay
|
||||
- Attempts 6+: 30 seconds delay (max)
|
||||
|
||||
### 🔢 Max Retry Limit
|
||||
- **10 reconnection attempts** before giving up
|
||||
- Prevents infinite retry loops
|
||||
- Battery-friendly
|
||||
|
||||
### 📊 UI Feedback
|
||||
- **Connection status shows:**
|
||||
- "Connecting..." - Initial connection
|
||||
- "Connected ✅" - Successfully connected
|
||||
- "Disconnected" - Connection lost
|
||||
- "Reconnecting... (attempt X, Ys)" - Auto-reconnecting
|
||||
- "Error: Connection lost - max retries exceeded" - Gave up after 10 attempts
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Connection Loss Detected
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ shouldReconnect? │
|
||||
│ (enabled) │
|
||||
└────────┬───────────┘
|
||||
│ yes
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Check attempt # │
|
||||
│ < 10 attempts? │
|
||||
└────────┬───────────┘
|
||||
│ yes
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Calculate delay │
|
||||
│ (exponential) │
|
||||
└────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Schedule retry │
|
||||
│ (Handler) │
|
||||
└────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Wait delay... │
|
||||
└────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Attempt connect │
|
||||
└────────┬───────────┘
|
||||
│
|
||||
├─ Success → Reset counter, connected ✅
|
||||
│
|
||||
└─ Failure → Loop back to retry
|
||||
```
|
||||
|
||||
## Code Details
|
||||
|
||||
### GatewayClient.kt Changes
|
||||
|
||||
**New State Variables:**
|
||||
```kotlin
|
||||
private var shouldReconnect = true
|
||||
private var reconnectAttempts = 0
|
||||
private val maxReconnectAttempts = 10
|
||||
private val baseReconnectDelayMs = 1000L
|
||||
private val maxReconnectDelayMs = 30000L
|
||||
private var reconnectHandler: Handler? = null
|
||||
```
|
||||
|
||||
**Reconnection Logic:**
|
||||
```kotlin
|
||||
private fun scheduleReconnect() {
|
||||
if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
Log.e(TAG, "Max reconnection attempts reached")
|
||||
listener.onError("Connection lost - max retries exceeded")
|
||||
shouldReconnect = false
|
||||
return
|
||||
}
|
||||
|
||||
val delay = minOf(
|
||||
baseReconnectDelayMs * (1 shl reconnectAttempts),
|
||||
maxReconnectDelayMs
|
||||
)
|
||||
|
||||
reconnectAttempts++
|
||||
listener.onReconnecting(reconnectAttempts, delay)
|
||||
|
||||
reconnectHandler?.postDelayed({
|
||||
if (shouldReconnect && !isConnected) {
|
||||
connect()
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
```
|
||||
|
||||
**Reset on Success:**
|
||||
```kotlin
|
||||
private fun resetReconnectionState() {
|
||||
reconnectAttempts = 0
|
||||
reconnectHandler?.removeCallbacksAndMessages(null)
|
||||
reconnectHandler = null
|
||||
}
|
||||
```
|
||||
|
||||
### GatewayListener Interface
|
||||
|
||||
**New Callback:**
|
||||
```kotlin
|
||||
interface GatewayListener {
|
||||
// ... existing callbacks ...
|
||||
fun onReconnecting(attempt: Int, delayMs: Long)
|
||||
}
|
||||
```
|
||||
|
||||
### MainScreen.kt Implementation
|
||||
|
||||
```kotlin
|
||||
override fun onReconnecting(attempt: Int, delayMs: Long) {
|
||||
val delaySec = delayMs / 1000
|
||||
connectionStatus = "Reconnecting... (attempt $attempt, ${delaySec}s)"
|
||||
Log.d("MainScreen", "Reconnecting: attempt $attempt, delay ${delayMs}ms")
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
**1. Restart OAuth Proxy**
|
||||
```bash
|
||||
# Kill and restart the proxy
|
||||
pkill -f "node server.js"
|
||||
cd ~/.openclaw/workspace/alfred-proxy
|
||||
node server.js > /tmp/alfred-proxy.log 2>&1 &
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
- App shows "Disconnected"
|
||||
- After ~1 second: "Reconnecting... (attempt 1, 1s)"
|
||||
- After ~3 seconds: "Connected ✅"
|
||||
|
||||
**2. Network Switch (WiFi ↔ Cellular)**
|
||||
- Turn off WiFi on mobile device
|
||||
- App automatically reconnects via cellular
|
||||
- Or vice versa
|
||||
|
||||
**3. Temporary Network Loss**
|
||||
- Enable airplane mode for 5 seconds
|
||||
- Disable airplane mode
|
||||
- App reconnects automatically
|
||||
|
||||
**4. Extended Network Outage**
|
||||
- Enable airplane mode for 5 minutes
|
||||
- App will attempt 10 reconnections over ~2 minutes
|
||||
- Shows "Connection lost - max retries exceeded"
|
||||
- Disable airplane mode
|
||||
- **Restart app** to reconnect
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Connect the app**
|
||||
- Status should show "Connected ✅"
|
||||
|
||||
2. **Kill the proxy**
|
||||
```bash
|
||||
pkill -f "node server.js"
|
||||
```
|
||||
- Watch the status bar for reconnection attempts
|
||||
|
||||
3. **Restart the proxy**
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/alfred-proxy
|
||||
node server.js > /tmp/alfred-proxy.log 2>&1 &
|
||||
```
|
||||
- App should reconnect within a few seconds
|
||||
|
||||
4. **Check logs**
|
||||
```bash
|
||||
adb logcat GatewayClient:D MainScreen:D *:S
|
||||
```
|
||||
- Should see reconnection attempts and backoff delays
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### When Reconnection is Disabled
|
||||
|
||||
**User-initiated disconnect:**
|
||||
- Logging out
|
||||
- App closure
|
||||
- Manual disconnect
|
||||
|
||||
The `shouldReconnect` flag is set to `false` in these cases to prevent unwanted reconnection attempts.
|
||||
|
||||
### Max Retries Reached
|
||||
|
||||
After 10 failed attempts:
|
||||
- `shouldReconnect` is set to `false`
|
||||
- Error message displayed
|
||||
- User must restart the app to reconnect
|
||||
|
||||
**Why 10 attempts?**
|
||||
- Total time: ~2 minutes of reconnection attempts
|
||||
- Balance between persistence and battery life
|
||||
- Prevents infinite loops
|
||||
|
||||
### State Cleanup
|
||||
|
||||
**On successful connection:**
|
||||
- `reconnectAttempts` reset to 0
|
||||
- Reconnection handler cleared
|
||||
- Fresh state for next potential disconnection
|
||||
|
||||
**On manual disconnect:**
|
||||
- `shouldReconnect` set to false
|
||||
- All handlers cancelled
|
||||
- Clean shutdown
|
||||
|
||||
## Benefits
|
||||
|
||||
### User Experience
|
||||
- ✅ Seamless reconnection after temporary network issues
|
||||
- ✅ No manual intervention required
|
||||
- ✅ Clear status feedback
|
||||
- ✅ Works across network type changes (WiFi ↔ Cellular)
|
||||
|
||||
### Battery Efficiency
|
||||
- ✅ Exponential backoff reduces connection attempts over time
|
||||
- ✅ Max retry limit prevents infinite loops
|
||||
- ✅ Handler cleanup prevents memory leaks
|
||||
|
||||
### Reliability
|
||||
- ✅ Handles "Software caused connection abort" errors
|
||||
- ✅ Resilient to proxy/server restarts
|
||||
- ✅ Graceful degradation (max retries)
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Logs to Watch
|
||||
|
||||
**GatewayClient:**
|
||||
```
|
||||
D/GatewayClient: WebSocket failure
|
||||
D/GatewayClient: Scheduling reconnect attempt 1 in 1000ms
|
||||
D/GatewayClient: Attempting reconnection (attempt 1)
|
||||
D/GatewayClient: Connect successful!
|
||||
```
|
||||
|
||||
**MainScreen:**
|
||||
```
|
||||
D/MainScreen: Reconnecting: attempt 1, delay 1000ms
|
||||
D/MainScreen: Reconnecting: attempt 2, delay 2000ms
|
||||
```
|
||||
|
||||
### Status Bar Messages
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| Connecting... | Initial connection in progress |
|
||||
| Connected ✅ | Successfully connected |
|
||||
| Disconnected | Connection lost |
|
||||
| Reconnecting... (attempt 1, 1s) | First reconnection attempt |
|
||||
| Reconnecting... (attempt 5, 16s) | Fifth attempt with backoff |
|
||||
| Error: Connection lost - max retries exceeded | Gave up after 10 attempts |
|
||||
|
||||
## Configuration
|
||||
|
||||
### Adjustable Parameters
|
||||
|
||||
Edit `GatewayClient.kt` to customize:
|
||||
|
||||
```kotlin
|
||||
// Maximum reconnection attempts
|
||||
private val maxReconnectAttempts = 10 // Change to 5, 20, etc.
|
||||
|
||||
// Initial retry delay
|
||||
private val baseReconnectDelayMs = 1000L // Change to 2000L (2s), etc.
|
||||
|
||||
// Maximum retry delay
|
||||
private val maxReconnectDelayMs = 30000L // Change to 60000L (60s), etc.
|
||||
```
|
||||
|
||||
### Exponential Backoff Formula
|
||||
|
||||
```kotlin
|
||||
delay = min(
|
||||
baseReconnectDelayMs * (2 ^ reconnectAttempts),
|
||||
maxReconnectDelayMs
|
||||
)
|
||||
```
|
||||
|
||||
**Example with current settings:**
|
||||
- Attempt 1: min(1000 * 2^0, 30000) = 1000ms
|
||||
- Attempt 2: min(1000 * 2^1, 30000) = 2000ms
|
||||
- Attempt 3: min(1000 * 2^2, 30000) = 4000ms
|
||||
- Attempt 4: min(1000 * 2^3, 30000) = 8000ms
|
||||
- Attempt 5: min(1000 * 2^4, 30000) = 16000ms
|
||||
- Attempt 6: min(1000 * 2^5, 30000) = 30000ms (capped)
|
||||
- Attempts 7-10: 30000ms (capped)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### App not reconnecting
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
adb logcat | grep -E "(GatewayClient|MainScreen)"
|
||||
```
|
||||
|
||||
**Possible causes:**
|
||||
- Max retries exceeded (restart app)
|
||||
- `shouldReconnect` set to false
|
||||
- Handler not scheduled
|
||||
|
||||
### Reconnecting too slowly
|
||||
|
||||
**Reduce backoff delays:**
|
||||
```kotlin
|
||||
private val baseReconnectDelayMs = 500L // 500ms instead of 1s
|
||||
private val maxReconnectDelayMs = 10000L // 10s instead of 30s
|
||||
```
|
||||
|
||||
### Too many reconnection attempts
|
||||
|
||||
**Reduce max attempts:**
|
||||
```kotlin
|
||||
private val maxReconnectAttempts = 5 // 5 instead of 10
|
||||
```
|
||||
|
||||
### Memory leaks
|
||||
|
||||
**Ensure cleanup:**
|
||||
- Check `disconnect()` calls `resetReconnectionState()`
|
||||
- Verify `reconnectHandler?.removeCallbacksAndMessages(null)`
|
||||
- Look for uncancelled handlers in logs
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
- [ ] **Jitter** - Add randomness to backoff to prevent thundering herd
|
||||
- [ ] **Network detection** - Skip retries when network is unavailable
|
||||
- [ ] **Manual retry button** - User can trigger reconnection after max retries
|
||||
- [ ] **Smarter reset** - Reset counter after stable connection (e.g., 5 minutes)
|
||||
- [ ] **Configurable via settings** - User-adjustable retry behavior
|
||||
- [ ] **Background reconnection** - Continue attempting when app is backgrounded
|
||||
|
||||
## Version
|
||||
|
||||
1.0.0 - Initial auto-reconnection implementation (February 2026)
|
||||
Reference in New Issue
Block a user