Coverage Summary for Class: DeviceStatusManager (release.com.greybox.projectmesh)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| DeviceStatusManager$startPeriodicStatusChecks$1 |
0%
(0/1)
|
|
| DeviceStatusManager$updateConversations$1 |
0%
(0/1)
|
0%
(0/7)
|
0%
(0/9)
|
0%
(0/100)
|
| DeviceStatusManager$verifyDeviceStatus$1 |
0%
(0/1)
|
|
| DeviceStatusManager$verifyDeviceStatus$1$isReachable$1 |
0%
(0/1)
|
0%
(0/1)
|
0%
(0/2)
|
0%
(0/16)
|
| Total |
0%
(0/16)
|
0%
(0/8)
|
0%
(0/11)
|
0%
(0/116)
|
package com.greybox.projectmesh
import android.util.Log
import com.greybox.projectmesh.server.AppServer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import java.net.InetAddress
/**
* Centralized manager for tracking online/offline status of devices.
* This singleton provides a single source of truth that can be observed by different parts of the app.
*/
object DeviceStatusManager {
//private mutable state flow that stores device IP address to online status mapping
private val _deviceStatusMap = MutableStateFlow<Map<String, Boolean>>(emptyMap())
//public read only state flow for detection
val deviceStatusMap: StateFlow<Map<String, Boolean>> = _deviceStatusMap.asStateFlow()
// min time between status checks
private const val MIN_STATUS_CHECK_INTERVAL = 5000L
//map to track when a device was last checked
private val lastCheckedTimes = mutableMapOf<String, Long>()
//coroutine scope for background operations
private val scope = CoroutineScope(Dispatchers.IO + Job())
//reference to AppServer - will be set during initialization
private var appServer: AppServer? = null
//track consecutive failures
private val failureCountMap = mutableMapOf<String, Int>()
//special test device addresses that should be handled differently
private val specialDevices = setOf(
"192.168.0.99", // Online test device
"192.168.0.98" // Offline test device
)
//initialize the device status manager with necessary dependencies
fun initialize(server: AppServer) {
appServer = server
// Start periodic background checks
startPeriodicStatusChecks()
}
fun updateDeviceStatus(ipAddress: String, isOnline: Boolean, verified: Boolean = false) {
//if this is a special device, handle according to its predefined status
if (ipAddress == "192.168.0.99") { // Online test device
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = true
mutable
}
Log.d("DeviceStatusManager", "Updated test device status for $ipAddress: online")
return
} else if (ipAddress == "192.168.0.98") { // Offline test device
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = false
mutable
}
Log.d("DeviceStatusManager", "Updated test device status for $ipAddress: offline")
return
}
//for normal devices, if the update is verified (from a trusted component), update immediately
if (verified) {
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = isOnline
Log.d("DeviceStatusManager", "Updated verified status for $ipAddress: ${if (isOnline) "online" else "offline"}")
mutable
}
//also update the last checked time
lastCheckedTimes[ipAddress] = System.currentTimeMillis()
} else {
//if not verified, only change offline->online (we'll verify before changing online->offline)
if (isOnline && (!_deviceStatusMap.value.containsKey(ipAddress) || _deviceStatusMap.value[ipAddress] == false)) {
// Only update if we're going from offline/unknown to online
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = true
Log.d("DeviceStatusManager", "Updated status for $ipAddress: online (unverified)")
mutable
}
//schedule a verification check to confirm it's really online
verifyDeviceStatus(ipAddress)
} else if (!isOnline) {
//for marking devices offline, we need verification
verifyDeviceStatus(ipAddress)
}
}
}
//check if a device is currently online
fun isDeviceOnline(ipAddress: String): Boolean {
// Check if it's time to verify this device again
val lastChecked = lastCheckedTimes[ipAddress] ?: 0L
val now = System.currentTimeMillis()
if (now - lastChecked > MIN_STATUS_CHECK_INTERVAL &&
_deviceStatusMap.value[ipAddress] == true) {
// If it's been a while since we checked and device is marked online, verify
verifyDeviceStatus(ipAddress)
}
return _deviceStatusMap.value[ipAddress] ?: false
}
//verify if a device is actually online by attempting to connect
fun verifyDeviceStatus(ipAddress: String) {
// Skip verification for special test devices
if (ipAddress in specialDevices) {
return
}
// Check if we've verified recently
val lastChecked = lastCheckedTimes[ipAddress] ?: 0L
val now = System.currentTimeMillis()
if (now - lastChecked < MIN_STATUS_CHECK_INTERVAL) {
// Checked too recently, skip
return
}
scope.launch {
// Add a log to track verification attempts
Log.d("DeviceStatusManager", "Verifying device status for $ipAddress")
lastCheckedTimes[ipAddress] = System.currentTimeMillis()
try {
// IMPORTANT: Check if there are any recent messages from this device
// This information is maintained in NetworkScreenViewModel
// We can check if the IP is in the current known nodes list
// Instead of directly accessing originatorMessages, we'll use the current
// deviceStatusMap to see if the device was marked as online by any component
val isCurrentlyOnline = _deviceStatusMap.value[ipAddress] ?: false
// If the device was marked as online by any component and we're doing
// a verification check, give it more attempts before marking offline
val attemptsNeeded = if (isCurrentlyOnline) 3 else 1
val addr = InetAddress.getByName(ipAddress)
val isReachable = withTimeoutOrNull(3000) {
addr.isReachable(2000)
} ?: false
if (isReachable) {
// Reset failure count if reachable
failureCountMap.remove(ipAddress)
// Update status to online
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = true
Log.d("DeviceStatusManager", "Device $ipAddress is reachable, marking online")
mutable
}
} else {
// Not reachable - increment failure count
val failures = (failureCountMap[ipAddress] ?: 0) + 1
failureCountMap[ipAddress] = failures
// Only mark as offline after multiple consecutive failures
if (failures >= attemptsNeeded) {
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = false
Log.d("DeviceStatusManager",
"Device $ipAddress is unreachable after $failures attempts, marking offline")
mutable
}
} else {
Log.d("DeviceStatusManager",
"Device $ipAddress is unreachable (attempt $failures/$attemptsNeeded), still considered online")
}
}
// Then try the app-level check if the device is believed to be online
if (isReachable || (isCurrentlyOnline && failureCountMap[ipAddress] ?: 0 < attemptsNeeded)) {
appServer?.let { server ->
// Only do this check if we have the AppServer instance
try {
// Try a quick check to app server endpoints
val checkResult = server.checkDeviceReachable(addr)
if (checkResult) {
// Reset failure count on successful app-level check
failureCountMap.remove(ipAddress)
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = true
Log.d("DeviceStatusManager",
"App-level check for $ipAddress successful, marking online")
mutable
}
// If device is online, update user info and conversation status
server.requestRemoteUserInfo(addr)
}else {
//do nothing
}
} catch (e: Exception) {
// Log but don't immediately change status based on this check
Log.d("DeviceStatusManager", "App-level check for $ipAddress failed: ${e.message}")
}
}
}
} catch (e: Exception) {
// If any exception occurs, log but don't immediately mark as offline
Log.d("DeviceStatusManager", "Error checking device $ipAddress: ${e.message}")
// Increment failure count
val failures = (failureCountMap[ipAddress] ?: 0) + 1
failureCountMap[ipAddress] = failures
// Mark as offline after 3 consecutive failures
if (failures >= 3) {
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = false
Log.d("DeviceStatusManager",
"Device $ipAddress has $failures consecutive failures, marking offline")
mutable
}
}
}
}
}
fun handleNetworkDisconnect(ipAddress: String) {
Log.d("DeviceStatusManager", "Network layer reported disconnect for $ipAddress, immediately updating status")
// Skip for special test devices
if (ipAddress in specialDevices) {
return
}
// Immediately update status to offline
_deviceStatusMap.update { current ->
val mutable = current.toMutableMap()
mutable[ipAddress] = false
mutable
}
// Reset failure count
failureCountMap.remove(ipAddress)
// Update conversations immediately
updateConversations(ipAddress, false)
}
// Helper method to update conversations when device status changes
private fun updateConversations(ipAddress: String, isOnline: Boolean) {
scope.launch {
try {
// Get user by IP
val user = GlobalApp.GlobalUserRepo.userRepository.getUserByIp(ipAddress)
if (user != null) {
// Update conversation status
GlobalApp.GlobalUserRepo.conversationRepository.updateUserStatus(
userUuid = user.uuid,
isOnline = isOnline,
userAddress = if (isOnline) ipAddress else null
)
Log.d("DeviceStatusManager", "Updated conversation status for ${user.name}: online=$isOnline")
}
} catch (e: Exception) {
Log.e("DeviceStatusManager", "Error updating conversation for $ipAddress", e)
}
}
}
//get all online devices
fun getOnlineDevices(): List<String> {
return _deviceStatusMap.value.filter { it.value }.keys.toList()
}
//clear all device statuses
//use when restarting app or when network changes alot
fun clearAllStatuses() {
_deviceStatusMap.value = emptyMap()
lastCheckedTimes.clear()
Log.d("DeviceStatusManager", "Cleared all device statuses")
}
//Start periodic background checks for device status
private fun startPeriodicStatusChecks() {
scope.launch {
while (true) {
// Check all devices marked as online
_deviceStatusMap.value.filter { it.value }.keys.forEach { ipAddress ->
// Skip special devices
if (ipAddress !in specialDevices) {
verifyDeviceStatus(ipAddress)
}
}
// Wait between check cycles
delay(30000) // 30 seconds between checks
}
}
}
}