# How Bitchat Android Tracks Peer Availability in the Mesh Network

> Discover how Bitchat Android tracks peer availability in its mesh network using reactive state flows and a foreground service. Learn how it efficiently notifies users about peer status.

- Repository: [permissionlesstech/bitchat-android](https://github.com/permissionlesstech/bitchat-android)
- Tags: internals
- Published: 2026-08-04

---

**Bitchat Android uses a reactive state flow architecture where mesh services update a shared `AppStateStore.peers` list, a foreground service observes distinct peer counts, and a gated `PeerAvailabilityNotifier` decides when to show user notifications.**

Bitchat Android is a permissionless, decentralized chat application that operates without internet connectivity by forming device-to-device mesh networks over Bluetooth Low Energy (BLE) and Wi-Fi Aware. Understanding how peer availability is tracked within this mesh is essential for developers building similar offline-first communication systems. The implementation relies on Kotlin coroutines, `StateFlow` for reactive state management, and carefully designed anti-flapping logic to prevent notification spam.

## Core Architecture: From Mesh Discovery to UI Updates

The peer tracking system in Bitchat Android follows a unidirectional data flow pattern. Mesh transport layers detect peers, push updates to centralized state, a foreground service monitors changes, and a dedicated notifier gates user-visible alerts.

### Step 1: Centralized Peer State in AppStateStore

All peer availability data converges at **`AppStateStore.peers`**, a `MutableStateFlow<List<String>>` that holds the current set of peer identifiers as strings[^1^].

```kotlin
// Located in: app/src/main/java/com/bitchat/android/services/AppStateStore.kt
object AppStateStore {
    val peers: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
    // ... other app-wide state flows
}

```

This singleton object acts as the single source of truth. Any component needing peer information—whether for UI rendering, connection management, or notifications—subscribes to this flow rather than accessing transport layers directly.

### Step 2: Mesh Services Push Peer Updates

The **`UnifiedMeshService`** coordinates multiple transport implementations (`BluetoothMeshService` and `WifiAwareMeshService`). When any transport discovers or loses a peer, it updates `AppStateStore.peers`[^2^].

```kotlin
// Simplified pattern from mesh service implementations
fun onPeerDiscovered(peerId: String) {
    val current = AppStateStore.peers.value.toMutableList()
    if (!current.contains(peerId)) {
        current.add(peerId)
        AppStateStore.peers.value = current
    }
}

```

This abstraction allows the notification system to remain agnostic of underlying transport mechanics—whether peers arrive via BLE advertisements or Wi-Fi Aware service discoveries, the state update path is identical.

### Step 3: Foreground Service Observes Distinct Peer Count

The **`MeshForegroundService`** subscribes to a transformed stream of `AppStateStore.peers` and forwards changes to the notification subsystem[^3^].

```kotlin
// Located in: app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt (lines 129-136)
override fun onCreate() {
    super.onCreate()
    
    serviceScope.launch {
        AppStateStore.peers
            .map { it.distinct().size }           // Count unique peers
            .distinctUntilChanged()                // Only emit on count changes
            .collect { peerCount ->
                peerAvailabilityNotifier.onPeerCountChanged(
                    peerCount = peerCount,
                    isAppInBackground = !ProcessLifecycleOwner.get()
                        .lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
                )
            }
    }
}

```

Key observations about this implementation:
- **`distinct().size`** handles duplicate peer IDs that may appear from multiple transports discovering the same device
- **`distinctUntilChanged()`** eliminates redundant emissions when the count hasn't actually changed
- **Background detection** uses `ProcessLifecycleOwner` to determine app visibility state

## Anti-Flapping Notification Gating

Raw peer count changes would generate excessive notifications as devices move in and out of range. Bitchat Android implements a two-layer gating system in **`PeerAvailabilityNotifier`** and its inner **`PeerAvailabilityTracker`** class[^4^].

### PeerAvailabilityTracker: Core State Machine

The tracker encapsulates all stateful gating logic at lines 87-123 of [`PeerAvailabilityNotifier.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/PeerAvailabilityNotifier.kt):

| Gate | Purpose | Threshold |
|------|---------|-----------|
| **Cooldown gate** | Prevent repeated alerts | `ALERT_COOLDOWN_MS = 5 minutes` |
| **Empty re-arm gate** | Require mesh emptiness before new epoch | `EMPTY_REARM_DELAY_MS = 30 seconds` |

```kotlin
// Simplified structure from PeerAvailabilityTracker.update()
fun update(peerCount: Int, isAppInBackground: Boolean): PeerAvailabilityAction {
    val now = System.currentTimeMillis()
    
    // Check cooldown: has enough time passed since last alert?
    val cooldownElapsed = now - lastAlertTime > ALERT_COOLDOWN_MS
    
    // Check empty re-arm: was mesh empty for sufficient duration?
    val canRearm = peerCount == 0 && (now - emptySince) > EMPTY_REARM_DELAY_MS
    
    return when {
        peerCount > 0 && isAppInBackground && cooldownElapsed -> {
            lastAlertTime = now
            PeerAvailabilityAction.SHOW
        }
        canRearm -> {
            // Reset state for new epoch
            PeerAvailabilityAction.RESET
        }
        else -> PeerAvailabilityAction.NONE
    }
}

```

### Notification Aggregation Window

When the tracker returns **`SHOW`**, the notifier starts a 10-second aggregation window before actually posting the notification[^5^]:

```kotlin
// Located in: PeerAvailabilityNotifier.kt (lines 14-26)
private fun scheduleNotification() {
    if (pendingNotificationJob?.isActive == true) return
    
    pendingNotificationJob = scope.launch {
        delay(AGGREGATION_WINDOW_MS)  // 10 seconds
        
        // Re-verify conditions at window completion
        if (latestPeerCount > 0 && isAppCurrentlyInBackground()) {
            showNotification(latestPeerCount)
        }
    }
}

```

This aggregation serves two purposes:
1. **Coalescing rapid changes** — multiple peers arriving within 10 seconds produce one notification
2. **Final condition verification** — the mesh state is re-checked before display, preventing stale alerts

### Clearance on Mesh Empty State

When peer count drops to zero, **`PeerAvailabilityNotifier.clear()`** cancels pending jobs and removes existing notifications[^6^]:

```kotlin
// Located in: PeerAvailabilityNotifier.kt (lines 8-12)
fun clear() {
    pendingNotificationJob?.cancel()
    pendingNotificationJob = null
    notificationManager.cancel(PEER_AVAILABILITY_NOTIFICATION_ID)
}

```

## Complete Data Flow Diagram

```

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  BLE/Wi-Fi      │────▶│  AppStateStore   │────▶│  MeshForeground │
│  Mesh Services  │     │  .peers (StateFlow)    │  Service        │
└─────────────────┘     └──────────────────┘     └────────┬────────┘
                                                          │
                                    ┌─────────────────────┘
                                    ▼
                          ┌───────────────────┐
                          │ PeerAvailability  │
                          │ Notifier          │
                          │ ┌───────────────┐ │
                          │ │  Tracker      │ │
                          │ │  (gates)      │ │
                          │ └───────────────┘ │
                          │ ┌───────────────┐ │
                          │ │  Aggregation  │ │
                          │ │  (10s window) │ │
                          │ └───────────────┘ │
                          └─────────┬─────────┘
                                    ▼
                          ┌───────────────────┐
                          │  System Notification │
                          │  "bitchatters nearby" │
                          └───────────────────┘

```

## Configuration Constants

The anti-flapping behavior is controlled by three constants in [`PeerAvailabilityNotifier.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/PeerAvailabilityNotifier.kt)[^7^]:

```kotlin
companion object {
    const val AGGREGATION_WINDOW_MS = 10_000L      // 10 seconds
    const val ALERT_COOLDOWN_MS = 300_000L          // 5 minutes
    const val EMPTY_REARM_DELAY_MS = 30_000L        // 30 seconds
}

```

These values represent trade-offs between user awareness and notification fatigue. The 5-minute cooldown ensures users aren't repeatedly alerted to the same persistent mesh, while the 30-second empty requirement prevents flapping when a single peer momentarily drops and rejoins.

## Summary

- **`AppStateStore.peers`** provides centralized, reactive state for all peer identifiers in the mesh
- **`MeshForegroundService`** transforms peer list changes into distinct count emissions and forwards them with background state context
- **`PeerAvailabilityTracker`** applies cooldown and re-arm gates to prevent notification spam
- **10-second aggregation window** coalesces rapid peer changes and verifies final conditions before display
- **Automatic clearance** removes notifications when the mesh becomes empty, with state machine reset for future epochs

## Frequently Asked Questions

### How does Bitchat Android handle duplicate peer discoveries from multiple transports?

The `distinct()` operation on `AppStateStore.peers` ensures each unique peer ID is counted only once, regardless of whether it was discovered via BLE, Wi-Fi Aware, or both transports simultaneously. This deduplication happens in the flow transformation within `MeshForegroundService.onCreate` before any notification logic processes the count.

### What triggers the "bitchatters nearby" notification to actually display?

Four conditions must all be satisfied: the peer count must be greater than zero, the app must be in the background, the 5-minute cooldown since the last alert must have elapsed, and the 10-second aggregation window must complete with the count still positive. The empty re-arm gate additionally requires the mesh to have been completely empty for 30 seconds before a new notification epoch can begin.

### Where are the notification timing constants defined in the source code?

All three timing constants—`AGGREGATION_WINDOW_MS` (10 seconds), `ALERT_COOLDOWN_MS` (5 minutes), and `EMPTY_REARM_DELAY_MS` (30 seconds)—are defined as `const val` properties in the companion object of [`PeerAvailabilityNotifier.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/PeerAvailabilityNotifier.kt). These can be modified at compile time to adjust the trade-off between user awareness and notification frequency.

### How does the system know when the app is in the background?

`MeshForegroundService` uses `ProcessLifecycleOwner.get().lifecycle.currentState` from the Android Architecture Components library. It checks `isAtLeast(Lifecycle.State.STARTED)`—when this returns false, the app is considered to be in the background and notifications are eligible for display.