diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 553ad387..0ba81c88 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -511,6 +511,8 @@ class MainActivity : OrientationAwareActivity() { Log.d("MainActivity", "Location services enabled by user") mainViewModel.updateLocationLoading(false) mainViewModel.updateLocationStatus(LocationStatus.ENABLED) + // Ensure Wi-Fi Aware starts now that location is enabled + com.bitchat.android.wifiaware.WifiAwareController.startIfPossible() checkBatteryOptimizationAndProceed() } @@ -780,6 +782,9 @@ class MainActivity : OrientationAwareActivity() { mainViewModel.updateLocationStatus(currentLocationStatus) mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) mainViewModel.updateLocationLoading(false) + } else { + // If location is enabled, ensure Wi-Fi Aware starts if it was blocked by location earlier + com.bitchat.android.wifiaware.WifiAwareController.startIfPossible() } } } diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareConnectionTracker.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareConnectionTracker.kt index b9964472..76e295c5 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareConnectionTracker.kt @@ -43,9 +43,19 @@ class WifiAwareConnectionTracker( try { it.close() } catch (e: Exception) { Log.w(TAG, "Error closing server socket for $id: ${e.message}") } } - // 3. Unregister network callback + // Ensure any pending/active network request is explicitly released + releaseNetworkRequest(id) + } + + fun releaseNetworkRequest(id: String) { + if (!networkCallbacks.containsKey(id)) return + + // 3. Unregister network callback properly from ConnectivityManager networkCallbacks.remove(id)?.let { - try { cm.unregisterNetworkCallback(it) } catch (e: Exception) { Log.w(TAG, "Error unregistering callback for $id: ${e.message}") } + try { + Log.d(TAG, "Unregistering network callback for $id") + cm.unregisterNetworkCallback(it) + } catch (e: Exception) { Log.w(TAG, "Error unregistering callback for $id: ${e.message}") } } } diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt index acdb3c91..e70ed670 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt @@ -71,13 +71,36 @@ object WifiAwareController { } fun startIfPossible() { - if (_running.value) return + if (_running.value) { + if (!_enabled.value) return + // If already marked running but we are calling this (e.g. after a drop), + // ensure we clean up the old service before restarting. + stop() + } + if (!_enabled.value) return val ctx = appContext ?: return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { Log.w(TAG, "Wi‑Fi Aware requires Android 10 (Q)+; disabled.") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware not supported on this device (requires Android 10+)")) } catch (_: Exception) {} return } + + // Check system location setting: WifiAwareManager.attach() throws SecurityException if disabled + val lm = ctx.getSystemService(Context.LOCATION_SERVICE) as? android.location.LocationManager + val locationEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + lm?.isLocationEnabled == true + } else { + @Suppress("DEPRECATION") + lm?.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER) == true || + lm?.isProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER) == true + } + + if (!locationEnabled) { + Log.w(TAG, "Location services are disabled; Wi-Fi Aware cannot start.") + try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Enable Location Services to start Wi-Fi Aware")) } catch (_: Exception) {} + return + } + // Android 13+: require NEARBY_WIFI_DEVICES runtime permission if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val granted = androidx.core.content.ContextCompat.checkSelfPermission(ctx, android.Manifest.permission.NEARBY_WIFI_DEVICES) == android.content.pm.PackageManager.PERMISSION_GRANTED @@ -89,6 +112,7 @@ object WifiAwareController { } try { service = WifiAwareMeshService(ctx).also { + Log.i(TAG, "Instantiating WifiAwareMeshService...") it.startServices() _running.value = true try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware started")) } catch (_: Exception) {} diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt index 580743f2..5f295b6e 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt @@ -227,6 +227,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor override fun onPublishStarted(pub: PublishDiscoverySession) { publishSession = pub Log.d(TAG, "PUBLISH: onPublishStarted()") + try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi-Fi Aware Publish Started")) } catch (_: Exception) {} } override fun onServiceDiscovered( peerHandle: PeerHandle, @@ -235,7 +236,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor ) { val peerId = try { String(serviceSpecificInfo) } catch (_: Exception) { "" } handleToPeerId[peerHandle] = peerId - if (peerId.isNotBlank()) discoveredTimestamps[peerId] = System.currentTimeMillis() + if (peerId.isNotBlank()) { + discoveredTimestamps[peerId] = System.currentTimeMillis() + Log.i(TAG, "PUBLISH: Discovered subscriber '$peerId' via Aware") + } Log.d(TAG, "PUBLISH: onServiceDiscovered ssi='${peerId.take(16)}' len=${serviceSpecificInfo.size}") } @@ -250,9 +254,19 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor handleToPeerId[peerHandle] = subscriberId if (subscriberId.isNotBlank()) discoveredTimestamps[subscriberId] = System.currentTimeMillis() - Log.d(TAG, "PUBLISH: got ping from $subscriberId; spinning up server") + Log.i(TAG, "PUBLISH: Received discovery ping from subscriber '$subscriberId'") handleSubscriberPing(publishSession!!, peerHandle) } + + override fun onSessionTerminated() { + Log.e(TAG, "PUBLISH: onSessionTerminated()") + publishSession = null + if (isActive) { + Log.i(TAG, "PUBLISH: Attempting to restart publish session...") + // Delay and check if we need to restart services entirely + serviceScope.launch { delay(2000); if (isActive) startServices() } + } + } }, Handler(Looper.getMainLooper()) ) @@ -266,6 +280,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor override fun onSubscribeStarted(sub: SubscribeDiscoverySession) { subscribeSession = sub Log.d(TAG, "SUBSCRIBE: onSubscribeStarted()") + try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi-Fi Aware Subscribe Started")) } catch (_: Exception) {} } override fun onServiceDiscovered( peerHandle: PeerHandle, @@ -289,9 +304,18 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val peerId = handleToPeerId[peerHandle] ?: return if (peerId == myPeerID) return - Log.d(TAG, "SUBSCRIBE: onMessageReceived() → server-ready from ${peerId.take(8)} payload=${message.size}B") + Log.d(TAG, "SUBSCRIBE: onMessageReceived() \u2192 server-ready from ${peerId.take(8)} payload=${message.size}B") handleServerReady(peerHandle, message) } + + override fun onSessionTerminated() { + Log.e(TAG, "SUBSCRIBE: onSessionTerminated()") + subscribeSession = null + if (isActive) { + Log.i(TAG, "SUBSCRIBE: Attempting to restart subscribe session...") + serviceScope.launch { delay(2000); if (isActive) startServices() } + } + } }, Handler(Looper.getMainLooper()) ) @@ -299,6 +323,13 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor override fun onAttachFailed() { Log.e(TAG, "Wi-Fi Aware attach failed") } + + override fun onAwareSessionTerminated() { + Log.e(TAG, "Aware Session Terminated unexpectedly") + wifiAwareSession = null + isActive = false + if (com.bitchat.android.wifiaware.WifiAwareController.enabled.value) { serviceScope.launch { delay(3000); com.bitchat.android.wifiaware.WifiAwareController.startIfPossible() } } + } }, Handler(Looper.getMainLooper())) meshCore.startCore() @@ -366,7 +397,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor for (peerId in disconnectedPeers) { // Find the PeerHandle for this peerId val handle = handleToPeerId.entries.find { it.value == peerId }?.key ?: continue - + // Check tracker policy if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue @@ -407,14 +438,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor return } - val ss = ServerSocket(0) - connectionTracker.addServerSocket(peerId, ss) - val port = ss.localPort - - // Ensure port is set to reuse if connection was recently closed (TIME_WAIT) + val ss = ServerSocket() try { ss.reuseAddress = true - } catch (_: Exception) {} + ss.bind(java.net.InetSocketAddress(0)) + } catch (e: Exception) { Log.e(TAG, "Failed to bind server socket", e) } + + connectionTracker.addServerSocket(peerId, ss) + val port = ss.localPort Log.d(TAG, "SERVER: listening for ${peerId.take(8)} on port $port") @@ -431,20 +462,23 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val cb = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { + Log.i(TAG, "SERVER: onAvailable() - Aware network is ready for ${peerId.take(8)}") try { val client = ss.accept() + Log.i(TAG, "SERVER: Accepted raw TCP connection from ${peerId.take(8)}") try { network.bindSocket(client) } catch (e: Exception) { Log.w(TAG, "Server bindSocket EPERM: ${e.message}") } client.keepAlive = true - Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}") + Log.i(TAG, "SERVER: Bound and established TCP with ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}") connectionTracker.onClientConnected(peerId, client) try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {} try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} listenerExec.execute { listenToPeer(client, peerId) } handleSubscriberKeepAlive(client, peerId, pubSession, peerHandle) + // Kick off Noise handshake for this logical peer if (myPeerID < peerId) { meshCore.initiateNoiseHandshake(peerId) - Log.d(TAG, "SERVER: initiating Noise handshake to ${peerId.take(8)} (lower ID)") + Log.i(TAG, "SERVER: Initiating Noise handshake to ${peerId.take(8)}") } // Ensure fast presence even before handshake settles serviceScope.launch { delay(150); sendBroadcastAnnounce() } @@ -452,15 +486,27 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor Log.e(TAG, "SERVER: accept failed for ${peerId.take(8)}", ioe) } } + + override fun onUnavailable() { + Log.e(TAG, "SERVER: onUnavailable() - Failed to acquire Aware network for ${peerId.take(8)} (timeout or refused)") + handleNetworkFailure(peerId) + } + override fun onLost(network: Network) { - connectionTracker.networkCallbacks.remove(peerId) - Log.d(TAG, "SERVER: network lost for ${peerId.take(8)}") + handlePeerDisconnection(peerId) + Log.i(TAG, "SERVER: WiFi Aware network lost for ${peerId.take(8)}") } } connectionTracker.addNetworkCallback(peerId, cb) - Log.d(TAG, "SERVER: requesting Aware network for ${peerId.take(8)}") - cm.requestNetwork(req, cb) + Log.i(TAG, "SERVER: [Calling requestNetwork] for ${peerId.take(8)} with port $port") + try { + // use requestNetwork with a timeout to trigger onUnavailable if it fails + cm.requestNetwork(req, cb, 30_000) + } catch (e: Exception) { + Log.e(TAG, "SERVER: ConnectivityManager.requestNetwork threw exception", e) + connectionTracker.disconnect(peerId) + } val readyId = (System.nanoTime() and 0x7fffffff).toInt() val portBytes = ByteBuffer.allocate(4) @@ -530,7 +576,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } val port = ByteBuffer.wrap(payload).order(ByteOrder.BIG_ENDIAN).int - Log.d(TAG, "CLIENT: connecting to ${peerId.take(8)} port=$port") + Log.i(TAG, "CLIENT: Received server-ready from ${peerId.take(8)} on port $port. Requesting network...") val spec = WifiAwareNetworkSpecifier.Builder(subscribeSession!!, peerHandle) .setPskPassphrase(PSK) @@ -542,12 +588,19 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val cb = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { + Log.i(TAG, "CLIENT: onAvailable() - Aware network is ready for ${peerId.take(8)}") // Do not bind process for Aware; use per-socket binding instead } + + override fun onUnavailable() { + Log.e(TAG, "CLIENT: onUnavailable() - Failed to acquire Aware network for ${peerId.take(8)}") + handleNetworkFailure(peerId) + } + override fun onCapabilitiesChanged(network: Network, nc: NetworkCapabilities) { - if (connectionTracker.peerSockets.containsKey(peerId)) return val info = (nc.transportInfo as? WifiAwareNetworkInfo) ?: return val addr = info.peerIpv6Addr as? Inet6Address ?: return + Log.i(TAG, "CLIENT: onCapabilitiesChanged() - Peer IPv6 discovered: $addr") val lp = cm.getLinkProperties(network) val iface = lp?.interfaceName @@ -570,17 +623,18 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } sock.connect(java.net.InetSocketAddress(scopedAddr, port), 7000) - Log.d(TAG, "CLIENT: TCP connected to ${peerId.take(8)} addr=$scopedAddr:$port (iface=$iface)") + Log.i(TAG, "CLIENT: TCP connected to ${peerId.take(8)} at $scopedAddr:$port") connectionTracker.onClientConnected(peerId, sock) try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {} try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} listenerExec.execute { listenToPeer(sock, peerId) } handleServerKeepAlive(sock, peerId, peerHandle) + // Kick off Noise handshake for this logical peer if (myPeerID < peerId) { meshCore.initiateNoiseHandshake(peerId) - Log.d(TAG, "CLIENT: initiating Noise handshake to ${peerId.take(8)} (lower ID)") + Log.i(TAG, "CLIENT: Initiating Noise handshake to ${peerId.take(8)}") } // Ensure fast presence even before handshake settles serviceScope.launch { delay(150); sendBroadcastAnnounce() } @@ -589,14 +643,19 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } } override fun onLost(network: Network) { - connectionTracker.networkCallbacks.remove(peerId) - Log.d(TAG, "CLIENT: network lost for ${peerId.take(8)}") + handlePeerDisconnection(peerId) + Log.i(TAG, "CLIENT: WiFi Aware network lost for ${peerId.take(8)}") } } connectionTracker.addNetworkCallback(peerId, cb) - Log.d(TAG, "CLIENT: requesting Aware network for ${peerId.take(8)}") - cm.requestNetwork(req, cb) + Log.i(TAG, "CLIENT: [Calling requestNetwork] for ${peerId.take(8)}") + try { + cm.requestNetwork(req, cb, 30_000) + } catch (e: Exception) { + Log.e(TAG, "CLIENT: ConnectivityManager.requestNetwork threw exception", e) + connectionTracker.disconnect(peerId) + } } /** @@ -673,6 +732,15 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor socket.closeQuietly() } + private fun handleNetworkFailure(peerId: String) { + serviceScope.launch { + Log.d(TAG, "Network failure cleanup for: $peerId") + // Specifically release the callback if it didn't happen automatically + connectionTracker.releaseNetworkRequest(peerId) + meshCore.removePeer(peerId) + } + } + private fun handlePeerDisconnection(initialId: String) { serviceScope.launch { Log.d(TAG, "Cleaning up peer: $initialId")