379 lines
9.8 KiB
Markdown
379 lines
9.8 KiB
Markdown
|
|
# 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)
|