How Bitchat Android Maintains Persistent Background Connectivity
Bitchat Android maintains persistent background connectivity through a layered architecture that combines a unified orchestration service with transport-specific implementations for Bluetooth Low-Energy and Wi-Fi Aware, backed by power-profile-driven maintenance loops and keep-alive messaging.
The permissionlesstech/bitchat-android repository implements a mesh-wide, always-on connection that remains active even when the user interface is closed. This is achieved by coordinating three tightly-integrated layers: transport selection, transport services, and a shared mesh core that provides common utilities like gossip synchronization and peer management.
Transport Selection and Unified Orchestration
The UnifiedMeshService class serves as the central façade that the UI and higher-level code interact with. Located in app/src/main/java/com/bitchat/android/mesh/UnifiedMeshService.kt, this service decides at runtime whether to use BLE or Wi-Fi Aware and forwards all traffic to whichever transport is active for a given peer.
When startServices() is invoked, the service initializes both transport layers conditionally:
if (isBleEnabled()) {
bluetooth.startServices()
} else {
bluetooth.setBleTransportEnabled(false)
}
WifiAwareController.startIfPossible()
The method also launches a periodic announcement scheduler that keeps the mesh discoverable. This scheduler reads the current power profile and broadcasts mesh announcements at the configured interval whenever direct peers exist:
serviceScope.launch {
powerManager.profile
.map { it.meshAnnouncementIntervalMs to it.hasDirectPeers }
.collectLatest { (intervalMs, hasRecipients) ->
while (isActive) {
delay(intervalMs)
if (powerManager.profile.value.hasDirectPeers) sendBroadcastAnnounce()
}
}
}
All outgoing methods—sendMessage, sendPrivateMessage, sendFileBroadcast, and others—route traffic based on peer-specific readiness checks (isBleReady, isWifiReady) defined in the target peer's metadata.
BLE Transport: Foreground-Style Background Service
The BLE implementation in BluetoothMeshService.kt operates as a persistent background service that survives UI detachment. It registers with the TransportBridgeService to enable cross-transport packet forwarding:
TransportBridgeService.register("BLE", this)
The service maintains a long-living coroutine scope for I/O operations:
CoroutineScope(Dispatchers.IO + SupervisorJob())
This scope processes inbound voice frames, background direct message notifications, and periodic debug logging without requiring an active activity.
When a private message arrives while delegate is null (indicating the UI is backgrounded or destroyed), the service posts a system notification:
if (delegate == null && message.isPrivate && message.sender != "system") {
serviceNotificationManager.setAppBackgroundState(true)
serviceNotificationManager.showPrivateMessageNotification(...)
}
Peer lifecycle management includes a grace period mechanism to handle transient disconnects. The constant PEER_DISCONNECT_GRACE_MS defines how long to wait before removing a peer's Noise session:
delay(PEER_DISCONNECT_GRACE_MS)
if (!linkBack && !seenAfterDisconnect) {
peerManager.removePeer(peerID)
}
This prevents stale sessions from accumulating while tolerating brief connectivity fluctuations common in mobile environments.
Wi-Fi Aware Transport: Resilient Peer-to-Peer Links
The Wi-Fi Aware implementation in WifiAwareMeshService.kt creates publish and subscribe discovery sessions that autonomously maintain connectivity through several resilience mechanisms.
Periodic Connection Maintenance
The startPeriodicConnectionMaintenance() method runs a background loop that:
- Prunes stale discovery entries
- Detects peers with no active socket and attempts reconnection
- Refreshes discovery sessions after extended idle periods
Server-Ready Handshake
When a peer requests role reversal or a client connection, the service opens a TCP server socket, advertises the port via a NetworkSpecifier, and accepts connections bound to the Wi-Fi Aware network:
// Server socket creation and network binding
val serverSocket = ServerSocket(0) // System-assigned port
val networkSpecifier = WifiAwareNetworkSpecifier.Builder(session, peerHandle)
.setPort(serverSocket.localPort)
.build()
// Connection tracked for keep-alive management
connectionTracker.addConnection(socket, peerId)
Client Connection with Retry Logic
The client side handles connection establishment with exponential back-off through connectAwareClientSocket. If repeated attempts fail, it requests a role reversal to switch which peer acts as server.
Keep-Alive Messaging
Both sides send periodic empty payloads to prevent underlying data path expiration:
sendSubscribePing()on the subscriber sidesendServerReadyPayload()on the publisher side
These messages maintain the WifiAwareNetworkInfo association without consuming significant bandwidth.
Power Management Integration
Background connectivity behavior is governed by a PowerManager singleton that supplies dynamic power profiles. These profiles define:
| Parameter | Purpose |
|---|---|
meshAnnouncementIntervalMs |
Cadence for mesh-wide broadcast announcements |
wifiAware.discoverySessionRefreshMinMs |
Minimum lifetime before Wi-Fi Aware discovery refresh |
wifiAware.connectionMaintenanceMs |
Interval for connection maintenance loop execution |
All background coroutines consume PowerManager.getInstance(context).profile as a StateFlow, enabling runtime adjustment of battery consumption characteristics without service restart.
End-to-End Background Message Flow
Understanding how a message is processed while the app is backgrounded demonstrates the integration of these components:
- Packet reception — BLE or Wi-Fi Aware receives a
BitchatPacketthrough the respective transport's socket handler - Packet processing —
packetProcessorinMeshCore.ktreassembles fragments if needed - Message handling —
MessageHandlervalidates structure and routes by type - Security validation —
SecurityManagerverifies signatures and decrypts payloads - Admission control —
IncomingMessageAdmission.admitToAppState(message)deduplicates and filters - Dispatch decision — If
delegate(UI) is attached, deliver to UI; otherwise triggerserviceNotificationManagerfor system notification - Background persistence — Announcement scheduler and connection maintenance loops continue executing, keeping the mesh alive for subsequent messages
Practical Implementation Examples
Starting the Mesh from Application Code
val unifiedMesh = UnifiedMeshService(
applicationContext,
BluetoothMeshService(applicationContext)
)
unifiedMesh.startServices() // Launches BLE + Wi-Fi Aware, schedules announcements
Sending a Background Private Message
unifiedMesh.sendPrivateMessage(
content = "Hey, are you there?",
recipientPeerID = peerId,
recipientNickname = "Alice"
)
// Notification fires automatically if UI is detached
Clean Shutdown
unifiedMesh.stopServices() // Stops BLE, Wi-Fi Aware, cancels scopes, clears peers
Debugging Peer Connectivity
val peers = unifiedMesh.getPeerNicknames() // Map<PeerID, Nickname>
Log.d("MeshDebug", "Connected peers: $peers")
Key Source Files
| File | Responsibility |
|---|---|
UnifiedMeshService.kt |
Transport orchestration, announcement scheduling, unified API |
BluetoothMeshService.kt |
BLE transport, background notifications, peer lifecycle |
WifiAwareMeshService.kt |
Wi-Fi Aware discovery, reconnection, keep-alive |
PowerManager.kt |
Dynamic power profiles for background interval tuning |
MeshCore.kt |
Shared fragment management, gossip sync, peer lookup |
TransportBridgeService.kt |
Cross-transport packet forwarding without loops |
MeshService.kt |
Public interface contract for UI and services |
Summary
- UnifiedMeshService coordinates BLE and Wi-Fi Aware through a single API, automatically routing to the appropriate transport per peer.
- BluetoothMeshService runs as a persistent background coroutine scope with foreground-style notification handling for incoming private messages.
- WifiAwareMeshService implements resilient peer-to-peer links through publish/subscribe discovery, server-ready handshakes, retry logic with role reversal, and keep-alive messaging.
- PowerManager profiles drive all background intervals, enabling runtime battery optimization without service restarts.
- Grace periods and stale session pruning prevent resource leaks while tolerating mobile connectivity volatility.
Frequently Asked Questions
How does Bitchat Android keep the mesh alive when the app is closed?
The combination of UnifiedMeshService.startAnnouncementScheduler() and WifiAwareMeshService.startPeriodicConnectionMaintenance() creates coroutine-based loops that outlive the UI by operating within the application process scope rather than activity scope. These loops use Dispatchers.IO with SupervisorJob to survive configuration changes and activity destruction, tied to power-profile intervals that refresh discovery sessions and emit keep-alive announcements.
What triggers a background notification for new messages?
In BluetoothMeshService.kt, the condition delegate == null && message.isPrivate triggers serviceNotificationManager.showPrivateMessageNotification(). The delegate field holds a reference to the active UI component; when null, the app is either backgrounded or destroyed. The same pattern exists in Wi-Fi Aware message handling through WifiAwareMeshService.handleMessageReceived.
How is battery consumption optimized for background operation?
All background loops consume intervals from PowerManager.getInstance(context).profile, which exposes meshAnnouncementIntervalMs and related timings as reactive state. Users can adjust these through debug UI, and the changes propagate immediately to running coroutines via StateFlow collection. This design allows aggressive throttling or near-real-time responsiveness without code changes or service restarts.
What prevents message loops when both BLE and Wi-Fi Aware are active?
TransportBridgeService acts as a deduplication layer that assigns each transport a unique identifier ("BLE", "WIFI_AWARE"). When one transport receives a packet, the bridge checks whether that packet was already seen from the other transport before forwarding. This prevents the same message from bouncing between radio interfaces and flooding the mesh, while still allowing either transport to serve as fallback for the other.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →