mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
security: stop logging Noise key material, reduce noisy logging app-wide (#775)
C1 from security review: SymmetricState/HandshakeState logged raw X25519 shared secrets, chaining keys, and handshake hashes in hex to logcat on every handshake, in release builds. A logcat transcript of a handshake allowed full session decryption. Both classes no longer log at all. Also reduces excessive logging across the app (~50% fewer log calls in the noisiest files): - NoiseSession emits one line per completed handshake; per-message encrypt/decrypt and per-handshake-step debug logs removed - Removes all content/key logging: decrypted DM content, file names, payload hex dumps, pubkeys, event IDs, lat/lon, peer IPs, arti log forwarding - Collapses multi-line banner/emoji log sequences into single factual lifecycle lines (connect/disconnect, relay/Tor state transitions) - Keeps security-relevant warnings (signature failures, replay detection, key mismatches, panic wipe) in compact form No logic changes. Includes the full security review report in docs/security-review-jul-27.md.
This commit is contained in:
parent
4a34408db9
commit
bc49c71ea0
@ -221,7 +221,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
onDispose {
|
onDispose {
|
||||||
try {
|
try {
|
||||||
context.unregisterReceiver(receiver)
|
context.unregisterReceiver(receiver)
|
||||||
Log.d("BluetoothStatusUI", "BroadcastReceiver unregistered")
|
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
Log.w("BluetoothStatusUI", "Receiver was not registered")
|
Log.w("BluetoothStatusUI", "Receiver was not registered")
|
||||||
}
|
}
|
||||||
@ -354,7 +353,7 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
when (state) {
|
when (state) {
|
||||||
OnboardingState.COMPLETE -> {
|
OnboardingState.COMPLETE -> {
|
||||||
// App is fully initialized, mesh service is running
|
// App is fully initialized, mesh service is running
|
||||||
android.util.Log.d("MainActivity", "Onboarding completed - app ready")
|
android.util.Log.i("MainActivity", "Onboarding completed - app ready")
|
||||||
}
|
}
|
||||||
OnboardingState.ERROR -> {
|
OnboardingState.ERROR -> {
|
||||||
android.util.Log.e("MainActivity", "Onboarding error state reached")
|
android.util.Log.e("MainActivity", "Onboarding error state reached")
|
||||||
@ -364,8 +363,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkOnboardingStatus() {
|
private fun checkOnboardingStatus() {
|
||||||
Log.d("MainActivity", "Checking onboarding status")
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Small delay to show the checking state
|
// Small delay to show the checking state
|
||||||
delay(500)
|
delay(500)
|
||||||
@ -379,19 +376,15 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Check Bluetooth status and proceed with onboarding flow
|
* Check Bluetooth status and proceed with onboarding flow
|
||||||
*/
|
*/
|
||||||
private fun checkBluetoothAndProceed() {
|
private fun checkBluetoothAndProceed() {
|
||||||
// Log.d("MainActivity", "Checking Bluetooth status")
|
|
||||||
|
|
||||||
// Check if user has skipped Bluetooth check for this session
|
// Check if user has skipped Bluetooth check for this session
|
||||||
if (mainViewModel.isBluetoothCheckSkipped.value) {
|
if (mainViewModel.isBluetoothCheckSkipped.value) {
|
||||||
Log.d("MainActivity", "Bluetooth check skipped by user, proceeding to location check")
|
|
||||||
checkLocationAndProceed()
|
checkLocationAndProceed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For first-time users, skip Bluetooth check and go straight to permissions
|
// For first-time users, skip Bluetooth check and go straight to permissions
|
||||||
// We'll check Bluetooth after permissions are granted
|
// We'll check Bluetooth after permissions are granted
|
||||||
if (permissionManager.isFirstTimeLaunch()) {
|
if (permissionManager.isFirstTimeLaunch()) {
|
||||||
Log.d("MainActivity", "First-time launch, skipping Bluetooth check - will check after permissions")
|
|
||||||
proceedWithPermissionCheck()
|
proceedWithPermissionCheck()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -413,7 +406,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
BluetoothStatus.DISABLED -> {
|
BluetoothStatus.DISABLED -> {
|
||||||
// Show Bluetooth enable screen (should have permissions as existing user)
|
// Show Bluetooth enable screen (should have permissions as existing user)
|
||||||
Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||||||
mainViewModel.updateBluetoothLoading(false)
|
mainViewModel.updateBluetoothLoading(false)
|
||||||
}
|
}
|
||||||
@ -430,16 +422,12 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Proceed with permission checking
|
* Proceed with permission checking
|
||||||
*/
|
*/
|
||||||
private fun proceedWithPermissionCheck() {
|
private fun proceedWithPermissionCheck() {
|
||||||
Log.d("MainActivity", "Proceeding with permission check")
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
delay(200) // Small delay for smooth transition
|
delay(200) // Small delay for smooth transition
|
||||||
|
|
||||||
if (permissionManager.isFirstTimeLaunch()) {
|
if (permissionManager.isFirstTimeLaunch()) {
|
||||||
Log.d("MainActivity", "First time launch, showing permission explanation")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||||
} else if (permissionManager.areRequiredPermissionsGranted()) {
|
} else if (permissionManager.areRequiredPermissionsGranted()) {
|
||||||
Log.d("MainActivity", "Existing user with required permissions")
|
|
||||||
if (permissionManager.needsBackgroundLocationPermission() &&
|
if (permissionManager.needsBackgroundLocationPermission() &&
|
||||||
!permissionManager.isBackgroundLocationGranted() &&
|
!permissionManager.isBackgroundLocationGranted() &&
|
||||||
!com.bitchat.android.onboarding.BackgroundLocationPreferenceManager.isSkipped(this@MainActivity)
|
!com.bitchat.android.onboarding.BackgroundLocationPreferenceManager.isSkipped(this@MainActivity)
|
||||||
@ -450,7 +438,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
initializeApp()
|
initializeApp()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d("MainActivity", "Existing user missing permissions, showing explanation")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -460,7 +447,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Handle Bluetooth enabled callback
|
* Handle Bluetooth enabled callback
|
||||||
*/
|
*/
|
||||||
private fun handleBluetoothEnabled() {
|
private fun handleBluetoothEnabled() {
|
||||||
Log.d("MainActivity", "Bluetooth enabled by user")
|
|
||||||
mainViewModel.updateBluetoothLoading(false)
|
mainViewModel.updateBluetoothLoading(false)
|
||||||
mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
|
mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
|
||||||
checkLocationAndProceed()
|
checkLocationAndProceed()
|
||||||
@ -470,12 +456,9 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Check Location services status and proceed with onboarding flow
|
* Check Location services status and proceed with onboarding flow
|
||||||
*/
|
*/
|
||||||
private fun checkLocationAndProceed() {
|
private fun checkLocationAndProceed() {
|
||||||
Log.d("MainActivity", "Checking location services status")
|
|
||||||
|
|
||||||
// For first-time users, skip location check and go straight to permissions
|
// For first-time users, skip location check and go straight to permissions
|
||||||
// We'll check location after permissions are granted
|
// We'll check location after permissions are granted
|
||||||
if (permissionManager.isFirstTimeLaunch()) {
|
if (permissionManager.isFirstTimeLaunch()) {
|
||||||
Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions")
|
|
||||||
proceedWithPermissionCheck()
|
proceedWithPermissionCheck()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -491,7 +474,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
LocationStatus.DISABLED -> {
|
LocationStatus.DISABLED -> {
|
||||||
// Show location enable screen (should have permissions as existing user)
|
// Show location enable screen (should have permissions as existing user)
|
||||||
Log.d("MainActivity", "Location services disabled, showing enable screen")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||||||
mainViewModel.updateLocationLoading(false)
|
mainViewModel.updateLocationLoading(false)
|
||||||
}
|
}
|
||||||
@ -508,7 +490,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Handle Location enabled callback
|
* Handle Location enabled callback
|
||||||
*/
|
*/
|
||||||
private fun handleLocationEnabled() {
|
private fun handleLocationEnabled() {
|
||||||
Log.d("MainActivity", "Location services enabled by user")
|
|
||||||
mainViewModel.updateLocationLoading(false)
|
mainViewModel.updateLocationLoading(false)
|
||||||
mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
|
mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
|
||||||
// Ensure Wi-Fi Aware starts now that location is enabled
|
// Ensure Wi-Fi Aware starts now that location is enabled
|
||||||
@ -554,12 +535,10 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> {
|
message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> {
|
||||||
// During first-time onboarding, if Bluetooth enable fails due to permissions,
|
// During first-time onboarding, if Bluetooth enable fails due to permissions,
|
||||||
// proceed to permission explanation screen where user will grant permissions first
|
// proceed to permission explanation screen where user will grant permissions first
|
||||||
Log.d("MainActivity", "Bluetooth enable requires permissions, proceeding to permission explanation")
|
|
||||||
proceedWithPermissionCheck()
|
proceedWithPermissionCheck()
|
||||||
}
|
}
|
||||||
message.contains("Permission") -> {
|
message.contains("Permission") -> {
|
||||||
// For existing users, redirect to permission explanation to grant missing permissions
|
// For existing users, redirect to permission explanation to grant missing permissions
|
||||||
Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@ -570,8 +549,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnboardingComplete() {
|
private fun handleOnboardingComplete() {
|
||||||
Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app")
|
|
||||||
|
|
||||||
// After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status
|
// After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status
|
||||||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||||||
val currentLocationStatus = locationStatusManager.checkLocationStatus()
|
val currentLocationStatus = locationStatusManager.checkLocationStatus()
|
||||||
@ -585,28 +562,24 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
when {
|
when {
|
||||||
bleRequired2 && currentBluetoothStatus != BluetoothStatus.ENABLED -> {
|
bleRequired2 && currentBluetoothStatus != BluetoothStatus.ENABLED -> {
|
||||||
// Bluetooth still disabled, but now we have permissions to enable it
|
// Bluetooth still disabled, but now we have permissions to enable it
|
||||||
Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
|
|
||||||
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
|
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||||||
mainViewModel.updateBluetoothLoading(false)
|
mainViewModel.updateBluetoothLoading(false)
|
||||||
}
|
}
|
||||||
currentLocationStatus != LocationStatus.ENABLED -> {
|
currentLocationStatus != LocationStatus.ENABLED -> {
|
||||||
// Location services still disabled, but now we have permissions to enable it
|
// Location services still disabled, but now we have permissions to enable it
|
||||||
Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
|
|
||||||
mainViewModel.updateLocationStatus(currentLocationStatus)
|
mainViewModel.updateLocationStatus(currentLocationStatus)
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||||||
mainViewModel.updateLocationLoading(false)
|
mainViewModel.updateLocationLoading(false)
|
||||||
}
|
}
|
||||||
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
|
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
|
||||||
// Battery optimization still enabled, show battery optimization screen
|
// Battery optimization still enabled, show battery optimization screen
|
||||||
android.util.Log.d("MainActivity", "Permissions granted, but battery optimization still enabled. Showing battery optimization screen.")
|
|
||||||
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
|
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
||||||
mainViewModel.updateBatteryOptimizationLoading(false)
|
mainViewModel.updateBatteryOptimizationLoading(false)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Both are enabled, proceed to app initialization
|
// Both are enabled, proceed to app initialization
|
||||||
Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||||||
initializeApp()
|
initializeApp()
|
||||||
}
|
}
|
||||||
@ -639,19 +612,15 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Check Battery Optimization status and proceed with onboarding flow
|
* Check Battery Optimization status and proceed with onboarding flow
|
||||||
*/
|
*/
|
||||||
private fun checkBatteryOptimizationAndProceed() {
|
private fun checkBatteryOptimizationAndProceed() {
|
||||||
android.util.Log.d("MainActivity", "Checking battery optimization status")
|
|
||||||
|
|
||||||
// For first-time users, skip battery optimization check and go straight to permissions
|
// For first-time users, skip battery optimization check and go straight to permissions
|
||||||
// We'll check battery optimization after permissions are granted
|
// We'll check battery optimization after permissions are granted
|
||||||
if (permissionManager.isFirstTimeLaunch()) {
|
if (permissionManager.isFirstTimeLaunch()) {
|
||||||
android.util.Log.d("MainActivity", "First-time launch, skipping battery optimization check - will check after permissions")
|
|
||||||
proceedWithPermissionCheck()
|
proceedWithPermissionCheck()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has previously skipped battery optimization
|
// Check if user has previously skipped battery optimization
|
||||||
if (BatteryOptimizationPreferenceManager.isSkipped(this)) {
|
if (BatteryOptimizationPreferenceManager.isSkipped(this)) {
|
||||||
android.util.Log.d("MainActivity", "User previously skipped battery optimization, proceeding to permissions")
|
|
||||||
proceedWithPermissionCheck()
|
proceedWithPermissionCheck()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -672,7 +641,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
BatteryOptimizationStatus.ENABLED -> {
|
BatteryOptimizationStatus.ENABLED -> {
|
||||||
// Show battery optimization disable screen
|
// Show battery optimization disable screen
|
||||||
android.util.Log.d("MainActivity", "Battery optimization enabled, showing disable screen")
|
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
||||||
mainViewModel.updateBatteryOptimizationLoading(false)
|
mainViewModel.updateBatteryOptimizationLoading(false)
|
||||||
}
|
}
|
||||||
@ -683,7 +651,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
* Handle Battery Optimization disabled callback
|
* Handle Battery Optimization disabled callback
|
||||||
*/
|
*/
|
||||||
private fun handleBatteryOptimizationDisabled() {
|
private fun handleBatteryOptimizationDisabled() {
|
||||||
android.util.Log.d("MainActivity", "Battery optimization disabled by user")
|
|
||||||
mainViewModel.updateBatteryOptimizationLoading(false)
|
mainViewModel.updateBatteryOptimizationLoading(false)
|
||||||
mainViewModel.updateBatteryOptimizationStatus(BatteryOptimizationStatus.DISABLED)
|
mainViewModel.updateBatteryOptimizationStatus(BatteryOptimizationStatus.DISABLED)
|
||||||
proceedWithPermissionCheck()
|
proceedWithPermissionCheck()
|
||||||
@ -707,19 +674,14 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeApp() {
|
private fun initializeApp() {
|
||||||
Log.d("MainActivity", "Starting app initialization")
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
// Initialize the app with a proper delay to ensure Bluetooth stack is ready
|
// Initialize the app with a proper delay to ensure Bluetooth stack is ready
|
||||||
// This solves the issue where app needs restart to work on first install
|
// This solves the issue where app needs restart to work on first install
|
||||||
delay(1000) // Give the system time to process permission grants
|
delay(1000) // Give the system time to process permission grants
|
||||||
|
|
||||||
Log.d("MainActivity", "Permissions verified, initializing chat system")
|
|
||||||
|
|
||||||
// Initialize PoW preferences early in the initialization process
|
// Initialize PoW preferences early in the initialization process
|
||||||
PoWPreferenceManager.init(this@MainActivity)
|
PoWPreferenceManager.init(this@MainActivity)
|
||||||
Log.d("MainActivity", "PoW preferences initialized")
|
|
||||||
|
|
||||||
// Initialize Location Notes Manager (extracted to separate file)
|
// Initialize Location Notes Manager (extracted to separate file)
|
||||||
com.bitchat.android.nostr.LocationNotesInitializer.initialize(this@MainActivity)
|
com.bitchat.android.nostr.LocationNotesInitializer.initialize(this@MainActivity)
|
||||||
@ -736,16 +698,14 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
unifiedMeshService.delegate = chatViewModel
|
unifiedMeshService.delegate = chatViewModel
|
||||||
unifiedMeshService.startServices()
|
unifiedMeshService.startServices()
|
||||||
startMeshForegroundServiceBestEffort()
|
startMeshForegroundServiceBestEffort()
|
||||||
|
|
||||||
Log.d("MainActivity", "Mesh service started successfully")
|
|
||||||
|
|
||||||
// Handle any notification intent
|
// Handle any notification intent
|
||||||
handleNotificationIntent(intent)
|
handleNotificationIntent(intent)
|
||||||
handleVerificationIntent(intent)
|
handleVerificationIntent(intent)
|
||||||
|
|
||||||
// Small delay to ensure mesh service is fully initialized
|
// Small delay to ensure mesh service is fully initialized
|
||||||
delay(500)
|
delay(500)
|
||||||
Log.d("MainActivity", "App initialization complete")
|
Log.i("MainActivity", "App initialization complete")
|
||||||
mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
|
mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MainActivity", "Failed to initialize app", e)
|
Log.e("MainActivity", "Failed to initialize app", e)
|
||||||
@ -902,7 +862,6 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
// Cleanup location status manager
|
// Cleanup location status manager
|
||||||
try {
|
try {
|
||||||
locationStatusManager.cleanup()
|
locationStatusManager.cleanup()
|
||||||
Log.d("MainActivity", "Location status manager cleaned up successfully")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
|
Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,20 +145,16 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* UNIFIED: Only requests location if location services are enabled by user
|
* UNIFIED: Only requests location if location services are enabled by user
|
||||||
*/
|
*/
|
||||||
fun enableLocationChannels() {
|
fun enableLocationChannels() {
|
||||||
Log.d(TAG, "enableLocationChannels() called")
|
|
||||||
|
|
||||||
if (!_locationServicesEnabled.value || !_systemLocationEnabled.value) {
|
if (!_locationServicesEnabled.value || !_systemLocationEnabled.value) {
|
||||||
Log.w(TAG, "Location services disabled (app or system) - not requesting location")
|
Log.w(TAG, "Location services disabled (app or system) - not requesting location")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (getCurrentPermissionStatus() == PermissionState.AUTHORIZED) {
|
if (getCurrentPermissionStatus() == PermissionState.AUTHORIZED) {
|
||||||
Log.d(TAG, "Permission authorized - requesting location")
|
|
||||||
_permissionState.value = PermissionState.AUTHORIZED
|
_permissionState.value = PermissionState.AUTHORIZED
|
||||||
requestOneShotLocation()
|
requestOneShotLocation()
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Permission not granted")
|
|
||||||
_permissionState.value = PermissionState.DENIED
|
_permissionState.value = PermissionState.DENIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,8 +173,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Uses requestLocationUpdates for continuous updates, plus a one-shot to prime state immediately
|
* Uses requestLocationUpdates for continuous updates, plus a one-shot to prime state immediately
|
||||||
*/
|
*/
|
||||||
fun beginLiveRefresh(interval: Long = 5000L) {
|
fun beginLiveRefresh(interval: Long = 5000L) {
|
||||||
Log.d(TAG, "Beginning live refresh (continuous updates)")
|
|
||||||
|
|
||||||
if (_permissionState.value != PermissionState.AUTHORIZED) {
|
if (_permissionState.value != PermissionState.AUTHORIZED) {
|
||||||
Log.w(TAG, "Cannot start live refresh - permission not authorized")
|
Log.w(TAG, "Cannot start live refresh - permission not authorized")
|
||||||
return
|
return
|
||||||
@ -204,7 +198,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Stop periodic refreshes when selector UI is dismissed
|
* Stop periodic refreshes when selector UI is dismissed
|
||||||
*/
|
*/
|
||||||
fun endLiveRefresh() {
|
fun endLiveRefresh() {
|
||||||
Log.d(TAG, "Ending live refresh")
|
|
||||||
locationProvider.removeLocationUpdates(locationUpdateCallback)
|
locationProvider.removeLocationUpdates(locationUpdateCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +224,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
)
|
)
|
||||||
val isTeleportedNow = currentGeohash != channel.channel.geohash
|
val isTeleportedNow = currentGeohash != channel.channel.geohash
|
||||||
_teleported.value = isTeleportedNow
|
_teleported.value = isTeleportedNow
|
||||||
Log.d(TAG, "Teleported (immediate recompute): $isTeleportedNow (current: $currentGeohash, selected: ${channel.channel.geohash})")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,7 +233,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Set teleported status (for manual geohash teleportation)
|
* Set teleported status (for manual geohash teleportation)
|
||||||
*/
|
*/
|
||||||
fun setTeleported(teleported: Boolean) {
|
fun setTeleported(teleported: Boolean) {
|
||||||
Log.d(TAG, "Setting teleported status: $teleported")
|
|
||||||
_teleported.value = teleported
|
_teleported.value = teleported
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +240,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Enable location services (user-controlled toggle)
|
* Enable location services (user-controlled toggle)
|
||||||
*/
|
*/
|
||||||
fun enableLocationServices() {
|
fun enableLocationServices() {
|
||||||
Log.d(TAG, "enableLocationServices() called by user")
|
|
||||||
_locationServicesEnabled.value = true
|
_locationServicesEnabled.value = true
|
||||||
saveLocationServicesState(true)
|
saveLocationServicesState(true)
|
||||||
|
|
||||||
@ -263,7 +253,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Disable location services (user-controlled toggle)
|
* Disable location services (user-controlled toggle)
|
||||||
*/
|
*/
|
||||||
fun disableLocationServices() {
|
fun disableLocationServices() {
|
||||||
Log.d(TAG, "disableLocationServices() called by user")
|
|
||||||
_locationServicesEnabled.value = false
|
_locationServicesEnabled.value = false
|
||||||
saveLocationServicesState(false)
|
saveLocationServicesState(false)
|
||||||
|
|
||||||
@ -298,21 +287,17 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Requesting one-shot location")
|
|
||||||
// Set loading state initially
|
// Set loading state initially
|
||||||
_isLoadingLocation.value = true
|
_isLoadingLocation.value = true
|
||||||
|
|
||||||
locationProvider.getLastKnownLocation { cached ->
|
locationProvider.getLastKnownLocation { cached ->
|
||||||
// If we have a cached location and it's reasonably recent (e.g. < 5 mins), use it
|
// If we have a cached location and it's reasonably recent (e.g. < 5 mins), use it
|
||||||
// For now, we just use it if it exists, similar to previous logic
|
// For now, we just use it if it exists, similar to previous logic
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
Log.d(TAG, "Using last known location: ${cached.latitude}, ${cached.longitude}")
|
|
||||||
onLocationUpdated(cached)
|
onLocationUpdated(cached)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "No last known location available, requesting fresh...")
|
|
||||||
locationProvider.requestFreshLocation { fresh ->
|
locationProvider.requestFreshLocation { fresh ->
|
||||||
if (fresh != null) {
|
if (fresh != null) {
|
||||||
Log.d(TAG, "Fresh location received: ${fresh.latitude}, ${fresh.longitude}")
|
|
||||||
onLocationUpdated(fresh)
|
onLocationUpdated(fresh)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to get fresh location")
|
Log.w(TAG, "Failed to get fresh location")
|
||||||
@ -348,7 +333,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
val newState = if (hasPermission) PermissionState.AUTHORIZED else PermissionState.DENIED
|
val newState = if (hasPermission) PermissionState.AUTHORIZED else PermissionState.DENIED
|
||||||
|
|
||||||
if (_permissionState.value != newState) {
|
if (_permissionState.value != newState) {
|
||||||
Log.d(TAG, "Permission state updated to: $newState")
|
|
||||||
_permissionState.value = newState
|
_permissionState.value = newState
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,11 +340,9 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun computeChannels(location: Location) {
|
private fun computeChannels(location: Location) {
|
||||||
Log.d(TAG, "Computing channels for location: ${location.latitude}, ${location.longitude}")
|
|
||||||
|
|
||||||
val levels = GeohashChannelLevel.allCases()
|
val levels = GeohashChannelLevel.allCases()
|
||||||
val result = mutableListOf<GeohashChannel>()
|
val result = mutableListOf<GeohashChannel>()
|
||||||
|
|
||||||
for (level in levels) {
|
for (level in levels) {
|
||||||
val geohash = Geohash.encode(
|
val geohash = Geohash.encode(
|
||||||
latitude = location.latitude,
|
latitude = location.latitude,
|
||||||
@ -368,8 +350,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
precision = level.precision
|
precision = level.precision
|
||||||
)
|
)
|
||||||
result.add(GeohashChannel(level = level, geohash = geohash))
|
result.add(GeohashChannel(level = level, geohash = geohash))
|
||||||
|
|
||||||
Log.v(TAG, "Generated ${level.displayName}: $geohash")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_availableChannels.value = result
|
_availableChannels.value = result
|
||||||
@ -388,7 +368,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
)
|
)
|
||||||
val isTeleported = currentGeohash != selectedChannelValue.channel.geohash
|
val isTeleported = currentGeohash != selectedChannelValue.channel.geohash
|
||||||
_teleported.value = isTeleported
|
_teleported.value = isTeleported
|
||||||
Log.d(TAG, "Teleported status: $isTeleported (current: $currentGeohash, selected: ${selectedChannelValue.channel.geohash})")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,8 +378,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
geocodingJob = scope.launch(Dispatchers.IO) {
|
geocodingJob = scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Starting reverse geocoding")
|
|
||||||
|
|
||||||
val addresses = geocoderProvider.getFromLocation(location.latitude, location.longitude, 1)
|
val addresses = geocoderProvider.getFromLocation(location.latitude, location.longitude, 1)
|
||||||
|
|
||||||
if (!isActive) return@launch
|
if (!isActive) return@launch
|
||||||
@ -408,7 +385,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
if (addresses.isNotEmpty()) {
|
if (addresses.isNotEmpty()) {
|
||||||
val address = addresses[0]
|
val address = addresses[0]
|
||||||
val names = namesByLevel(address)
|
val names = namesByLevel(address)
|
||||||
Log.d(TAG, "Reverse geocoding result: $names")
|
|
||||||
_locationNames.value = names
|
_locationNames.value = names
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "No reverse geocoding results")
|
Log.w(TAG, "No reverse geocoding results")
|
||||||
@ -482,7 +458,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
dataManager?.saveLastGeohashChannel(channelData)
|
dataManager?.saveLastGeohashChannel(channelData)
|
||||||
Log.d(TAG, "Saved channel selection: ${channel.displayName}")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to save channel selection: ${e.message}")
|
Log.e(TAG, "Failed to save channel selection: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -499,13 +474,10 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
val channel = persisted?.toChannel()
|
val channel = persisted?.toChannel()
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
_selectedChannel.value = channel
|
_selectedChannel.value = channel
|
||||||
Log.d(TAG, "Restored persisted channel: ${channel.displayName}")
|
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh")
|
|
||||||
_selectedChannel.value = ChannelID.Mesh
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "No persisted channel found, defaulting to Mesh")
|
|
||||||
_selectedChannel.value = ChannelID.Mesh
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
} catch (e: JsonSyntaxException) {
|
} catch (e: JsonSyntaxException) {
|
||||||
@ -540,7 +512,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
dataManager?.clearLastGeohashChannel()
|
dataManager?.clearLastGeohashChannel()
|
||||||
_selectedChannel.value = ChannelID.Mesh
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
_teleported.value = false
|
_teleported.value = false
|
||||||
Log.d(TAG, "Cleared persisted channel selection")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Location Services State Persistence
|
// MARK: - Location Services State Persistence
|
||||||
@ -551,7 +522,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private fun saveLocationServicesState(enabled: Boolean) {
|
private fun saveLocationServicesState(enabled: Boolean) {
|
||||||
try {
|
try {
|
||||||
dataManager?.saveLocationServicesEnabled(enabled)
|
dataManager?.saveLocationServicesEnabled(enabled)
|
||||||
Log.d(TAG, "Saved location services state: $enabled")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to save location services state: ${e.message}")
|
Log.e(TAG, "Failed to save location services state: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -564,7 +534,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
val enabled = dataManager?.isLocationServicesEnabled() ?: false
|
val enabled = dataManager?.isLocationServicesEnabled() ?: false
|
||||||
_locationServicesEnabled.value = enabled
|
_locationServicesEnabled.value = enabled
|
||||||
Log.d(TAG, "Loaded location services state: $enabled")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load location services state: ${e.message}")
|
Log.e(TAG, "Failed to load location services state: ${e.message}")
|
||||||
_locationServicesEnabled.value = false
|
_locationServicesEnabled.value = false
|
||||||
@ -575,7 +544,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Cleanup resources
|
* Cleanup resources
|
||||||
*/
|
*/
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
Log.d(TAG, "Cleaning up LocationChannelManager")
|
|
||||||
endLiveRefresh()
|
endLiveRefresh()
|
||||||
locationProvider.cancel()
|
locationProvider.cancel()
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,6 @@ class BluetoothConnectionManager(
|
|||||||
device: BluetoothDevice?,
|
device: BluetoothDevice?,
|
||||||
ingressLinkID: String
|
ingressLinkID: String
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)")
|
|
||||||
device?.let { bluetoothDevice ->
|
device?.let { bluetoothDevice ->
|
||||||
// Get current RSSI for this device and update if available
|
// Get current RSSI for this device and update if available
|
||||||
val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address)
|
val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address)
|
||||||
@ -183,10 +182,8 @@ class BluetoothConnectionManager(
|
|||||||
|
|
||||||
toEvict.forEach { conn ->
|
toEvict.forEach { conn ->
|
||||||
if (conn.isClient) {
|
if (conn.isClient) {
|
||||||
Log.d(TAG, "Evicting client ${conn.device.address}")
|
|
||||||
try { conn.gatt?.disconnect() } catch (_: Exception) { }
|
try { conn.gatt?.disconnect() } catch (_: Exception) { }
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Evicting server ${conn.device.address}")
|
|
||||||
serverManager.disconnectDevice(conn.device)
|
serverManager.disconnectDevice(conn.device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,8 +197,6 @@ class BluetoothConnectionManager(
|
|||||||
* Start all Bluetooth services with power optimization
|
* Start all Bluetooth services with power optimization
|
||||||
*/
|
*/
|
||||||
fun startServices(): Boolean {
|
fun startServices(): Boolean {
|
||||||
Log.i(TAG, "Starting power-optimized Bluetooth services...")
|
|
||||||
|
|
||||||
if (!isBleTransportEnabled()) {
|
if (!isBleTransportEnabled()) {
|
||||||
Log.i(TAG, "BLE transport disabled by debug settings; not starting Bluetooth services")
|
Log.i(TAG, "BLE transport disabled by debug settings; not starting Bluetooth services")
|
||||||
disableTransport()
|
disableTransport()
|
||||||
@ -220,7 +215,6 @@ class BluetoothConnectionManager(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isActive = true
|
isActive = true
|
||||||
Log.d(TAG, "ConnectionManager activated (permissions and adapter OK)")
|
|
||||||
|
|
||||||
// set the adapter's name to our 8-character peerID for iOS privacy, TODO: Make this configurable
|
// set the adapter's name to our 8-character peerID for iOS privacy, TODO: Make this configurable
|
||||||
// try {
|
// try {
|
||||||
@ -250,7 +244,6 @@ class BluetoothConnectionManager(
|
|||||||
this@BluetoothConnectionManager.isActive = false
|
this@BluetoothConnectionManager.isActive = false
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Log.d(TAG, "GATT Server started")
|
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "GATT Server disabled by debug settings; not starting")
|
Log.i(TAG, "GATT Server disabled by debug settings; not starting")
|
||||||
}
|
}
|
||||||
@ -261,7 +254,6 @@ class BluetoothConnectionManager(
|
|||||||
this@BluetoothConnectionManager.isActive = false
|
this@BluetoothConnectionManager.isActive = false
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Log.d(TAG, "GATT Client started")
|
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "GATT Client disabled by debug settings; not starting")
|
Log.i(TAG, "GATT Client disabled by debug settings; not starting")
|
||||||
}
|
}
|
||||||
@ -295,12 +287,9 @@ class BluetoothConnectionManager(
|
|||||||
* Stop all Bluetooth services with proper cleanup
|
* Stop all Bluetooth services with proper cleanup
|
||||||
*/
|
*/
|
||||||
fun stopServices() {
|
fun stopServices() {
|
||||||
Log.i(TAG, "Stopping power-optimized Bluetooth services")
|
|
||||||
|
|
||||||
isActive = false
|
isActive = false
|
||||||
|
|
||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
Log.d(TAG, "Stopping client/server and power components...")
|
|
||||||
// Stop component managers
|
// Stop component managers
|
||||||
clientManager.stop()
|
clientManager.stop()
|
||||||
serverManager.stop()
|
serverManager.stop()
|
||||||
@ -323,11 +312,7 @@ class BluetoothConnectionManager(
|
|||||||
* Returns false if its coroutine scope has been cancelled.
|
* Returns false if its coroutine scope has been cancelled.
|
||||||
*/
|
*/
|
||||||
fun isReusable(): Boolean {
|
fun isReusable(): Boolean {
|
||||||
val active = connectionScope.isActive
|
return connectionScope.isActive
|
||||||
if (!active) {
|
|
||||||
Log.d(TAG, "BluetoothConnectionManager isReusable=false (scope cancelled)")
|
|
||||||
}
|
|
||||||
return active
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -492,15 +477,12 @@ class BluetoothConnectionManager(
|
|||||||
// Only restart scanning if the duty cycle behavior changed
|
// Only restart scanning if the duty cycle behavior changed
|
||||||
val nowUsingDutyCycle = powerManager.shouldUseDutyCycle()
|
val nowUsingDutyCycle = powerManager.shouldUseDutyCycle()
|
||||||
if (wasUsingDutyCycle != nowUsingDutyCycle) {
|
if (wasUsingDutyCycle != nowUsingDutyCycle) {
|
||||||
Log.d(TAG, "Duty cycle behavior changed (${wasUsingDutyCycle} -> ${nowUsingDutyCycle}), restarting scan")
|
|
||||||
val clientEnabled = isGattClientEnabled()
|
val clientEnabled = isGattClientEnabled()
|
||||||
if (clientEnabled) {
|
if (clientEnabled) {
|
||||||
clientManager.restartScanning()
|
clientManager.restartScanning()
|
||||||
} else {
|
} else {
|
||||||
clientManager.stop()
|
clientManager.stop()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Duty cycle behavior unchanged, keeping existing scan state")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce connection limits
|
// Enforce connection limits
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class BluetoothGattClientManager(
|
|||||||
*/
|
*/
|
||||||
fun connectToAddress(deviceAddress: String): Boolean {
|
fun connectToAddress(deviceAddress: String): Boolean {
|
||||||
if (!isClientRoleEnabled()) {
|
if (!isClientRoleEnabled()) {
|
||||||
Log.i(TAG, "connectToAddress skipped: BLE client disabled")
|
Log.d(TAG, "connectToAddress skipped: BLE client disabled")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
|
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
|
||||||
@ -111,7 +111,6 @@ class BluetoothGattClientManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
Log.d(TAG, "GATT client already active; start is a no-op")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (!permissionManager.hasBluetoothPermissions()) {
|
if (!permissionManager.hasBluetoothPermissions()) {
|
||||||
@ -159,7 +158,6 @@ class BluetoothGattClientManager(
|
|||||||
// Idempotent stop
|
// Idempotent stop
|
||||||
stopScanning()
|
stopScanning()
|
||||||
stopRSSIMonitoring()
|
stopRSSIMonitoring()
|
||||||
Log.i(TAG, "GATT client manager stopped (already inactive)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,10 +203,9 @@ class BluetoothGattClientManager(
|
|||||||
val connectedDevices = connectionTracker.getConnectedDevices()
|
val connectedDevices = connectionTracker.getConnectedDevices()
|
||||||
connectedDevices.values.filter { it.isClient && it.gatt != null }.forEach { deviceConn ->
|
connectedDevices.values.filter { it.isClient && it.gatt != null }.forEach { deviceConn ->
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Requesting RSSI from ${deviceConn.device.address}")
|
|
||||||
deviceConn.gatt?.readRemoteRssi()
|
deviceConn.gatt?.readRemoteRssi()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to request RSSI from ${deviceConn.device.address}: ${e.message}")
|
Log.d(TAG, "Failed to request RSSI from ${deviceConn.device.address}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(AppConstants.Mesh.RSSI_UPDATE_INTERVAL_MS)
|
delay(AppConstants.Mesh.RSSI_UPDATE_INTERVAL_MS)
|
||||||
@ -240,14 +237,13 @@ class BluetoothGattClientManager(
|
|||||||
// Rate limit scan starts to prevent "scanning too frequently" errors
|
// Rate limit scan starts to prevent "scanning too frequently" errors
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
if (isCurrentlyScanning) {
|
if (isCurrentlyScanning) {
|
||||||
Log.d(TAG, "Scan already in progress, skipping start request")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val timeSinceLastStart = currentTime - lastScanStartTime
|
val timeSinceLastStart = currentTime - lastScanStartTime
|
||||||
if (timeSinceLastStart < scanRateLimit) {
|
if (timeSinceLastStart < scanRateLimit) {
|
||||||
val remainingWait = scanRateLimit - timeSinceLastStart
|
val remainingWait = scanRateLimit - timeSinceLastStart
|
||||||
Log.w(TAG, "Scan rate limited: need to wait ${remainingWait}ms before starting scan")
|
Log.d(TAG, "Scan rate limited: waiting ${remainingWait}ms before starting scan")
|
||||||
|
|
||||||
// Schedule delayed scan start
|
// Schedule delayed scan start
|
||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
@ -263,63 +259,52 @@ class BluetoothGattClientManager(
|
|||||||
.setServiceUuid(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID))
|
.setServiceUuid(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val scanFilters = listOf(scanFilter)
|
val scanFilters = listOf(scanFilter)
|
||||||
|
|
||||||
Log.d(TAG, "Starting BLE scan with target service UUID: ${AppConstants.Mesh.Gatt.SERVICE_UUID}")
|
|
||||||
|
|
||||||
scanCallback = object : ScanCallback() {
|
scanCallback = object : ScanCallback() {
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
// Log.d(TAG, "Scan result received: ${result.device.address}")
|
|
||||||
handleScanResult(result)
|
handleScanResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||||
Log.d(TAG, "Batch scan results received: ${results.size} devices")
|
|
||||||
results.forEach { result ->
|
results.forEach { result ->
|
||||||
handleScanResult(result)
|
handleScanResult(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScanFailed(errorCode: Int) {
|
override fun onScanFailed(errorCode: Int) {
|
||||||
Log.e(TAG, "Scan failed: $errorCode")
|
|
||||||
isCurrentlyScanning = false
|
isCurrentlyScanning = false
|
||||||
lastScanStopTime = System.currentTimeMillis()
|
lastScanStopTime = System.currentTimeMillis()
|
||||||
|
|
||||||
when (errorCode) {
|
when (errorCode) {
|
||||||
1 -> {
|
1 -> {
|
||||||
// Already started: the stack thinks a scan is running. Re-arm from a clean
|
// Already started: the stack thinks a scan is running. Re-arm from a clean
|
||||||
// state so we don't stay wedged (stop then restart with backoff).
|
// state so we don't stay wedged (stop then restart with backoff).
|
||||||
Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED")
|
|
||||||
stopScanning()
|
stopScanning()
|
||||||
scheduleScanRestart("already-started", SCAN_RETRY_BASE_MS)
|
scheduleScanRestart("already-started", SCAN_RETRY_BASE_MS)
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
// App registration failed: common transient stack fault. Previously had NO
|
// App registration failed: common transient stack fault. Previously had NO
|
||||||
// retry, which left discovery dead until a manual BLE toggle.
|
// retry, which left discovery dead until a manual BLE toggle.
|
||||||
Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED")
|
|
||||||
scheduleScanRestart("registration-failed", SCAN_RETRY_BASE_MS)
|
scheduleScanRestart("registration-failed", SCAN_RETRY_BASE_MS)
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR")
|
|
||||||
scheduleScanRestart("internal-error", SCAN_RETRY_BASE_MS)
|
scheduleScanRestart("internal-error", SCAN_RETRY_BASE_MS)
|
||||||
}
|
}
|
||||||
4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED") // permanent: don't retry
|
4 -> Unit // permanent: don't retry
|
||||||
5 -> {
|
5 -> {
|
||||||
// Out of hardware resources: back off longer so other scanners/connections
|
// Out of hardware resources: back off longer so other scanners/connections
|
||||||
// can free up before we try again.
|
// can free up before we try again.
|
||||||
Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES")
|
|
||||||
scheduleScanRestart("out-of-resources", SCAN_RETRY_BASE_MS * 3)
|
scheduleScanRestart("out-of-resources", SCAN_RETRY_BASE_MS * 3)
|
||||||
}
|
}
|
||||||
6 -> {
|
6 -> {
|
||||||
Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY")
|
|
||||||
Log.w(TAG, "Scan failed due to rate limiting - will retry after delay")
|
|
||||||
scheduleScanRestart("too-frequently", 10_000L)
|
scheduleScanRestart("too-frequently", 10_000L)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(TAG, "Unknown scan failure code: $errorCode")
|
|
||||||
scheduleScanRestart("unknown-$errorCode", SCAN_RETRY_BASE_MS)
|
scheduleScanRestart("unknown-$errorCode", SCAN_RETRY_BASE_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.e(TAG, "Scan failed: $errorCode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +313,7 @@ class BluetoothGattClientManager(
|
|||||||
isCurrentlyScanning = true
|
isCurrentlyScanning = true
|
||||||
|
|
||||||
bleScanner.startScan(scanFilters, powerManager.getScanSettings(), scanCallback)
|
bleScanner.startScan(scanFilters, powerManager.getScanSettings(), scanCallback)
|
||||||
Log.d(TAG, "BLE scan started successfully")
|
Log.i(TAG, "BLE scan started")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Exception starting scan: ${e.message}")
|
Log.e(TAG, "Exception starting scan: ${e.message}")
|
||||||
isCurrentlyScanning = false
|
isCurrentlyScanning = false
|
||||||
@ -344,9 +329,9 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
if (isCurrentlyScanning) {
|
if (isCurrentlyScanning) {
|
||||||
try {
|
try {
|
||||||
scanCallback?.let {
|
scanCallback?.let {
|
||||||
bleScanner.stopScan(it)
|
bleScanner.stopScan(it)
|
||||||
Log.d(TAG, "BLE scan stopped successfully")
|
Log.i(TAG, "BLE scan stopped")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error stopping scan: ${e.message}")
|
Log.w(TAG, "Error stopping scan: ${e.message}")
|
||||||
@ -455,15 +440,11 @@ class BluetoothGattClientManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
// Log.v(TAG, "Found peerID $peerID in scan record for $deviceAddress")
|
|
||||||
if (connectionTracker.isPeerConnected(peerID)) {
|
if (connectionTracker.isPeerConnected(peerID)) {
|
||||||
Log.d(TAG, "Deduplication: Peer $peerID is already connected (ignoring $deviceAddress)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log.d(TAG, "Received scan result from $deviceAddress - already connected: ${connectionTracker.isDeviceConnected(deviceAddress)}")
|
|
||||||
|
|
||||||
// Store RSSI from scan results for later use (especially for server connections)
|
// Store RSSI from scan results for later use (especially for server connections)
|
||||||
connectionTracker.updateScanRSSI(deviceAddress, rssi)
|
connectionTracker.updateScanRSSI(deviceAddress, rssi)
|
||||||
|
|
||||||
@ -481,7 +462,6 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
// Power-aware RSSI filtering
|
// Power-aware RSSI filtering
|
||||||
if (rssi < powerManager.getRSSIThreshold()) {
|
if (rssi < powerManager.getRSSIThreshold()) {
|
||||||
Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}")
|
|
||||||
// Even if we skip connecting, still publish scan result to debug UI
|
// Even if we skip connecting, still publish scan result to debug UI
|
||||||
try {
|
try {
|
||||||
DebugSettingsManager.getInstance().addScanResult(
|
DebugSettingsManager.getInstance().addScanResult(
|
||||||
@ -503,7 +483,6 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
// Check if connection attempt is allowed
|
// Check if connection attempt is allowed
|
||||||
if (!connectionTracker.isConnectionAttemptAllowed(deviceAddress)) {
|
if (!connectionTracker.isConnectionAttemptAllowed(deviceAddress)) {
|
||||||
Log.d(TAG, "Connection to $deviceAddress not allowed due to recent attempts")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,7 +492,6 @@ class BluetoothGattClientManager(
|
|||||||
val maxClient = dbg?.maxClientConnections?.value ?: maxOverall
|
val maxClient = dbg?.maxClientConnections?.value ?: maxOverall
|
||||||
|
|
||||||
if (!connectionTracker.canConnectAsClient(maxOverall, maxClient)) {
|
if (!connectionTracker.canConnectAsClient(maxOverall, maxClient)) {
|
||||||
Log.d(TAG, "Client connection limit reached (overall: $maxOverall, client: $maxClient)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,14 +511,11 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
val deviceAddress = device.address
|
val deviceAddress = device.address
|
||||||
val linkID = UUID.randomUUID().toString()
|
val linkID = UUID.randomUUID().toString()
|
||||||
Log.i(TAG, "Connecting to bitchat device: $deviceAddress (peerID: $peerID)")
|
Log.d(TAG, "Connecting to bitchat device: $deviceAddress (peerID: $peerID)")
|
||||||
|
|
||||||
val gattCallback = object : BluetoothGattCallback() {
|
val gattCallback = object : BluetoothGattCallback() {
|
||||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||||
Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState")
|
|
||||||
|
|
||||||
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
|
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.i(TAG, "Client: Successfully connected to $deviceAddress. Requesting MTU...")
|
|
||||||
// Request a larger MTU. Must be done before any data transfer.
|
// Request a larger MTU. Must be done before any data transfer.
|
||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
delay(200) // A small delay can improve reliability of MTU request.
|
delay(200) // A small delay can improve reliability of MTU request.
|
||||||
@ -548,12 +523,9 @@ class BluetoothGattClientManager(
|
|||||||
}
|
}
|
||||||
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
||||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status")
|
Log.w(TAG, "Disconnected from $deviceAddress with error status $status (client)")
|
||||||
if (status == 147) {
|
|
||||||
Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress")
|
Log.i(TAG, "Disconnected from $deviceAddress (client)")
|
||||||
}
|
}
|
||||||
connectionTracker.cleanupDeviceConnectionIfCurrent(deviceAddress, linkID)
|
connectionTracker.cleanupDeviceConnectionIfCurrent(deviceAddress, linkID)
|
||||||
|
|
||||||
@ -573,11 +545,8 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||||
val deviceAddress = gatt.device.address
|
val deviceAddress = gatt.device.address
|
||||||
Log.i(TAG, "Client: MTU changed for $deviceAddress to $mtu with status $status")
|
|
||||||
|
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.i(TAG, "MTU successfully negotiated for $deviceAddress. Discovering services.")
|
|
||||||
|
|
||||||
// Now that MTU is set, connection is fully ready.
|
// Now that MTU is set, connection is fully ready.
|
||||||
val deviceConn = BluetoothConnectionTracker.DeviceConnection(
|
val deviceConn = BluetoothConnectionTracker.DeviceConnection(
|
||||||
device = gatt.device,
|
device = gatt.device,
|
||||||
@ -609,7 +578,7 @@ class BluetoothGattClientManager(
|
|||||||
linkID
|
linkID
|
||||||
) { it.copy(characteristic = characteristic) }
|
) { it.copy(characteristic = characteristic) }
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress")
|
// Characteristic stored on the current device connection
|
||||||
}
|
}
|
||||||
|
|
||||||
gatt.setCharacteristicNotification(characteristic, true)
|
gatt.setCharacteristicNotification(characteristic, true)
|
||||||
@ -620,7 +589,7 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
delay(200)
|
delay(200)
|
||||||
Log.i(TAG, "Client: Connection setup complete for $deviceAddress")
|
Log.i(TAG, "Connected to $deviceAddress (client)")
|
||||||
delegate?.onDeviceConnected(device)
|
delegate?.onDeviceConnected(device)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -643,42 +612,34 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||||
val value = characteristic.value
|
val value = characteristic.value
|
||||||
Log.i(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
|
|
||||||
val packet = BitchatPacket.fromBinaryData(value)
|
val packet = BitchatPacket.fromBinaryData(value)
|
||||||
if (packet != null) {
|
if (packet != null) {
|
||||||
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
|
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
|
||||||
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
|
|
||||||
delegate?.onPacketReceived(packet, peerID, gatt.device, linkID)
|
delegate?.onPacketReceived(packet, peerID, gatt.device, linkID)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes")
|
Log.d(TAG, "Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes")
|
||||||
Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
||||||
val deviceAddress = gatt.device.address
|
val deviceAddress = gatt.device.address
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.d(TAG, "Client: RSSI updated for $deviceAddress: $rssi dBm")
|
|
||||||
|
|
||||||
// Update the connection tracker with new RSSI value
|
// Update the connection tracker with new RSSI value
|
||||||
connectionTracker.updateDeviceConnectionIfCurrent(deviceAddress, linkID) {
|
connectionTracker.updateDeviceConnectionIfCurrent(deviceAddress, linkID) {
|
||||||
it.copy(rssi = rssi)
|
it.copy(rssi = rssi)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Client: Failed to read RSSI for $deviceAddress, status: $status")
|
Log.d(TAG, "Failed to read RSSI for $deviceAddress, status: $status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false")
|
|
||||||
val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
||||||
if (gatt == null) {
|
if (gatt == null) {
|
||||||
Log.e(TAG, "connectGatt returned null for $deviceAddress")
|
Log.e(TAG, "connectGatt returned null for $deviceAddress")
|
||||||
// keep the pending connection so we can avoid too many reconnections attempts, TODO: needs testing
|
// keep the pending connection so we can avoid too many reconnections attempts, TODO: needs testing
|
||||||
// connectionTracker.removePendingConnection(deviceAddress)
|
// connectionTracker.removePendingConnection(deviceAddress)
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}")
|
Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}")
|
||||||
|
|||||||
@ -87,7 +87,6 @@ class BluetoothGattServerManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
Log.d(TAG, "GATT server already active; start is a no-op")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (!permissionManager.hasBluetoothPermissions()) {
|
if (!permissionManager.hasBluetoothPermissions()) {
|
||||||
@ -127,7 +126,6 @@ class BluetoothGattServerManager(
|
|||||||
gattServer?.close()
|
gattServer?.close()
|
||||||
gattServer = null
|
gattServer = null
|
||||||
serverLinkIDs.clear()
|
serverLinkIDs.clear()
|
||||||
Log.i(TAG, "GATT server stopped (already inactive)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,13 +173,12 @@ class BluetoothGattServerManager(
|
|||||||
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
|
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
|
||||||
// Guard against callbacks after service shutdown
|
// Guard against callbacks after service shutdown
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Server: Ignoring connection state change after shutdown")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
when (newState) {
|
when (newState) {
|
||||||
BluetoothProfile.STATE_CONNECTED -> {
|
BluetoothProfile.STATE_CONNECTED -> {
|
||||||
Log.i(TAG, "Server: Device connected ${device.address}")
|
Log.i(TAG, "Connected to ${device.address} (server)")
|
||||||
val linkID = UUID.randomUUID().toString()
|
val linkID = UUID.randomUUID().toString()
|
||||||
serverLinkIDs[device.address] = linkID
|
serverLinkIDs[device.address] = linkID
|
||||||
|
|
||||||
@ -204,7 +201,7 @@ class BluetoothGattServerManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
Log.i(TAG, "Server: Device disconnected ${device.address}")
|
Log.i(TAG, "Disconnected from ${device.address} (server)")
|
||||||
val linkID = serverLinkIDs.remove(device.address)
|
val linkID = serverLinkIDs.remove(device.address)
|
||||||
if (linkID != null) {
|
if (linkID != null) {
|
||||||
connectionTracker.cleanupDeviceConnectionIfCurrent(device.address, linkID)
|
connectionTracker.cleanupDeviceConnectionIfCurrent(device.address, linkID)
|
||||||
@ -218,13 +215,10 @@ class BluetoothGattServerManager(
|
|||||||
override fun onServiceAdded(status: Int, service: BluetoothGattService) {
|
override fun onServiceAdded(status: Int, service: BluetoothGattService) {
|
||||||
// Guard against callbacks after service shutdown
|
// Guard against callbacks after service shutdown
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Server: Ignoring service added callback after shutdown")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
Log.d(TAG, "Server: Service added successfully: ${service.uuid}")
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status")
|
Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,15 +234,13 @@ class BluetoothGattServerManager(
|
|||||||
) {
|
) {
|
||||||
// Guard against callbacks after service shutdown
|
// Guard against callbacks after service shutdown
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Server: Ignoring characteristic write after shutdown")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (characteristic.uuid == AppConstants.Mesh.Gatt.CHARACTERISTIC_UUID) {
|
if (characteristic.uuid == AppConstants.Mesh.Gatt.CHARACTERISTIC_UUID) {
|
||||||
Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
|
|
||||||
val linkID = serverLinkIDs[device.address]
|
val linkID = serverLinkIDs[device.address]
|
||||||
if (linkID == null) {
|
if (linkID == null) {
|
||||||
Log.w(TAG, "Server: Dropping packet from stale connection ${device.address}")
|
Log.d(TAG, "Server: Dropping packet from stale connection ${device.address}")
|
||||||
if (responseNeeded) {
|
if (responseNeeded) {
|
||||||
gattServer?.sendResponse(
|
gattServer?.sendResponse(
|
||||||
device,
|
device,
|
||||||
@ -263,11 +255,9 @@ class BluetoothGattServerManager(
|
|||||||
val packet = BitchatPacket.fromBinaryData(value)
|
val packet = BitchatPacket.fromBinaryData(value)
|
||||||
if (packet != null) {
|
if (packet != null) {
|
||||||
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
|
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
|
||||||
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
|
|
||||||
delegate?.onPacketReceived(packet, peerID, device, linkID)
|
delegate?.onPacketReceived(packet, peerID, device, linkID)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
|
Log.d(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
|
||||||
Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseNeeded) {
|
if (responseNeeded) {
|
||||||
@ -287,14 +277,12 @@ class BluetoothGattServerManager(
|
|||||||
) {
|
) {
|
||||||
// Guard against callbacks after service shutdown
|
// Guard against callbacks after service shutdown
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Server: Ignoring descriptor write after shutdown")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
|
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
|
||||||
connectionTracker.addSubscribedDevice(device)
|
connectionTracker.addSubscribedDevice(device)
|
||||||
|
|
||||||
Log.d(TAG, "Server: Connection setup complete for ${device.address}")
|
|
||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
delay(100)
|
delay(100)
|
||||||
if (isActive) { // Check if still active
|
if (isActive) { // Check if still active
|
||||||
@ -311,19 +299,17 @@ class BluetoothGattServerManager(
|
|||||||
|
|
||||||
// Proper cleanup sequencing to prevent race conditions
|
// Proper cleanup sequencing to prevent race conditions
|
||||||
gattServer?.let { server ->
|
gattServer?.let { server ->
|
||||||
Log.d(TAG, "Cleaning up existing GATT server")
|
|
||||||
try {
|
try {
|
||||||
server.close()
|
server.close()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error closing existing GATT server: ${e.message}")
|
Log.w(TAG, "Error closing existing GATT server: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay to ensure cleanup is complete
|
// Small delay to ensure cleanup is complete
|
||||||
Thread.sleep(100)
|
Thread.sleep(100)
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Service inactive, skipping GATT server creation")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,11 +359,10 @@ class BluetoothGattServerManager(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Not starting advertising: manager not active")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
Log.i(TAG, "Not starting advertising: GATT Server disabled via debug settings")
|
Log.d(TAG, "Not starting advertising: GATT Server disabled via debug settings")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (bleAdvertiser == null) {
|
if (bleAdvertiser == null) {
|
||||||
@ -417,30 +402,24 @@ class BluetoothGattServerManager(
|
|||||||
val mode = try {
|
val mode = try {
|
||||||
powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]
|
powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]
|
||||||
} catch (_: Exception) { "unknown" }
|
} catch (_: Exception) { "unknown" }
|
||||||
Log.i(TAG, "Advertising started (power mode: $mode) with stable ID: ${peerIDBytes.joinToString("") { "%02x".format(it) }}")
|
Log.i(TAG, "Advertising started (power mode: $mode)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartFailure(errorCode: Int) {
|
override fun onStartFailure(errorCode: Int) {
|
||||||
Log.e(TAG, "Advertising failed: $errorCode")
|
Log.e(TAG, "Advertising failed: $errorCode")
|
||||||
// Previously this only logged, so if advertising failed this device became
|
// Previously this only logged, so if advertising failed this device became
|
||||||
// undiscoverable until a manual BLE toggle. Retry transient failures with backoff.
|
// undiscoverable until a manual BLE toggle. Retry transient failures with backoff.
|
||||||
when (errorCode) {
|
when (errorCode) {
|
||||||
ADVERTISE_FAILED_ALREADY_STARTED ->
|
ADVERTISE_FAILED_ALREADY_STARTED -> Unit // already advertising, no retry
|
||||||
Log.w(TAG, "ADVERTISE_FAILED_ALREADY_STARTED - already advertising, no retry")
|
ADVERTISE_FAILED_DATA_TOO_LARGE -> Unit // config issue, not retrying
|
||||||
ADVERTISE_FAILED_DATA_TOO_LARGE ->
|
ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> Unit // unsupported, not retrying
|
||||||
Log.e(TAG, "ADVERTISE_FAILED_DATA_TOO_LARGE - config issue, not retrying")
|
|
||||||
ADVERTISE_FAILED_FEATURE_UNSUPPORTED ->
|
|
||||||
Log.e(TAG, "ADVERTISE_FAILED_FEATURE_UNSUPPORTED - unsupported, not retrying")
|
|
||||||
ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
|
ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
|
||||||
Log.w(TAG, "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS - will retry after backoff")
|
|
||||||
scheduleAdvertiseRestart("too-many-advertisers")
|
scheduleAdvertiseRestart("too-many-advertisers")
|
||||||
}
|
}
|
||||||
ADVERTISE_FAILED_INTERNAL_ERROR -> {
|
ADVERTISE_FAILED_INTERNAL_ERROR -> {
|
||||||
Log.w(TAG, "ADVERTISE_FAILED_INTERNAL_ERROR - will retry after backoff")
|
|
||||||
scheduleAdvertiseRestart("internal-error")
|
scheduleAdvertiseRestart("internal-error")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unknown advertise failure $errorCode - will retry after backoff")
|
|
||||||
scheduleAdvertiseRestart("unknown-$errorCode")
|
scheduleAdvertiseRestart("unknown-$errorCode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,8 +170,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
peerManager.isPeerDirectlyConnected = { peerID ->
|
peerManager.isPeerDirectlyConnected = { peerID ->
|
||||||
connectionManager.addressPeerMap.containsValue(peerID)
|
connectionManager.addressPeerMap.containsValue(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Delegates set up; GossipSyncManager initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(packet: RoutedPacket) {
|
override fun send(packet: RoutedPacket) {
|
||||||
@ -205,19 +203,17 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
*/
|
*/
|
||||||
private fun startPeriodicDebugLogging() {
|
private fun startPeriodicDebugLogging() {
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Starting periodic debug logging loop")
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
delay(10000) // 10 seconds
|
delay(10000) // 10 seconds
|
||||||
if (isActive) { // Double-check before logging
|
if (isActive) { // Double-check before logging
|
||||||
val debugInfo = getDebugStatus()
|
val debugInfo = getDebugStatus()
|
||||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
|
Log.d(TAG, "Periodic debug status:\n$debugInfo")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Periodic debug logging loop ended (isActive=$isActive)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +223,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
private fun sendPeriodicBroadcastAnnounce() {
|
private fun sendPeriodicBroadcastAnnounce() {
|
||||||
announceJob?.cancel()
|
announceJob?.cancel()
|
||||||
announceJob = serviceScope.launch {
|
announceJob = serviceScope.launch {
|
||||||
Log.d(TAG, "Starting periodic announce loop")
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
delay(30000) // 30 seconds
|
delay(30000) // 30 seconds
|
||||||
@ -236,7 +231,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Periodic announce loop ended (isActive=$isActive)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +238,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* Setup delegate connections between components
|
* Setup delegate connections between components
|
||||||
*/
|
*/
|
||||||
private fun setupDelegates() {
|
private fun setupDelegates() {
|
||||||
Log.d(TAG, "Setting up component delegates")
|
|
||||||
// Provide nickname resolver to BLE broadcaster and debug manager
|
// Provide nickname resolver to BLE broadcaster and debug manager
|
||||||
try {
|
try {
|
||||||
val resolver: (String) -> String? = { pid -> peerManager.getPeerNickname(pid) }
|
val resolver: (String) -> String? = { pid -> peerManager.getPeerNickname(pid) }
|
||||||
@ -269,7 +262,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Also drop any Noise session state for this peer when they go offline
|
// Also drop any Noise session state for this peer when they go offline
|
||||||
try {
|
try {
|
||||||
encryptionService.removePeer(peerID)
|
encryptionService.removePeer(peerID)
|
||||||
Log.d(TAG, "Removed Noise session for offline peer $peerID")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to remove Noise session for $peerID: ${e.message}")
|
Log.w(TAG, "Failed to remove Noise session for $peerID: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -308,7 +300,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
}
|
}
|
||||||
// Send announcement and cached messages after key exchange
|
// Send announcement and cached messages after key exchange
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Key exchange completed with $peerID; sending follow-ups")
|
|
||||||
delay(100)
|
delay(100)
|
||||||
sendAnnouncementToPeer(peerID)
|
sendAnnouncementToPeer(peerID)
|
||||||
|
|
||||||
@ -331,7 +322,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Sign the handshake response
|
// Sign the handshake response
|
||||||
val signedPacket = signPacketBeforeBroadcast(responsePacket)
|
val signedPacket = signPacketBeforeBroadcast(responsePacket)
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPeerInfo(peerID: String): PeerInfo? {
|
override fun getPeerInfo(peerID: String): PeerInfo? {
|
||||||
@ -461,7 +451,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Sign the handshake packet before broadcasting
|
// Sign the handshake packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
|
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
|
||||||
}
|
}
|
||||||
@ -702,7 +691,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
|
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
|
||||||
// Send initial announcements after services are ready
|
// Send initial announcements after services are ready
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Device connected: ${device.address}; scheduling announce")
|
Log.i(TAG, "Device connected: ${device.address}")
|
||||||
delay(200)
|
delay(200)
|
||||||
sendBroadcastAnnounce()
|
sendBroadcastAnnounce()
|
||||||
}
|
}
|
||||||
@ -720,7 +709,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
device: android.bluetooth.BluetoothDevice,
|
device: android.bluetooth.BluetoothDevice,
|
||||||
linkID: String?
|
linkID: String?
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "Device disconnected: ${device.address}")
|
Log.i(TAG, "Device disconnected: ${device.address}")
|
||||||
val addr = device.address
|
val addr = device.address
|
||||||
clearProvisionalBleClaimsForLink(addr, linkID)
|
clearProvisionalBleClaimsForLink(addr, linkID)
|
||||||
|
|
||||||
@ -748,9 +737,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
provisionalBleClaims[peerID] = claim
|
provisionalBleClaims[peerID] = claim
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
delay(BLE_AUTHENTICATION_TIMEOUT_MS)
|
delay(BLE_AUTHENTICATION_TIMEOUT_MS)
|
||||||
if (provisionalBleClaims.remove(peerID, claim)) {
|
provisionalBleClaims.remove(peerID, claim)
|
||||||
Log.d(TAG, "Expired provisional BLE authentication claim for $peerID")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -793,10 +780,8 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
// Start periodic announcements for peer discovery and connectivity
|
// Start periodic announcements for peer discovery and connectivity
|
||||||
sendPeriodicBroadcastAnnounce()
|
sendPeriodicBroadcastAnnounce()
|
||||||
Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)")
|
|
||||||
// Start periodic syncs
|
// Start periodic syncs
|
||||||
com.bitchat.android.service.MeshServiceHolder.startSharedGossip("BLE")
|
com.bitchat.android.service.MeshServiceHolder.startSharedGossip("BLE")
|
||||||
Log.d(TAG, "GossipSyncManager started")
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to start Bluetooth services")
|
Log.e(TAG, "Failed to start Bluetooth services")
|
||||||
}
|
}
|
||||||
@ -847,14 +832,11 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
sendLeaveAnnouncement()
|
sendLeaveAnnouncement()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Stopping subcomponents and cancelling scope...")
|
|
||||||
delay(200) // Give leave message time to send
|
delay(200) // Give leave message time to send
|
||||||
|
|
||||||
// Stop all components
|
// Stop all components
|
||||||
com.bitchat.android.service.MeshServiceHolder.stopSharedGossip("BLE")
|
com.bitchat.android.service.MeshServiceHolder.stopSharedGossip("BLE")
|
||||||
Log.d(TAG, "GossipSyncManager stopped")
|
|
||||||
connectionManager.stopServices()
|
connectionManager.stopServices()
|
||||||
Log.d(TAG, "BluetoothConnectionManager stop requested")
|
|
||||||
peerManager.shutdown()
|
peerManager.shutdown()
|
||||||
fragmentManager.shutdown()
|
fragmentManager.shutdown()
|
||||||
securityManager.shutdown()
|
securityManager.shutdown()
|
||||||
@ -875,9 +857,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
*/
|
*/
|
||||||
fun isReusable(): Boolean {
|
fun isReusable(): Boolean {
|
||||||
val reusable = !terminated && serviceScope.isActive && connectionManager.isReusable()
|
val reusable = !terminated && serviceScope.isActive && connectionManager.isReusable()
|
||||||
if (!reusable) {
|
|
||||||
Log.d(TAG, "isReusable=false (terminated=$terminated, scopeActive=${serviceScope.isActive}, connReusable=${connectionManager.isReusable()})")
|
|
||||||
}
|
|
||||||
return reusable
|
return reusable
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -938,13 +917,11 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) {
|
fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "📤 sendFileBroadcast: name=${file.fileName}, size=${file.fileSize}")
|
|
||||||
val payload = file.encode()
|
val payload = file.encode()
|
||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
Log.e(TAG, "❌ Failed to encode file packet in sendFileBroadcast")
|
Log.e(TAG, "Failed to encode file packet in sendFileBroadcast")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📦 Encoded payload: ${payload.size} bytes")
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val packet = BitchatPacket(
|
val packet = BitchatPacket(
|
||||||
version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
|
version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
|
||||||
@ -963,8 +940,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ sendFileBroadcast failed: ${e.message}", e)
|
Log.e(TAG, "sendFileBroadcast failed (size=${file.fileSize}): ${e.message}", e)
|
||||||
Log.e(TAG, "❌ File: name=${file.fileName}, size=${file.fileSize}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -981,7 +957,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
is PrivateMediaPreparation.RequiresLegacyConsent ->
|
is PrivateMediaPreparation.RequiresLegacyConsent ->
|
||||||
Log.w(TAG, "Private media requires explicit one-shot legacy consent")
|
Log.w(TAG, "Private media requires explicit one-shot legacy consent")
|
||||||
PrivateMediaPreparation.NeedsHandshake -> {
|
PrivateMediaPreparation.NeedsHandshake -> {
|
||||||
Log.i(TAG, "Private media needs a Noise handshake; initiating without sending")
|
Log.d(TAG, "Private media needs a Noise handshake; initiating without sending")
|
||||||
initiateNoiseHandshake(recipientPeerID)
|
initiateNoiseHandshake(recipientPeerID)
|
||||||
}
|
}
|
||||||
PrivateMediaPreparation.AwaitingPeerState -> Unit
|
PrivateMediaPreparation.AwaitingPeerState -> Unit
|
||||||
@ -1051,9 +1027,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString()
|
val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString()
|
||||||
|
|
||||||
Log.d(TAG, "📨 Sending PM to $recipientPeerID: ${content.take(30)}...")
|
|
||||||
|
|
||||||
// Check if we have an established Noise session
|
// Check if we have an established Noise session
|
||||||
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||||
try {
|
try {
|
||||||
@ -1093,16 +1067,14 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
|
|
||||||
|
|
||||||
// The UI handles sent messages through its own sending path.
|
// The UI handles sent messages through its own sending path.
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}")
|
Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fire and forget - initiate handshake but don't queue exactly like iOS
|
// Fire and forget - initiate handshake but don't queue exactly like iOS
|
||||||
Log.d(TAG, "🤝 No session with $recipientPeerID, initiating handshake")
|
|
||||||
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
||||||
|
|
||||||
// The UI handles sent messages through its own sending path.
|
// The UI handles sent messages through its own sending path.
|
||||||
@ -1116,8 +1088,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
*/
|
*/
|
||||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "📖 Sending read receipt for message $messageID to $recipientPeerID")
|
|
||||||
|
|
||||||
// Route geohash read receipts via MessageRouter instead of here
|
// Route geohash read receipts via MessageRouter instead of here
|
||||||
val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull()
|
val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull()
|
||||||
val isGeoAlias = try {
|
val isGeoAlias = try {
|
||||||
@ -1133,7 +1103,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Avoid duplicate read receipts: check persistent store first
|
// Avoid duplicate read receipts: check persistent store first
|
||||||
val seenStore = try { com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) } catch (_: Exception) { null }
|
val seenStore = try { com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) } catch (_: Exception) { null }
|
||||||
if (seenStore?.hasRead(messageID) == true) {
|
if (seenStore?.hasRead(messageID) == true) {
|
||||||
Log.d(TAG, "Skipping read receipt for $messageID - already marked read")
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1161,7 +1130,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
|
|
||||||
|
|
||||||
// Persist as read after successful send
|
// Persist as read after successful send
|
||||||
try { seenStore?.markRead(messageID) } catch (_: Exception) { }
|
try { seenStore?.markRead(messageID) } catch (_: Exception) { }
|
||||||
@ -1209,7 +1177,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
|
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -1220,7 +1187,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
||||||
*/
|
*/
|
||||||
fun sendBroadcastAnnounce() {
|
fun sendBroadcastAnnounce() {
|
||||||
Log.d(TAG, "Sending broadcast announce")
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val nickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID }
|
val nickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID }
|
||||||
|
|
||||||
@ -1273,7 +1239,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
} ?: announcePacket
|
} ?: announcePacket
|
||||||
|
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
|
|
||||||
// Track announce for sync
|
// Track announce for sync
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
@ -1337,7 +1302,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
broadcastRoutedPacket(RoutedPacket(signedPacket))
|
||||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||||
Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
|
|
||||||
|
|
||||||
// Track announce for sync
|
// Track announce for sync
|
||||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||||
@ -1592,7 +1556,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Sign the packet data using our signing key
|
// Sign the packet data using our signing key
|
||||||
val signature = encryptionService.signData(packetDataForSigning)
|
val signature = encryptionService.signData(packetDataForSigning)
|
||||||
if (signature != null) {
|
if (signature != null) {
|
||||||
Log.d(TAG, "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)")
|
|
||||||
withRoute.copy(signature = signature)
|
withRoute.copy(signature = signature)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned")
|
Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned")
|
||||||
@ -1610,20 +1573,19 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* Clear all internal mesh service data (for panic mode)
|
* Clear all internal mesh service data (for panic mode)
|
||||||
*/
|
*/
|
||||||
fun clearAllInternalData() {
|
fun clearAllInternalData() {
|
||||||
Log.w(TAG, "🚨 Clearing all mesh service internal data")
|
Log.w(TAG, "Clearing all mesh service internal data")
|
||||||
try {
|
try {
|
||||||
// Stop services to cease broadcasting old ID immediately
|
// Stop services to cease broadcasting old ID immediately
|
||||||
stopServices()
|
stopServices()
|
||||||
|
|
||||||
// Clear all managers
|
// Clear all managers
|
||||||
fragmentManager.clearAllFragments()
|
fragmentManager.clearAllFragments()
|
||||||
storeForwardManager.clearAllCache()
|
storeForwardManager.clearAllCache()
|
||||||
securityManager.clearAllData()
|
securityManager.clearAllData()
|
||||||
peerManager.clearAllPeers()
|
peerManager.clearAllPeers()
|
||||||
peerManager.clearAllFingerprints()
|
peerManager.clearAllFingerprints()
|
||||||
Log.d(TAG, "✅ Cleared all mesh service internal data")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}")
|
Log.e(TAG, "Error clearing mesh service internal data: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1631,13 +1593,12 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* Clear all encryption and cryptographic data (for panic mode)
|
* Clear all encryption and cryptographic data (for panic mode)
|
||||||
*/
|
*/
|
||||||
fun clearAllEncryptionData() {
|
fun clearAllEncryptionData() {
|
||||||
Log.w(TAG, "🚨 Clearing all encryption data")
|
Log.w(TAG, "Clearing all encryption data")
|
||||||
try {
|
try {
|
||||||
// Clear encryption service persistent identity (includes Ed25519 signing keys)
|
// Clear encryption service persistent identity (includes Ed25519 signing keys)
|
||||||
encryptionService.clearPersistentIdentity()
|
encryptionService.clearPersistentIdentity()
|
||||||
Log.d(TAG, "✅ Cleared all encryption data")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error clearing encryption data: ${e.message}")
|
Log.e(TAG, "Error clearing encryption data: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,13 +122,8 @@ class BluetoothPacketBroadcaster(
|
|||||||
private val broadcasterActor = broadcasterScope.actor<BroadcastRequest>(
|
private val broadcasterActor = broadcasterScope.actor<BroadcastRequest>(
|
||||||
capacity = Channel.UNLIMITED
|
capacity = Channel.UNLIMITED
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "🎭 Created packet broadcaster actor")
|
for (request in channel) {
|
||||||
try {
|
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
|
||||||
for (request in channel) {
|
|
||||||
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Log.d(TAG, "🎭 Packet broadcaster actor terminated")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,10 +190,6 @@ class BluetoothPacketBroadcaster(
|
|||||||
// iOS-compatible: Use selective padding policy for BLE
|
// iOS-compatible: Use selective padding policy for BLE
|
||||||
val padForBLE = BLEPacketPaddingPolicy.shouldPadForBLE(packet.type)
|
val padForBLE = BLEPacketPaddingPolicy.shouldPadForBLE(packet.type)
|
||||||
val data = packet.toBinaryData(padding = padForBLE) ?: return false
|
val data = packet.toBinaryData(padding = padForBLE) ?: return false
|
||||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
|
||||||
if (isFile) {
|
|
||||||
Log.d(TAG, "📤 Broadcasting FILE_TRANSFER: ${packet.payload.size} bytes")
|
|
||||||
}
|
|
||||||
val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString()
|
val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString()
|
||||||
val senderPeerID = routed.peerID ?: packet.senderID.toHexString()
|
val senderPeerID = routed.peerID ?: packet.senderID.toHexString()
|
||||||
val incomingAddr = routed.relayAddress
|
val incomingAddr = routed.relayAddress
|
||||||
@ -300,16 +291,14 @@ class BluetoothPacketBroadcaster(
|
|||||||
// If we are the sender and a source route is defined, we must send ONLY to the first hop.
|
// If we are the sender and a source route is defined, we must send ONLY to the first hop.
|
||||||
if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) {
|
if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) {
|
||||||
val firstHop = packet.route!![0].toHexString()
|
val firstHop = packet.route!![0].toHexString()
|
||||||
Log.d(TAG, "Source Routing: Packet has explicit route, attempting to send to first hop: $firstHop")
|
|
||||||
|
|
||||||
var sent = false
|
var sent = false
|
||||||
|
|
||||||
// Try to find first hop in server connections (subscribedDevices)
|
// Try to find first hop in server connections (subscribedDevices)
|
||||||
val serverTarget = connectionTracker.getSubscribedDevices()
|
val serverTarget = connectionTracker.getSubscribedDevices()
|
||||||
.firstOrNull { connectionTracker.addressPeerMap[it.address] == firstHop }
|
.firstOrNull { connectionTracker.addressPeerMap[it.address] == firstHop }
|
||||||
|
|
||||||
if (serverTarget != null) {
|
if (serverTarget != null) {
|
||||||
Log.d(TAG, "Source Routing: sending directly to first hop (server conn) $firstHop: ${serverTarget.address}")
|
|
||||||
if (notifyDevice(serverTarget, data, gattServer, characteristic)) {
|
if (notifyDevice(serverTarget, data, gattServer, characteristic)) {
|
||||||
val toPeer = connectionTracker.addressPeerMap[serverTarget.address]
|
val toPeer = connectionTracker.addressPeerMap[serverTarget.address]
|
||||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, serverTarget.address, packet.ttl, packet.version, routeInfo)
|
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, serverTarget.address, packet.ttl, packet.version, routeInfo)
|
||||||
@ -323,7 +312,6 @@ class BluetoothPacketBroadcaster(
|
|||||||
.firstOrNull { connectionTracker.addressPeerMap[it.device.address] == firstHop }
|
.firstOrNull { connectionTracker.addressPeerMap[it.device.address] == firstHop }
|
||||||
|
|
||||||
if (clientTarget != null) {
|
if (clientTarget != null) {
|
||||||
Log.d(TAG, "Source Routing: sending directly to first hop (client conn) $firstHop: ${clientTarget.device.address}")
|
|
||||||
if (writeToDeviceConn(clientTarget, data)) {
|
if (writeToDeviceConn(clientTarget, data)) {
|
||||||
val toPeer = connectionTracker.addressPeerMap[clientTarget.device.address]
|
val toPeer = connectionTracker.addressPeerMap[clientTarget.device.address]
|
||||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, clientTarget.device.address, packet.ttl, packet.version, routeInfo)
|
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, clientTarget.device.address, packet.ttl, packet.version, routeInfo)
|
||||||
@ -333,8 +321,8 @@ class BluetoothPacketBroadcaster(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sent) return
|
if (sent) return
|
||||||
|
|
||||||
Log.w(TAG, "Source Routing: First hop $firstHop not connected. Falling back to standard broadcast logic.")
|
Log.d(TAG, "Source Routing: First hop $firstHop not connected. Falling back to standard broadcast logic.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packet.recipientID != SpecialRecipients.BROADCAST) {
|
if (packet.recipientID != SpecialRecipients.BROADCAST) {
|
||||||
@ -346,7 +334,6 @@ class BluetoothPacketBroadcaster(
|
|||||||
|
|
||||||
// If found, send directly
|
// If found, send directly
|
||||||
if (targetDevice != null) {
|
if (targetDevice != null) {
|
||||||
Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}")
|
|
||||||
if (notifyDevice(targetDevice, data, gattServer, characteristic)) {
|
if (notifyDevice(targetDevice, data, gattServer, characteristic)) {
|
||||||
val toPeer = connectionTracker.addressPeerMap[targetDevice.address]
|
val toPeer = connectionTracker.addressPeerMap[targetDevice.address]
|
||||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDevice.address, packet.ttl, packet.version, routeInfo)
|
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDevice.address, packet.ttl, packet.version, routeInfo)
|
||||||
@ -360,7 +347,6 @@ class BluetoothPacketBroadcaster(
|
|||||||
|
|
||||||
// If found, send directly
|
// If found, send directly
|
||||||
if (targetDeviceConn != null) {
|
if (targetDeviceConn != null) {
|
||||||
Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}")
|
|
||||||
if (writeToDeviceConn(targetDeviceConn, data)) {
|
if (writeToDeviceConn(targetDeviceConn, data)) {
|
||||||
val toPeer = connectionTracker.addressPeerMap[targetDeviceConn.device.address]
|
val toPeer = connectionTracker.addressPeerMap[targetDeviceConn.device.address]
|
||||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDeviceConn.device.address, packet.ttl, packet.version, routeInfo)
|
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDeviceConn.device.address, packet.ttl, packet.version, routeInfo)
|
||||||
@ -372,19 +358,15 @@ class BluetoothPacketBroadcaster(
|
|||||||
// Else, continue with broadcasting to all devices
|
// Else, continue with broadcasting to all devices
|
||||||
val subscribedDevices = connectionTracker.getSubscribedDevices()
|
val subscribedDevices = connectionTracker.getSubscribedDevices()
|
||||||
val connectedDevices = connectionTracker.getConnectedDevices()
|
val connectedDevices = connectionTracker.getConnectedDevices()
|
||||||
|
|
||||||
Log.i(TAG, "Broadcasting packet v${packet.version} type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections")
|
|
||||||
|
|
||||||
val senderID = packet.senderID.toHexString()
|
val senderID = packet.senderID.toHexString()
|
||||||
|
|
||||||
// Send to server connections (devices connected to our GATT server)
|
// Send to server connections (devices connected to our GATT server)
|
||||||
subscribedDevices.forEach { device ->
|
subscribedDevices.forEach { device ->
|
||||||
if (device.address == routed.relayAddress) {
|
if (device.address == routed.relayAddress) {
|
||||||
Log.d(TAG, "Skipping broadcast to client back to relayer: ${device.address}")
|
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
if (connectionTracker.addressPeerMap[device.address] == senderID) {
|
if (connectionTracker.addressPeerMap[device.address] == senderID) {
|
||||||
Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}")
|
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val sent = notifyDevice(device, data, gattServer, characteristic)
|
val sent = notifyDevice(device, data, gattServer, characteristic)
|
||||||
@ -398,11 +380,9 @@ class BluetoothPacketBroadcaster(
|
|||||||
connectedDevices.values.forEach { deviceConn ->
|
connectedDevices.values.forEach { deviceConn ->
|
||||||
if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
|
if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
|
||||||
if (deviceConn.device.address == routed.relayAddress) {
|
if (deviceConn.device.address == routed.relayAddress) {
|
||||||
Log.d(TAG, "Skipping broadcast to server back to relayer: ${deviceConn.device.address}")
|
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) {
|
if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) {
|
||||||
Log.d(TAG, "Skipping roadcast to server back to sender: ${deviceConn.device.address}")
|
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val sent = writeToDeviceConn(deviceConn, data)
|
val sent = writeToDeviceConn(deviceConn, data)
|
||||||
@ -479,14 +459,10 @@ class BluetoothPacketBroadcaster(
|
|||||||
* Shutdown the broadcaster actor gracefully
|
* Shutdown the broadcaster actor gracefully
|
||||||
*/
|
*/
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
Log.d(TAG, "Shutting down BluetoothPacketBroadcaster actor")
|
|
||||||
|
|
||||||
// Close the actor gracefully
|
// Close the actor gracefully
|
||||||
broadcasterActor.close()
|
broadcasterActor.close()
|
||||||
|
|
||||||
// Cancel the broadcaster scope
|
// Cancel the broadcaster scope
|
||||||
broadcasterScope.cancel()
|
broadcasterScope.cancel()
|
||||||
|
|
||||||
Log.d(TAG, "BluetoothPacketBroadcaster shutdown complete")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,22 +66,19 @@ class FragmentManager {
|
|||||||
Log.w(TAG, "Rejecting invalid outbound fragment limit: $maxFragments")
|
Log.w(TAG, "Rejecting invalid outbound fragment limit: $maxFragments")
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
Log.d(TAG, "🔀 Creating fragments for packet type ${packet.type}, payload: ${packet.payload.size} bytes")
|
|
||||||
val encoded = packet.toBinaryData()
|
val encoded = packet.toBinaryData()
|
||||||
if (encoded == null) {
|
if (encoded == null) {
|
||||||
Log.e(TAG, "❌ Failed to encode packet to binary data")
|
Log.e(TAG, "Failed to encode packet to binary data")
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📦 Encoded to ${encoded.size} bytes")
|
|
||||||
|
|
||||||
// Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix
|
// Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix
|
||||||
val fullData = try {
|
val fullData = try {
|
||||||
MessagePadding.unpad(encoded)
|
MessagePadding.unpad(encoded)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to unpad data: ${e.message}", e)
|
Log.e(TAG, "Failed to unpad data: ${e.message}", e)
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📏 Unpadded to ${fullData.size} bytes")
|
|
||||||
|
|
||||||
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
|
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
|
||||||
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
|
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||||
@ -111,21 +108,15 @@ class FragmentManager {
|
|||||||
val maxDataSize = (512 - packetOverhead).coerceAtMost(MAX_FRAGMENT_SIZE)
|
val maxDataSize = (512 - packetOverhead).coerceAtMost(MAX_FRAGMENT_SIZE)
|
||||||
|
|
||||||
if (maxDataSize <= 0) {
|
if (maxDataSize <= 0) {
|
||||||
Log.e(TAG, "❌ Calculated maxDataSize is non-positive ($maxDataSize). Route too large?")
|
Log.e(TAG, "Calculated maxDataSize is non-positive ($maxDataSize). Route too large?")
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📏 Dynamic fragment size: $maxDataSize (MAX: $MAX_FRAGMENT_SIZE, Overhead: $packetOverhead)")
|
|
||||||
|
|
||||||
val requiredFragments = (
|
val requiredFragments = (
|
||||||
(fullData.size.toLong() + maxDataSize.toLong() - 1L) / maxDataSize.toLong()
|
(fullData.size.toLong() + maxDataSize.toLong() - 1L) / maxDataSize.toLong()
|
||||||
).toInt()
|
).toInt()
|
||||||
if (requiredFragments > maxFragments) {
|
if (requiredFragments > maxFragments) {
|
||||||
Log.w(
|
Log.w(TAG, "Rejecting outbound packet requiring $requiredFragments fragments (caller cap: $maxFragments)")
|
||||||
TAG,
|
|
||||||
"Rejecting outbound packet requiring $requiredFragments fragments " +
|
|
||||||
"(caller cap: $maxFragments)"
|
|
||||||
)
|
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,8 +126,6 @@ class FragmentManager {
|
|||||||
fullData.sliceArray(offset..<endOffset)
|
fullData.sliceArray(offset..<endOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Creating ${fragmentChunks.size} fragments for ${fullData.size} byte packet (iOS compatible)")
|
|
||||||
|
|
||||||
// iOS: for (index, fragment) in fragments.enumerated()
|
// iOS: for (index, fragment) in fragments.enumerated()
|
||||||
for (index in fragmentChunks.indices) {
|
for (index in fragmentChunks.indices) {
|
||||||
val fragmentData = fragmentChunks[index]
|
val fragmentData = fragmentChunks[index]
|
||||||
@ -167,11 +156,9 @@ class FragmentManager {
|
|||||||
fragments.add(fragmentPacket)
|
fragments.add(fragmentPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✅ Created ${fragments.size} fragments successfully")
|
|
||||||
return fragments
|
return fragments
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Fragment creation failed: ${e.message}", e)
|
Log.e(TAG, "Fragment creation failed (type=${packet.type}, payload=${packet.payload.size} bytes): ${e.message}", e)
|
||||||
Log.e(TAG, "❌ Packet type: ${packet.type}, payload: ${packet.payload.size} bytes")
|
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +170,7 @@ class FragmentManager {
|
|||||||
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||||
// iOS: guard packet.payload.count > 13 else { return }
|
// iOS: guard packet.payload.count > 13 else { return }
|
||||||
if (packet.payload.size < FragmentPayload.HEADER_SIZE) {
|
if (packet.payload.size < FragmentPayload.HEADER_SIZE) {
|
||||||
Log.w(TAG, "Fragment packet too small: ${packet.payload.size}")
|
Log.d(TAG, "Fragment packet too small: ${packet.payload.size}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,14 +181,12 @@ class FragmentManager {
|
|||||||
// Use FragmentPayload for type-safe decoding
|
// Use FragmentPayload for type-safe decoding
|
||||||
val fragmentPayload = FragmentPayload.decode(packet.payload)
|
val fragmentPayload = FragmentPayload.decode(packet.payload)
|
||||||
if (fragmentPayload == null || !fragmentPayload.isValid()) {
|
if (fragmentPayload == null || !fragmentPayload.isValid()) {
|
||||||
Log.w(TAG, "Invalid fragment payload")
|
Log.d(TAG, "Invalid fragment payload")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS: let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined()
|
// iOS: let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined()
|
||||||
val fragmentIDString = fragmentPayload.getFragmentIDString()
|
val fragmentIDString = fragmentPayload.getFragmentIDString()
|
||||||
|
|
||||||
Log.d(TAG, "Received fragment ${fragmentPayload.index}/${fragmentPayload.total} for fragmentID: $fragmentIDString, originalType: ${fragmentPayload.originalType}")
|
|
||||||
|
|
||||||
val maxFragments = com.bitchat.android.util.AppConstants.Fragmentation.MAX_FRAGMENTS_PER_ID
|
val maxFragments = com.bitchat.android.util.AppConstants.Fragmentation.MAX_FRAGMENTS_PER_ID
|
||||||
if (fragmentPayload.total > maxFragments) {
|
if (fragmentPayload.total > maxFragments) {
|
||||||
@ -212,11 +197,7 @@ class FragmentManager {
|
|||||||
synchronized(fragmentStateLock) {
|
synchronized(fragmentStateLock) {
|
||||||
fragmentMetadata[fragmentIDString]?.let { (expectedType, expectedTotal, _) ->
|
fragmentMetadata[fragmentIDString]?.let { (expectedType, expectedTotal, _) ->
|
||||||
if (expectedTotal != fragmentPayload.total || expectedType != fragmentPayload.originalType) {
|
if (expectedTotal != fragmentPayload.total || expectedType != fragmentPayload.originalType) {
|
||||||
Log.w(
|
Log.w(TAG, "Rejecting fragment for $fragmentIDString: inconsistent metadata")
|
||||||
TAG,
|
|
||||||
"Rejecting fragment for $fragmentIDString: inconsistent metadata " +
|
|
||||||
"(expected type=$expectedType total=$expectedTotal, got type=${fragmentPayload.originalType} total=${fragmentPayload.total})"
|
|
||||||
)
|
|
||||||
removeFragmentSetLocked(fragmentIDString)
|
removeFragmentSetLocked(fragmentIDString)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -265,10 +246,7 @@ class FragmentManager {
|
|||||||
val delta = (fragmentPayload.data.size - oldEntrySize).toLong()
|
val delta = (fragmentPayload.data.size - oldEntrySize).toLong()
|
||||||
val maxGlobalBytes = com.bitchat.android.util.AppConstants.Fragmentation.MAX_GLOBAL_FRAGMENT_TOTAL_BYTES
|
val maxGlobalBytes = com.bitchat.android.util.AppConstants.Fragmentation.MAX_GLOBAL_FRAGMENT_TOTAL_BYTES
|
||||||
if (globalBufferedBytes + delta > maxGlobalBytes) {
|
if (globalBufferedBytes + delta > maxGlobalBytes) {
|
||||||
Log.w(
|
Log.w(TAG, "Rejecting fragment for $fragmentIDString: global buffered bytes exceed cap $maxGlobalBytes")
|
||||||
TAG,
|
|
||||||
"Rejecting fragment for $fragmentIDString: global buffered bytes ${(globalBufferedBytes + delta)} exceeds cap $maxGlobalBytes"
|
|
||||||
)
|
|
||||||
if (isNewSet) {
|
if (isNewSet) {
|
||||||
removeFragmentSetLocked(fragmentIDString)
|
removeFragmentSetLocked(fragmentIDString)
|
||||||
}
|
}
|
||||||
@ -281,8 +259,6 @@ class FragmentManager {
|
|||||||
|
|
||||||
val expectedTotal = fragmentMetadata[fragmentIDString]?.second ?: fragmentPayload.total
|
val expectedTotal = fragmentMetadata[fragmentIDString]?.second ?: fragmentPayload.total
|
||||||
if (fragmentMap.size == expectedTotal) {
|
if (fragmentMap.size == expectedTotal) {
|
||||||
Log.d(TAG, "All fragments received for $fragmentIDString, reassembling...")
|
|
||||||
|
|
||||||
// iOS reassembly logic: for i in 0..<total { if let fragment = fragments[i] { reassembled.append(fragment) } }
|
// iOS reassembly logic: for i in 0..<total { if let fragment = fragments[i] { reassembled.append(fragment) } }
|
||||||
val reassembledData = mutableListOf<Byte>()
|
val reassembledData = mutableListOf<Byte>()
|
||||||
for (i in 0 until expectedTotal) {
|
for (i in 0 until expectedTotal) {
|
||||||
@ -296,15 +272,11 @@ class FragmentManager {
|
|||||||
removeFragmentSetLocked(fragmentIDString)
|
removeFragmentSetLocked(fragmentIDString)
|
||||||
|
|
||||||
val suppressedTtlPacket = originalPacket.copy(ttl = 0u.toUByte())
|
val suppressedTtlPacket = originalPacket.copy(ttl = 0u.toUByte())
|
||||||
Log.d(TAG, "Successfully reassembled original (${reassembledData.size} bytes); set TTL=0 to suppress relay")
|
|
||||||
return suppressedTtlPacket
|
return suppressedTtlPacket
|
||||||
} else {
|
} else {
|
||||||
val metadata = fragmentMetadata[fragmentIDString]
|
val metadata = fragmentMetadata[fragmentIDString]
|
||||||
Log.e(TAG, "Failed to decode reassembled packet (type=${metadata?.first}, total=${metadata?.second})")
|
Log.e(TAG, "Failed to decode reassembled packet (type=${metadata?.first}, total=${metadata?.second})")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val received = fragmentMap.size
|
|
||||||
Log.d(TAG, "Fragment ${fragmentPayload.index} stored, have $received/$expectedTotal fragments for $fragmentIDString")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,10 +325,6 @@ class FragmentManager {
|
|||||||
for (fragmentID in oldFragments) {
|
for (fragmentID in oldFragments) {
|
||||||
removeFragmentSetLocked(fragmentID)
|
removeFragmentSetLocked(fragmentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldFragments.isNotEmpty()) {
|
|
||||||
Log.d(TAG, "Cleaned up ${oldFragments.size} old fragment sets (iOS compatible)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,15 +46,12 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
|
|
||||||
Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)")
|
|
||||||
|
|
||||||
// Skip our own messages
|
// Skip our own messages
|
||||||
if (peerID == myPeerID) return
|
if (peerID == myPeerID) return
|
||||||
|
|
||||||
// Check if this message is for us
|
// Check if this message is for us
|
||||||
val recipientID = packet.recipientID?.toHexString()
|
val recipientID = packet.recipientID?.toHexString()
|
||||||
if (recipientID != myPeerID) {
|
if (recipientID != myPeerID) {
|
||||||
Log.d(TAG, "🔐 Encrypted message not for me (for $recipientID, I am $myPeerID)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,15 +75,11 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "🔓 Decrypted NoisePayload type ${noisePayload.type} from $peerID")
|
|
||||||
|
|
||||||
when (noisePayload.type) {
|
when (noisePayload.type) {
|
||||||
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||||
// Decode TLV private message exactly like iOS
|
// Decode TLV private message exactly like iOS
|
||||||
val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data)
|
val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data)
|
||||||
if (privateMessage != null) {
|
if (privateMessage != null) {
|
||||||
Log.d(TAG, "🔓 Decrypted TLV PM from $peerID: ${privateMessage.content.take(30)}...")
|
|
||||||
|
|
||||||
// Handle favorite/unfavorite notifications embedded as PMs
|
// Handle favorite/unfavorite notifications embedded as PMs
|
||||||
val pmContent = privateMessage.content
|
val pmContent = privateMessage.content
|
||||||
if (FavoriteControlMessage.parse(pmContent) != null) {
|
if (FavoriteControlMessage.parse(pmContent) != null) {
|
||||||
@ -122,7 +115,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
// Handle encrypted file transfer; generate unique message ID
|
// Handle encrypted file transfer; generate unique message ID
|
||||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(noisePayload.data)
|
val file = com.bitchat.android.model.BitchatFilePacket.decode(noisePayload.data)
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
Log.d(TAG, "🔓 Decrypted encrypted file from $peerID: name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}'")
|
Log.d(TAG, "Encrypted file from $peerID: ${file.fileSize} bytes")
|
||||||
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
||||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
@ -137,13 +130,12 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
senderPeerID = peerID
|
senderPeerID = peerID
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d(TAG, "📄 Saved encrypted incoming file to $savedPath (msgId=$uniqueMsgId)")
|
|
||||||
delegate?.onMessageReceived(message)
|
delegate?.onMessageReceived(message)
|
||||||
|
|
||||||
// Send delivery ACK with generated message ID
|
// Send delivery ACK with generated message ID
|
||||||
sendDeliveryAck(uniqueMsgId, peerID)
|
sendDeliveryAck(uniqueMsgId, peerID)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Failed to decode encrypted file transfer from $peerID")
|
Log.w(TAG, "Failed to decode encrypted file transfer from $peerID")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +155,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
||||||
// Handle delivery ACK exactly like iOS
|
// Handle delivery ACK exactly like iOS
|
||||||
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||||
Log.d(TAG, "📬 Delivery ACK received from $peerID for message $messageID")
|
Log.d(TAG, "Delivery ACK from $peerID for $messageID")
|
||||||
|
|
||||||
// Simplified: Call delegate with messageID and peerID directly
|
// Simplified: Call delegate with messageID and peerID directly
|
||||||
delegate?.onDeliveryAckReceived(messageID, peerID)
|
delegate?.onDeliveryAckReceived(messageID, peerID)
|
||||||
@ -172,17 +164,15 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
||||||
// Handle read receipt exactly like iOS
|
// Handle read receipt exactly like iOS
|
||||||
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||||
Log.d(TAG, "👁️ Read receipt received from $peerID for message $messageID")
|
Log.d(TAG, "Read receipt from $peerID for $messageID")
|
||||||
|
|
||||||
// Simplified: Call delegate with messageID and peerID directly
|
// Simplified: Call delegate with messageID and peerID directly
|
||||||
delegate?.onReadReceiptReceived(messageID, peerID)
|
delegate?.onReadReceiptReceived(messageID, peerID)
|
||||||
}
|
}
|
||||||
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
|
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
|
||||||
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
|
|
||||||
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||||
}
|
}
|
||||||
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
|
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
|
||||||
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
|
|
||||||
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,7 +213,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
)
|
)
|
||||||
|
|
||||||
delegate?.sendPacket(packet)
|
delegate?.sendPacket(packet)
|
||||||
Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
|
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
|
||||||
@ -250,8 +239,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
if (clockSkewMs > ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS) {
|
if (clockSkewMs > ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS) {
|
||||||
Log.w(TAG, "Ignoring ANNOUNCE from ${peerID.take(8)} with excessive clock skew (${clockSkewMs}ms > ${ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS}ms)")
|
Log.w(TAG, "Ignoring ANNOUNCE from ${peerID.take(8)} with excessive clock skew (${clockSkewMs}ms > ${ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS}ms)")
|
||||||
return AnnounceHandlingResult.Rejected
|
return AnnounceHandlingResult.Rejected
|
||||||
} else if (clockSkewMs > com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS) {
|
|
||||||
Log.w(TAG, "Accepting ANNOUNCE from ${peerID.take(8)} within clock skew tolerance (${clockSkewMs}ms)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val announcement = AnnouncementIdentityValidator.verify(packet, peerID) { signature, data, key ->
|
val announcement = AnnouncementIdentityValidator.verify(packet, peerID) { signature, data, key ->
|
||||||
@ -277,7 +264,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
val existingPeer = delegate?.getPeerInfo(peerID)
|
val existingPeer = delegate?.getPeerInfo(peerID)
|
||||||
|
|
||||||
if (existingPeer != null && existingPeer.noisePublicKey != null && !existingPeer.noisePublicKey!!.contentEquals(announcement.noisePublicKey)) {
|
if (existingPeer != null && existingPeer.noisePublicKey != null && !existingPeer.noisePublicKey!!.contentEquals(announcement.noisePublicKey)) {
|
||||||
Log.w(TAG, "⚠️ Announce key mismatch for ${peerID.take(8)}... — keeping unverified")
|
Log.w(TAG, "Announce key mismatch for ${peerID.take(8)} - keeping unverified")
|
||||||
verified = false
|
verified = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,15 +281,9 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
|
|
||||||
// Require verified announce; ignore otherwise (no backward compatibility)
|
// Require verified announce; ignore otherwise (no backward compatibility)
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...")
|
|
||||||
return AnnounceHandlingResult.Rejected
|
return AnnounceHandlingResult.Rejected
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successfully decoded TLV format exactly like iOS
|
|
||||||
Log.d(TAG, "✅ Verified announce from $peerID: nickname=${announcement.nickname}, " +
|
|
||||||
"noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " +
|
|
||||||
"signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
|
|
||||||
|
|
||||||
// Extract nickname and public keys from TLV data
|
// Extract nickname and public keys from TLV data
|
||||||
val nickname = announcement.nickname
|
val nickname = announcement.nickname
|
||||||
val noisePublicKey = announcement.noisePublicKey
|
val noisePublicKey = announcement.noisePublicKey
|
||||||
@ -325,7 +306,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
.updateFromAnnouncement(peerID, nickname, neighborsOrNull, packet.timestamp)
|
.updateFromAnnouncement(peerID, nickname, neighborsOrNull, packet.timestamp)
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID")
|
Log.d(TAG, "Verified announce from $peerID (${announcement.nickname})")
|
||||||
return AnnounceHandlingResult.Accepted(isFirstAnnounce)
|
return AnnounceHandlingResult.Accepted(isFirstAnnounce)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,15 +318,12 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
|
|
||||||
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
|
||||||
|
|
||||||
// Skip our own handshake messages
|
// Skip our own handshake messages
|
||||||
if (peerID == myPeerID) return
|
if (peerID == myPeerID) return
|
||||||
|
|
||||||
// Check if handshake is addressed to us
|
// Check if handshake is addressed to us
|
||||||
val recipientID = packet.recipientID?.toHexString()
|
val recipientID = packet.recipientID?.toHexString()
|
||||||
if (recipientID != myPeerID) {
|
if (recipientID != myPeerID) {
|
||||||
Log.d(TAG, "Handshake not for me (for $recipientID, I am $myPeerID)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,8 +332,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID)
|
val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID)
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
Log.d(TAG, "Generated handshake response for $peerID (${response.size} bytes)")
|
|
||||||
|
|
||||||
// Send response using same packet type (simplified iOS approach)
|
// Send response using same packet type (simplified iOS approach)
|
||||||
val responsePacket = BitchatPacket(
|
val responsePacket = BitchatPacket(
|
||||||
version = 1u,
|
version = 1u,
|
||||||
@ -369,13 +345,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
)
|
)
|
||||||
|
|
||||||
delegate?.sendPacket(responsePacket)
|
delegate?.sendPacket(responsePacket)
|
||||||
Log.d(TAG, "📤 Sent handshake response to $peerID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if session is now established
|
|
||||||
val hasSession = delegate?.hasNoiseSession(peerID) ?: false
|
|
||||||
if (hasSession) {
|
|
||||||
Log.d(TAG, "✅ Noise session established with $peerID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -418,7 +387,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
// Enforce: only accept public messages from verified peers we know
|
// Enforce: only accept public messages from verified peers we know
|
||||||
val peerInfo = delegate?.getPeerInfo(peerID)
|
val peerInfo = delegate?.getPeerInfo(peerID)
|
||||||
if (peerInfo == null || !peerInfo.isVerifiedNickname) {
|
if (peerInfo == null || !peerInfo.isVerifiedNickname) {
|
||||||
Log.w(TAG, "🚫 Dropping public message from unverified or unknown peer ${peerID.take(8)}...")
|
Log.w(TAG, "Dropping public message from unverified peer ${peerID.take(8)}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,9 +396,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
||||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
if (isFileTransfer) {
|
|
||||||
Log.d(TAG, "📥 FILE_TRANSFER decode success (broadcast): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
|
||||||
}
|
|
||||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
id = PacketIdUtil.computeIdHex(packet).uppercase(),
|
id = PacketIdUtil.computeIdHex(packet).uppercase(),
|
||||||
@ -439,11 +406,10 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
senderPeerID = peerID,
|
senderPeerID = peerID,
|
||||||
timestamp = Date(packet.timestamp.toLong())
|
timestamp = Date(packet.timestamp.toLong())
|
||||||
)
|
)
|
||||||
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
|
||||||
delegate?.onMessageReceived(message)
|
delegate?.onMessageReceived(message)
|
||||||
return
|
return
|
||||||
} else if (isFileTransfer) {
|
} else if (isFileTransfer) {
|
||||||
Log.w(TAG, "⚠️ FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)} payloadSize=${packet.payload.size}")
|
Log.w(TAG, "FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: plain text
|
// Fallback: plain text
|
||||||
@ -487,9 +453,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
|||||||
// Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER
|
// Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER
|
||||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
if (isFileTransfer) {
|
|
||||||
Log.d(TAG, "📥 FILE_TRANSFER decode success (private): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
|
||||||
}
|
|
||||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
id = java.util.UUID.randomUUID().toString().uppercase(),
|
id = java.util.UUID.randomUUID().toString().uppercase(),
|
||||||
|
|||||||
@ -46,15 +46,8 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
private fun getOrCreateActorForPeer(peerID: String) = processorScope.actor<RoutedPacket>(
|
private fun getOrCreateActorForPeer(peerID: String) = processorScope.actor<RoutedPacket>(
|
||||||
capacity = Channel.UNLIMITED
|
capacity = Channel.UNLIMITED
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "🎭 Created packet actor for peer: ${formatPeerForLog(peerID)}")
|
for (packet in channel) {
|
||||||
try {
|
handleReceivedPacket(packet)
|
||||||
for (packet in channel) {
|
|
||||||
Log.d(TAG, "📦 Processing packet type ${packet.packet.type} from ${formatPeerForLog(peerID)} (serialized)")
|
|
||||||
handleReceivedPacket(packet)
|
|
||||||
Log.d(TAG, "Completed packet type ${packet.packet.type} from ${formatPeerForLog(peerID)}")
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Log.d(TAG, "🎭 Packet actor for ${formatPeerForLog(peerID)} terminated")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +64,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* SURGICAL FIX: Route to per-peer actor for serialized processing
|
* SURGICAL FIX: Route to per-peer actor for serialized processing
|
||||||
*/
|
*/
|
||||||
fun processPacket(routed: RoutedPacket) {
|
fun processPacket(routed: RoutedPacket) {
|
||||||
Log.d(TAG, "processPacket ${routed.packet.type}")
|
|
||||||
val peerID = routed.peerID
|
val peerID = routed.peerID
|
||||||
|
|
||||||
if (peerID == null) {
|
if (peerID == null) {
|
||||||
@ -125,13 +117,11 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
|
|
||||||
// Basic validation and security checks
|
// Basic validation and security checks
|
||||||
if (!delegate?.validatePacketSecurity(packet, peerID)!!) {
|
if (!delegate?.validatePacketSecurity(packet, peerID)!!) {
|
||||||
Log.d(TAG, "Packet failed security validation from ${formatPeerForLog(peerID)}")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var validPacket = true
|
var validPacket = true
|
||||||
val messageType = MessageType.fromValue(packet.type)
|
val messageType = MessageType.fromValue(packet.type)
|
||||||
Log.d(TAG, "Processing packet type ${messageType} from ${formatPeerForLog(peerID)}")
|
|
||||||
// Verbose logging to debug manager (and chat via ChatViewModel observer)
|
// Verbose logging to debug manager (and chat via ChatViewModel observer)
|
||||||
try {
|
try {
|
||||||
val mt = messageType?.name ?: packet.type.toString()
|
val mt = messageType?.name ?: packet.type.toString()
|
||||||
@ -162,7 +152,7 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Private packet type ${messageType} not addressed to us (from: ${formatPeerForLog(peerID)} to ${packet.recipientID?.let { it.joinToString("") { b -> "%02x".format(b) } }}), skipping")
|
// Not addressed to us; only relay handling below applies
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,8 +170,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle Noise handshake message - SIMPLIFIED iOS-compatible version
|
* Handle Noise handshake message - SIMPLIFIED iOS-compatible version
|
||||||
*/
|
*/
|
||||||
private suspend fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
private suspend fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing Noise handshake from ${formatPeerForLog(peerID)}")
|
|
||||||
return delegate?.handleNoiseHandshake(routed) ?: false
|
return delegate?.handleNoiseHandshake(routed) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,8 +177,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle Noise encrypted transport message
|
* Handle Noise encrypted transport message
|
||||||
*/
|
*/
|
||||||
private suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
private suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing Noise encrypted message from ${formatPeerForLog(peerID)}")
|
|
||||||
delegate?.handleNoiseEncrypted(routed)
|
delegate?.handleNoiseEncrypted(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,8 +184,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle announce message
|
* Handle announce message
|
||||||
*/
|
*/
|
||||||
private suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
private suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing announce from ${formatPeerForLog(peerID)}")
|
|
||||||
return delegate?.handleAnnounce(routed) ?: false
|
return delegate?.handleAnnounce(routed) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,8 +191,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle regular message
|
* Handle regular message
|
||||||
*/
|
*/
|
||||||
private suspend fun handleMessage(routed: RoutedPacket) {
|
private suspend fun handleMessage(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing message from ${formatPeerForLog(peerID)}")
|
|
||||||
delegate?.handleMessage(routed)
|
delegate?.handleMessage(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,8 +198,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle leave message
|
* Handle leave message
|
||||||
*/
|
*/
|
||||||
private suspend fun handleLeave(routed: RoutedPacket) {
|
private suspend fun handleLeave(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing leave from ${formatPeerForLog(peerID)}")
|
|
||||||
delegate?.handleLeave(routed)
|
delegate?.handleLeave(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,12 +205,8 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle message fragments
|
* Handle message fragments
|
||||||
*/
|
*/
|
||||||
private suspend fun handleFragment(routed: RoutedPacket) {
|
private suspend fun handleFragment(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing fragment from ${formatPeerForLog(peerID)}")
|
|
||||||
|
|
||||||
val reassembledPacket = delegate?.handleFragment(routed.packet)
|
val reassembledPacket = delegate?.handleFragment(routed.packet)
|
||||||
if (reassembledPacket != null) {
|
if (reassembledPacket != null) {
|
||||||
Log.d(TAG, "Fragment reassembled, processing complete message")
|
|
||||||
handleReceivedPacket(
|
handleReceivedPacket(
|
||||||
RoutedPacket(
|
RoutedPacket(
|
||||||
packet = reassembledPacket,
|
packet = reassembledPacket,
|
||||||
@ -248,8 +224,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
* Handle REQUEST_SYNC packets (public, TTL=1)
|
* Handle REQUEST_SYNC packets (public, TTL=1)
|
||||||
*/
|
*/
|
||||||
private suspend fun handleRequestSync(routed: RoutedPacket) {
|
private suspend fun handleRequestSync(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing REQUEST_SYNC from ${formatPeerForLog(peerID)}")
|
|
||||||
delegate?.handleRequestSync(routed)
|
delegate?.handleRequestSync(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,8 +272,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
|
|
||||||
// Cancel the main scope
|
// Cancel the main scope
|
||||||
processorScope.cancel()
|
processorScope.cancel()
|
||||||
|
|
||||||
Log.d(TAG, "PacketProcessor shutdown complete")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
fun validatePacket(packet: BitchatPacket, peerID: String): Boolean {
|
fun validatePacket(packet: BitchatPacket, peerID: String): Boolean {
|
||||||
// Skip validation for our own packets
|
// Skip validation for our own packets
|
||||||
if (peerID == myPeerID) {
|
if (peerID == myPeerID) {
|
||||||
Log.d(TAG, "Skipping validation for our own packet")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,15 +82,12 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
packet.ttl >= com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
packet.ttl >= com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
||||||
|
|
||||||
if (!isFreshAnnounce) {
|
if (!isFreshAnnounce) {
|
||||||
Log.d(TAG, "Dropping duplicate packet: $messageID")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Allowing duplicate ANNOUNCE from direct neighbor: $messageID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce mandatory signature verification
|
// Enforce mandatory signature verification
|
||||||
if (!verifyPacketSignature(packet, peerID)) {
|
if (!verifyPacketSignature(packet, peerID)) {
|
||||||
Log.w(TAG, "Dropping packet from $peerID due to signature verification failure")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,8 +96,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
// later legitimate packet with the same timestamp and payload.
|
// later legitimate packet with the same timestamp and payload.
|
||||||
processedMessages.add(messageID)
|
processedMessages.add(messageID)
|
||||||
messageTimestamps[messageID] = currentTime
|
messageTimestamps[messageID] = currentTime
|
||||||
|
|
||||||
Log.d(TAG, "Packet validation passed for $peerID, messageID: $messageID")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,27 +110,24 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
|
|
||||||
// Skip handshakes not addressed to us
|
// Skip handshakes not addressed to us
|
||||||
if (packet.recipientID?.toHexString() != myPeerID) {
|
if (packet.recipientID?.toHexString() != myPeerID) {
|
||||||
Log.d(TAG, "Skipping handshake not addressed to us: $peerID")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip our own handshake messages
|
// Skip our own handshake messages
|
||||||
if (peerID == myPeerID) return false
|
if (peerID == myPeerID) return false
|
||||||
|
|
||||||
if (packet.payload.isEmpty()) {
|
if (packet.payload.isEmpty()) {
|
||||||
Log.w(TAG, "Noise handshake packet has empty payload")
|
Log.d(TAG, "Noise handshake packet has empty payload")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent duplicate handshake processing
|
// Prevent duplicate handshake processing
|
||||||
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
|
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
|
||||||
|
|
||||||
if (processedKeyExchanges.contains(exchangeKey)) {
|
if (processedKeyExchanges.contains(exchangeKey)) {
|
||||||
Log.d(TAG, "Already processed handshake: $exchangeKey")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// The session manager preserves an existing transport in a separate responder-candidate
|
// The session manager preserves an existing transport in a separate responder-candidate
|
||||||
// flow and reports whether this exact frame completed authentication. Never infer that
|
// flow and reports whether this exact frame completed authentication. Never infer that
|
||||||
@ -144,7 +136,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
processedKeyExchanges.add(exchangeKey)
|
processedKeyExchanges.add(exchangeKey)
|
||||||
|
|
||||||
if (result.response != null) {
|
if (result.response != null) {
|
||||||
Log.d(TAG, "Successfully processed Noise handshake from $peerID, sending response")
|
|
||||||
// Send handshake response through delegate
|
// Send handshake response through delegate
|
||||||
delegate?.sendHandshakeResponse(peerID, result.response)
|
delegate?.sendHandshakeResponse(peerID, result.response)
|
||||||
}
|
}
|
||||||
@ -162,7 +153,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val isDirectIngress = packet.ttl == com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
val isDirectIngress = packet.ttl == com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
||||||
Log.d(TAG, "✅ Noise handshake completed with $peerID")
|
Log.i(TAG, "Noise handshake completed with $peerID")
|
||||||
delegate?.onKeyExchangeCompleted(
|
delegate?.onKeyExchangeCompleted(
|
||||||
peerID = peerID,
|
peerID = peerID,
|
||||||
authenticatedRemoteStaticKey = authenticatedRemoteStaticKey,
|
authenticatedRemoteStaticKey = authenticatedRemoteStaticKey,
|
||||||
@ -175,7 +166,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
|
Log.w(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,7 +306,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
|
|
||||||
// 1. Mandatory Signature Check
|
// 1. Mandatory Signature Check
|
||||||
if (packet.signature == null) {
|
if (packet.signature == null) {
|
||||||
Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})")
|
Log.w(TAG, "Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,14 +317,14 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
if (signingPublicKey == null) {
|
if (signingPublicKey == null) {
|
||||||
// If we don't have a key (and it's not an announce), we can't verify.
|
// If we don't have a key (and it's not an announce), we can't verify.
|
||||||
// For security, we must reject packets from unknown peers unless it's an announce.
|
// For security, we must reject packets from unknown peers unless it's an announce.
|
||||||
Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNING_KEY_AVAILABLE (packet type ${packet.type})")
|
Log.w(TAG, "Signature check for $peerID: NO_SIGNING_KEY_AVAILABLE (packet type ${packet.type})")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get Canonical Data
|
// 3. Get Canonical Data
|
||||||
val packetDataForSigning = packet.toBinaryDataForSigning()
|
val packetDataForSigning = packet.toBinaryDataForSigning()
|
||||||
if (packetDataForSigning == null) {
|
if (packetDataForSigning == null) {
|
||||||
Log.w(TAG, "❌ Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})")
|
Log.w(TAG, "Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,15 +337,14 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (isSignatureValid) {
|
if (isSignatureValid) {
|
||||||
// Log.v(TAG, "✅ Signature verified for $peerID (type ${packet.type})")
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "❌ Signature INVALID for $peerID (type ${packet.type})")
|
Log.w(TAG, "Signature INVALID for $peerID (type ${packet.type})")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Signature verification error for $peerID: ${e.message}")
|
Log.e(TAG, "Signature verification error for $peerID: ${e.message}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,27 +395,23 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
*/
|
*/
|
||||||
private fun cleanupOldData() {
|
private fun cleanupOldData() {
|
||||||
val cutoffTime = System.currentTimeMillis() - MESSAGE_TIMEOUT
|
val cutoffTime = System.currentTimeMillis() - MESSAGE_TIMEOUT
|
||||||
var removedCount = 0
|
|
||||||
|
|
||||||
// Clean up old message timestamps and corresponding processed messages
|
// Clean up old message timestamps and corresponding processed messages
|
||||||
val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) ->
|
val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) ->
|
||||||
timestamp < cutoffTime
|
timestamp < cutoffTime
|
||||||
}.map { it.key }
|
}.map { it.key }
|
||||||
|
|
||||||
messagesToRemove.forEach { messageId ->
|
messagesToRemove.forEach { messageId ->
|
||||||
messageTimestamps.remove(messageId)
|
messageTimestamps.remove(messageId)
|
||||||
if (processedMessages.remove(messageId)) {
|
processedMessages.remove(messageId)
|
||||||
removedCount++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit the size of processed messages set
|
// Limit the size of processed messages set
|
||||||
if (processedMessages.size > MAX_PROCESSED_MESSAGES) {
|
if (processedMessages.size > MAX_PROCESSED_MESSAGES) {
|
||||||
val excess = processedMessages.size - MAX_PROCESSED_MESSAGES
|
val excess = processedMessages.size - MAX_PROCESSED_MESSAGES
|
||||||
val toRemove = processedMessages.take(excess)
|
val toRemove = processedMessages.take(excess)
|
||||||
processedMessages.removeAll(toRemove.toSet())
|
processedMessages.removeAll(toRemove.toSet())
|
||||||
removeFromMessageTimestamps(toRemove)
|
removeFromMessageTimestamps(toRemove)
|
||||||
removedCount += excess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit the size of processed key exchanges set
|
// Limit the size of processed key exchanges set
|
||||||
@ -434,10 +420,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
val toRemove = processedKeyExchanges.take(excess)
|
val toRemove = processedKeyExchanges.take(excess)
|
||||||
processedKeyExchanges.removeAll(toRemove.toSet())
|
processedKeyExchanges.removeAll(toRemove.toSet())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedCount > 0) {
|
|
||||||
Log.d(TAG, "Cleaned up $removedCount old processed messages")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -130,7 +130,6 @@ class ArtiTorManager private constructor() {
|
|||||||
val logListener = ArtiLogListener { logLine ->
|
val logListener = ArtiLogListener { logLine ->
|
||||||
val text = logLine ?: return@ArtiLogListener
|
val text = logLine ?: return@ArtiLogListener
|
||||||
val s = text
|
val s = text
|
||||||
Log.i(TAG, "arti: $s")
|
|
||||||
lastLogTime.set(System.currentTimeMillis())
|
lastLogTime.set(System.currentTimeMillis())
|
||||||
_statusFlow.update { it.copy(lastLogLine = s) }
|
_statusFlow.update { it.copy(lastLogLine = s) }
|
||||||
handleArtiLogLine(s)
|
handleArtiLogLine(s)
|
||||||
@ -198,10 +197,6 @@ class ArtiTorManager private constructor() {
|
|||||||
if (mode == s.mode && mode != TorMode.OFF &&
|
if (mode == s.mode && mode != TorMode.OFF &&
|
||||||
(lifecycleState == LifecycleState.STARTING || lifecycleState == LifecycleState.RUNNING)
|
(lifecycleState == LifecycleState.STARTING || lifecycleState == LifecycleState.RUNNING)
|
||||||
) {
|
) {
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"applyMode: already in progress/running mode=$mode, state=$lifecycleState; skip"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
when (mode) {
|
when (mode) {
|
||||||
@ -264,7 +259,6 @@ class ArtiTorManager private constructor() {
|
|||||||
private suspend fun startArti(application: Application, useDelay: Boolean = false) {
|
private suspend fun startArti(application: Application, useDelay: Boolean = false) {
|
||||||
try {
|
try {
|
||||||
stopArtiAndWait()
|
stopArtiAndWait()
|
||||||
Log.i(TAG, "Starting Arti on port $currentSocksPort…")
|
|
||||||
if (useDelay) {
|
if (useDelay) {
|
||||||
delay(RESTART_DELAY_MS)
|
delay(RESTART_DELAY_MS)
|
||||||
}
|
}
|
||||||
@ -296,10 +290,7 @@ class ArtiTorManager private constructor() {
|
|||||||
if (isBindError && bindRetryAttempts < MAX_RETRY_ATTEMPTS) {
|
if (isBindError && bindRetryAttempts < MAX_RETRY_ATTEMPTS) {
|
||||||
bindRetryAttempts++
|
bindRetryAttempts++
|
||||||
currentSocksPort++
|
currentSocksPort++
|
||||||
Log.w(
|
Log.w(TAG, "Port bind failed (attempt $bindRetryAttempts/$MAX_RETRY_ATTEMPTS), retrying with port $currentSocksPort")
|
||||||
TAG,
|
|
||||||
"Port bind failed (attempt $bindRetryAttempts/$MAX_RETRY_ATTEMPTS), retrying with port $currentSocksPort"
|
|
||||||
)
|
|
||||||
socksAddr = InetSocketAddress("127.0.0.1", currentSocksPort)
|
socksAddr = InetSocketAddress("127.0.0.1", currentSocksPort)
|
||||||
resetNetworkConnections()
|
resetNetworkConnections()
|
||||||
startArti(application, useDelay = false)
|
startArti(application, useDelay = false)
|
||||||
@ -347,7 +338,6 @@ class ArtiTorManager private constructor() {
|
|||||||
try {
|
try {
|
||||||
val proxy = artiProxy
|
val proxy = artiProxy
|
||||||
if (proxy != null) {
|
if (proxy != null) {
|
||||||
Log.i(TAG, "Stopping Arti…")
|
|
||||||
try {
|
try {
|
||||||
proxy.stop()
|
proxy.stop()
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
@ -397,10 +387,7 @@ class ArtiTorManager private constructor() {
|
|||||||
if (currentMode == TorMode.ON) {
|
if (currentMode == TorMode.ON) {
|
||||||
val bootstrapPercent = _statusFlow.value.bootstrapPercent
|
val bootstrapPercent = _statusFlow.value.bootstrapPercent
|
||||||
if (bootstrapPercent < 100) {
|
if (bootstrapPercent < 100) {
|
||||||
Log.w(
|
Log.w(TAG, "Inactivity detected (${timeSinceLastActivity}ms), restarting Arti")
|
||||||
TAG,
|
|
||||||
"Inactivity detected (${timeSinceLastActivity}ms), restarting Arti"
|
|
||||||
)
|
|
||||||
currentApplication?.let { app ->
|
currentApplication?.let { app ->
|
||||||
appScope.launch {
|
appScope.launch {
|
||||||
restartArti(app)
|
restartArti(app)
|
||||||
@ -429,7 +416,6 @@ class ArtiTorManager private constructor() {
|
|||||||
delay(delayMs)
|
delay(delayMs)
|
||||||
val currentMode = _statusFlow.value.mode
|
val currentMode = _statusFlow.value.mode
|
||||||
if (currentMode == TorMode.ON) {
|
if (currentMode == TorMode.ON) {
|
||||||
Log.i(TAG, "Retrying Arti start (attempt $retryAttempts)")
|
|
||||||
restartArti(application)
|
restartArti(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,7 +451,6 @@ class ArtiTorManager private constructor() {
|
|||||||
when {
|
when {
|
||||||
s.contains("AMEx: state changed to Initialized", ignoreCase = true) -> {
|
s.contains("AMEx: state changed to Initialized", ignoreCase = true) -> {
|
||||||
if (currentLifecycle != LifecycleState.STARTING && currentLifecycle != LifecycleState.RUNNING) {
|
if (currentLifecycle != LifecycleState.STARTING && currentLifecycle != LifecycleState.RUNNING) {
|
||||||
Log.w(TAG, "Ignoring stale 'Initialized' log (lifecycle: $currentLifecycle)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_statusFlow.update { it.copy(state = TorState.STARTING) }
|
_statusFlow.update { it.copy(state = TorState.STARTING) }
|
||||||
@ -474,7 +459,6 @@ class ArtiTorManager private constructor() {
|
|||||||
|
|
||||||
s.contains("AMEx: state changed to Starting", ignoreCase = true) -> {
|
s.contains("AMEx: state changed to Starting", ignoreCase = true) -> {
|
||||||
if (currentLifecycle != LifecycleState.STARTING && currentLifecycle != LifecycleState.RUNNING) {
|
if (currentLifecycle != LifecycleState.STARTING && currentLifecycle != LifecycleState.RUNNING) {
|
||||||
Log.w(TAG, "Ignoring stale 'Starting' log (lifecycle: $currentLifecycle)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_statusFlow.update { it.copy(state = TorState.STARTING) }
|
_statusFlow.update { it.copy(state = TorState.STARTING) }
|
||||||
@ -486,7 +470,6 @@ class ArtiTorManager private constructor() {
|
|||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
) -> {
|
) -> {
|
||||||
if (currentLifecycle != LifecycleState.RUNNING) {
|
if (currentLifecycle != LifecycleState.RUNNING) {
|
||||||
Log.w(TAG, "Ignoring bootstrap log (lifecycle: $currentLifecycle)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_statusFlow.update {
|
_statusFlow.update {
|
||||||
@ -502,7 +485,6 @@ class ArtiTorManager private constructor() {
|
|||||||
|
|
||||||
s.contains("We have found that guard [scrubbed] is usable.", ignoreCase = true) -> {
|
s.contains("We have found that guard [scrubbed] is usable.", ignoreCase = true) -> {
|
||||||
if (currentLifecycle != LifecycleState.RUNNING) {
|
if (currentLifecycle != LifecycleState.RUNNING) {
|
||||||
Log.w(TAG, "Ignoring guard discovery log (lifecycle: $currentLifecycle)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_statusFlow.update {
|
_statusFlow.update {
|
||||||
@ -517,7 +499,6 @@ class ArtiTorManager private constructor() {
|
|||||||
|
|
||||||
s.contains("AMEx: state changed to Stopping", ignoreCase = true) -> {
|
s.contains("AMEx: state changed to Stopping", ignoreCase = true) -> {
|
||||||
if (currentLifecycle != LifecycleState.STOPPING) {
|
if (currentLifecycle != LifecycleState.STOPPING) {
|
||||||
Log.w(TAG, "Ignoring stale 'Stopping' log (lifecycle: $currentLifecycle)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_statusFlow.update {
|
_statusFlow.update {
|
||||||
@ -530,10 +511,6 @@ class ArtiTorManager private constructor() {
|
|||||||
|
|
||||||
s.contains("AMEx: state changed to Stopped", ignoreCase = true) -> {
|
s.contains("AMEx: state changed to Stopped", ignoreCase = true) -> {
|
||||||
if (currentLifecycle != LifecycleState.STOPPING && currentLifecycle != LifecycleState.STOPPED) {
|
if (currentLifecycle != LifecycleState.STOPPING && currentLifecycle != LifecycleState.STOPPED) {
|
||||||
Log.w(
|
|
||||||
TAG,
|
|
||||||
"Ignoring stale 'Stopped' log (lifecycle: $currentLifecycle, preventing state corruption)"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_statusFlow.update {
|
_statusFlow.update {
|
||||||
|
|||||||
@ -88,7 +88,7 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
if (loadedKeyPair != null) {
|
if (loadedKeyPair != null) {
|
||||||
staticIdentityPrivateKey = loadedKeyPair.first
|
staticIdentityPrivateKey = loadedKeyPair.first
|
||||||
staticIdentityPublicKey = loadedKeyPair.second
|
staticIdentityPublicKey = loadedKeyPair.second
|
||||||
Log.d(TAG, "Loaded existing static identity key: ${calculateFingerprint(staticIdentityPublicKey)}")
|
Log.d(TAG, "Identity loaded: ${calculateFingerprint(staticIdentityPublicKey).take(16)}")
|
||||||
} else {
|
} else {
|
||||||
// Generate new identity key pair
|
// Generate new identity key pair
|
||||||
val keyPair = generateKeyPair()
|
val keyPair = generateKeyPair()
|
||||||
@ -97,7 +97,6 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
|
|
||||||
// Save to secure storage
|
// Save to secure storage
|
||||||
identityStateManager.saveStaticKey(staticIdentityPrivateKey, staticIdentityPublicKey)
|
identityStateManager.saveStaticKey(staticIdentityPrivateKey, staticIdentityPublicKey)
|
||||||
Log.d(TAG, "Generated and saved new static identity key")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load or create Ed25519 signing key (persistent across sessions)
|
// Load or create Ed25519 signing key (persistent across sessions)
|
||||||
@ -105,7 +104,6 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
if (loadedSigningKeyPair != null) {
|
if (loadedSigningKeyPair != null) {
|
||||||
signingPrivateKey = loadedSigningKeyPair.first
|
signingPrivateKey = loadedSigningKeyPair.first
|
||||||
signingPublicKey = loadedSigningKeyPair.second
|
signingPublicKey = loadedSigningKeyPair.second
|
||||||
Log.d(TAG, "Loaded existing Ed25519 signing key")
|
|
||||||
} else {
|
} else {
|
||||||
// Generate new Ed25519 signing key pair
|
// Generate new Ed25519 signing key pair
|
||||||
val signingKeyPair = generateEd25519KeyPair()
|
val signingKeyPair = generateEd25519KeyPair()
|
||||||
@ -114,7 +112,6 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
|
|
||||||
// Save to secure storage
|
// Save to secure storage
|
||||||
identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey)
|
identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey)
|
||||||
Log.d(TAG, "Generated and saved new Ed25519 signing key")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +160,7 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
* Clear persistent identity (for panic mode)
|
* Clear persistent identity (for panic mode)
|
||||||
*/
|
*/
|
||||||
fun clearPersistentIdentity() {
|
fun clearPersistentIdentity() {
|
||||||
Log.w(TAG, "🚨 Panic Mode: Clearing persistent identity and rotating in-memory keys")
|
Log.w(TAG, "Panic: clearing persistent identity and rotating in-memory keys")
|
||||||
|
|
||||||
// 1. Clear storage
|
// 1. Clear storage
|
||||||
identityStateManager.clearIdentityData()
|
identityStateManager.clearIdentityData()
|
||||||
@ -179,7 +176,6 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
// 4. Re-initialize SessionManager with new keys
|
// 4. Re-initialize SessionManager with new keys
|
||||||
initializeSessionManager()
|
initializeSessionManager()
|
||||||
|
|
||||||
Log.d(TAG, "✅ Identity cleared and keys rotated")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Handshake Management
|
// MARK: - Handshake Management
|
||||||
@ -377,7 +373,7 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
* Initiate rekey for a session (replaces old session with new handshake)
|
* Initiate rekey for a session (replaces old session with new handshake)
|
||||||
*/
|
*/
|
||||||
fun initiateRekey(peerID: String): ByteArray? {
|
fun initiateRekey(peerID: String): ByteArray? {
|
||||||
Log.d(TAG, "Initiating rekey for session with $peerID")
|
Log.d(TAG, "Rekeying session with $peerID")
|
||||||
|
|
||||||
// Remove old session
|
// Remove old session
|
||||||
sessionManager.removeSession(peerID)
|
sessionManager.removeSession(peerID)
|
||||||
@ -437,7 +433,6 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
// Calculate fingerprint for logging and callback
|
// Calculate fingerprint for logging and callback
|
||||||
val fingerprint = calculateFingerprint(remoteStaticKey)
|
val fingerprint = calculateFingerprint(remoteStaticKey)
|
||||||
|
|
||||||
Log.d(TAG, "Session established with $peerID, fingerprint: ${fingerprint.take(16)}...")
|
|
||||||
|
|
||||||
// Notify about authentication
|
// Notify about authentication
|
||||||
onPeerAuthenticated?.invoke(peerID, fingerprint)
|
onPeerAuthenticated?.invoke(peerID, fingerprint)
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package com.bitchat.android.noise
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.noise.southernstorm.protocol.*
|
import com.bitchat.android.noise.southernstorm.protocol.*
|
||||||
import com.bitchat.android.util.toHexString
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
|
||||||
@ -123,7 +122,6 @@ class NoiseSession(
|
|||||||
}
|
}
|
||||||
// Extract ciphertext (remaining bytes)
|
// Extract ciphertext (remaining bytes)
|
||||||
val ciphertext = combinedPayload.copyOfRange(NONCE_SIZE_BYTES, combinedPayload.size)
|
val ciphertext = combinedPayload.copyOfRange(NONCE_SIZE_BYTES, combinedPayload.size)
|
||||||
Log.d(TAG, "Extracted nonce: $extractedNonce, ciphertext size: ${ciphertext.size}")
|
|
||||||
return Pair(extractedNonce, ciphertext)
|
return Pair(extractedNonce, ciphertext)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -210,9 +208,7 @@ class NoiseSession(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
try {
|
||||||
// Validate static keys
|
|
||||||
validateStaticKeys()
|
validateStaticKeys()
|
||||||
Log.d(TAG, "Created ${if (isInitiator) "initiator" else "responder"} session for $peerID")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state = NoiseSessionState.Failed(e)
|
state = NoiseSessionState.Failed(e)
|
||||||
Log.e(TAG, "Failed to initialize Noise session: ${e.message}")
|
Log.e(TAG, "Failed to initialize Noise session: ${e.message}")
|
||||||
@ -237,8 +233,6 @@ class NoiseSession(
|
|||||||
if (localStaticPublicKey.all { it == 0.toByte() }) {
|
if (localStaticPublicKey.all { it == 0.toByte() }) {
|
||||||
throw IllegalArgumentException("Local static public key cannot be all zeros")
|
throw IllegalArgumentException("Local static public key cannot be all zeros")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Static keys validated successfully - private: ${localStaticPrivateKey.size} bytes, public: ${localStaticPublicKey.size} bytes")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -247,60 +241,24 @@ class NoiseSession(
|
|||||||
*/
|
*/
|
||||||
private fun initializeNoiseHandshake(role: Int) {
|
private fun initializeNoiseHandshake(role: Int) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Creating HandshakeState with role: ${if (role == HandshakeState.INITIATOR) "INITIATOR" else "RESPONDER"}")
|
|
||||||
|
|
||||||
// LOGGING: Track Android handshake initialization (matching iOS)
|
|
||||||
Log.d(TAG, "=== ANDROID NOISE SESSION - BEFORE HANDSHAKE INIT ===")
|
|
||||||
Log.d(TAG, "Creating NoiseHandshakeState for peer: $peerID")
|
|
||||||
Log.d(TAG, "Role: ${if (role == HandshakeState.INITIATOR) "INITIATOR" else "RESPONDER"}")
|
|
||||||
|
|
||||||
handshakeState = HandshakeState(PROTOCOL_NAME, role)
|
handshakeState = HandshakeState(PROTOCOL_NAME, role)
|
||||||
Log.d(TAG, "HandshakeState created successfully")
|
|
||||||
|
|
||||||
Log.d(TAG, "=== ANDROID NOISE SESSION - AFTER HANDSHAKE INIT ===")
|
|
||||||
Log.d(TAG, "NoiseHandshakeState created and mixPreMessageKeys() completed")
|
|
||||||
|
|
||||||
if (handshakeState?.needsLocalKeyPair() == true) {
|
if (handshakeState?.needsLocalKeyPair() == true) {
|
||||||
Log.d(TAG, "Local static key pair is required for XX pattern")
|
|
||||||
|
|
||||||
val localKeyPair = handshakeState?.getLocalKeyPair()
|
val localKeyPair = handshakeState?.getLocalKeyPair()
|
||||||
if (localKeyPair != null) {
|
if (localKeyPair != null) {
|
||||||
// FIXED: Use the provided persistent identity keys with our local fork
|
|
||||||
// Our local fork properly supports setting pre-existing keys
|
|
||||||
Log.d(TAG, "Setting persistent static identity keys...")
|
|
||||||
|
|
||||||
localKeyPair.setPrivateKey(localStaticPrivateKey, 0)
|
localKeyPair.setPrivateKey(localStaticPrivateKey, 0)
|
||||||
|
|
||||||
if (!localKeyPair.hasPrivateKey() || !localKeyPair.hasPublicKey()) {
|
if (!localKeyPair.hasPrivateKey() || !localKeyPair.hasPublicKey()) {
|
||||||
throw IllegalStateException("Failed to set static identity keys - local fork issue")
|
throw IllegalStateException("Failed to set static identity keys - local fork issue")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✓ Successfully set persistent static identity keys")
|
|
||||||
Log.d(TAG, "Algorithm: ${localKeyPair.dhName}")
|
|
||||||
Log.d(TAG, "Private key length: ${localKeyPair.privateKeyLength}")
|
|
||||||
Log.d(TAG, "Public key length: ${localKeyPair.publicKeyLength}")
|
|
||||||
|
|
||||||
// Verify the keys were set correctly
|
|
||||||
val verifyPrivate = ByteArray(32)
|
|
||||||
val verifyPublic = ByteArray(32)
|
|
||||||
localKeyPair.getPrivateKey(verifyPrivate, 0)
|
|
||||||
localKeyPair.getPublicKey(verifyPublic, 0)
|
|
||||||
|
|
||||||
Log.d(TAG, "Persistent identity public key: ${localStaticPublicKey.joinToString("") { "%02x".format(it) }}")
|
|
||||||
Log.d(TAG, "Set public key: ${verifyPublic.joinToString("") { "%02x".format(it) }}")
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException("HandshakeState returned null for local key pair")
|
throw IllegalStateException("HandshakeState returned null for local key pair")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Local static key pair not needed for this handshake pattern/role")
|
|
||||||
}
|
}
|
||||||
handshakeState?.start()
|
handshakeState?.start()
|
||||||
Log.d(TAG, "Handshake state started successfully with persistent identity keys")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Exception during handshake initialization: ${e.message}", e)
|
Log.e(TAG, "Handshake init failed for $peerID: ${e.message}", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,8 +273,6 @@ class NoiseSession(
|
|||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startHandshake(): ByteArray {
|
fun startHandshake(): ByteArray {
|
||||||
Log.d(TAG, "Starting noise XX handshake with $peerID as INITIATOR")
|
|
||||||
|
|
||||||
if (!isInitiator) {
|
if (!isInitiator) {
|
||||||
throw IllegalStateException("Only initiator can start handshake")
|
throw IllegalStateException("Only initiator can start handshake")
|
||||||
}
|
}
|
||||||
@ -343,15 +299,13 @@ class NoiseSession(
|
|||||||
|
|
||||||
// Validate message size matches XX pattern expectations
|
// Validate message size matches XX pattern expectations
|
||||||
if (firstMessage.size != XX_MESSAGE_1_SIZE) {
|
if (firstMessage.size != XX_MESSAGE_1_SIZE) {
|
||||||
Log.w(TAG, "Warning: XX message 1 size ${firstMessage.size} != expected $XX_MESSAGE_1_SIZE")
|
Log.w(TAG, "XX message 1 size ${firstMessage.size} != expected $XX_MESSAGE_1_SIZE")
|
||||||
}
|
}
|
||||||
|
|
||||||
val ePrefix = firstMessage.take(4).toByteArray().toHexString()
|
|
||||||
Log.d(TAG, "Sending XX handshake message 1 to $peerID (${firstMessage.size} bytes) e_prefix=$ePrefix currentPattern: $currentPattern")
|
|
||||||
return firstMessage
|
return firstMessage
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state = NoiseSessionState.Failed(e)
|
state = NoiseSessionState.Failed(e)
|
||||||
Log.e(TAG, "Failed to start handshake: ${e.message}")
|
Log.e(TAG, "Failed to start handshake with $peerID: ${e.message}")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -362,9 +316,6 @@ class NoiseSession(
|
|||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
||||||
val inputPrefix = message.take(4).toByteArray().toHexString()
|
|
||||||
Log.d(TAG, "Processing handshake message from $peerID (${message.size} bytes) prefix=$inputPrefix")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialize as responder if receiving first message
|
// Initialize as responder if receiving first message
|
||||||
if (state == NoiseSessionState.Uninitialized && !isInitiator) {
|
if (state == NoiseSessionState.Uninitialized && !isInitiator) {
|
||||||
@ -373,7 +324,6 @@ class NoiseSession(
|
|||||||
if (handshakeStartMs == null) {
|
if (handshakeStartMs == null) {
|
||||||
handshakeStartMs = System.currentTimeMillis()
|
handshakeStartMs = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Initialized as RESPONDER for XX handshake with $peerID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != NoiseSessionState.Handshaking) {
|
if (state != NoiseSessionState.Handshaking) {
|
||||||
@ -389,13 +339,10 @@ class NoiseSession(
|
|||||||
// Read the incoming message - the Noise library will handle validation
|
// Read the incoming message - the Noise library will handle validation
|
||||||
val payloadLength = handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0)
|
val payloadLength = handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0)
|
||||||
currentPattern++
|
currentPattern++
|
||||||
val readPrefix = message.take(4).toByteArray().toHexString()
|
|
||||||
Log.d(TAG, "Read handshake message, payload length: $payloadLength prefix=$readPrefix currentPattern: $currentPattern")
|
|
||||||
|
|
||||||
// Check what action the handshake state wants us to take next
|
// Check what action the handshake state wants us to take next
|
||||||
val action = handshakeStateLocal.getAction()
|
val action = handshakeStateLocal.getAction()
|
||||||
Log.d(TAG, "Handshake action after processing message: $action")
|
|
||||||
|
|
||||||
return when (action) {
|
return when (action) {
|
||||||
HandshakeState.WRITE_MESSAGE -> {
|
HandshakeState.WRITE_MESSAGE -> {
|
||||||
// Noise library says we need to send a response
|
// Noise library says we need to send a response
|
||||||
@ -403,33 +350,22 @@ class NoiseSession(
|
|||||||
val responseLength = handshakeStateLocal.writeMessage(responseBuffer, 0, null, 0, 0)
|
val responseLength = handshakeStateLocal.writeMessage(responseBuffer, 0, null, 0, 0)
|
||||||
currentPattern++
|
currentPattern++
|
||||||
val response = responseBuffer.copyOf(responseLength)
|
val response = responseBuffer.copyOf(responseLength)
|
||||||
|
|
||||||
Log.d(TAG, "Generated handshake response: ${response.size} bytes, action still: ${handshakeStateLocal.getAction()} currentPattern: $currentPattern")
|
|
||||||
completeHandshake()
|
completeHandshake()
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
HandshakeState.SPLIT -> {
|
HandshakeState.SPLIT -> {
|
||||||
// Handshake complete, split into transport keys
|
// Handshake complete, split into transport keys
|
||||||
completeHandshake()
|
completeHandshake()
|
||||||
Log.d(TAG, "SPLIT ✅ XX handshake completed with $peerID")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
HandshakeState.FAILED -> {
|
HandshakeState.FAILED -> {
|
||||||
throw Exception("Handshake failed - Noise library reported FAILED state")
|
throw Exception("Handshake failed - Noise library reported FAILED state")
|
||||||
}
|
}
|
||||||
|
|
||||||
HandshakeState.READ_MESSAGE -> {
|
else -> null
|
||||||
// Noise library expects us to read another message
|
|
||||||
Log.d(TAG, "Handshake waiting for next message from $peerID")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Log.d(TAG, "Handshake action: $action - no immediate action needed")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -448,8 +384,6 @@ class NoiseSession(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Completing XX handshake with $peerID")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val activeHandshake = handshakeState ?: throw NoiseSessionError.HandshakeFailed
|
val activeHandshake = handshakeState ?: throw NoiseSessionError.HandshakeFailed
|
||||||
|
|
||||||
@ -466,7 +400,6 @@ class NoiseSession(
|
|||||||
throw NoiseSessionError.PeerIdentityMismatch(peerID, derivedPeerID)
|
throw NoiseSessionError.PeerIdentityMismatch(peerID, derivedPeerID)
|
||||||
}
|
}
|
||||||
remoteStaticPublicKey = authenticatedRemoteKey
|
remoteStaticPublicKey = authenticatedRemoteKey
|
||||||
Log.d(TAG, "Remote static public key is bound to $peerID")
|
|
||||||
|
|
||||||
// Only a bound remote identity may derive transport ciphers.
|
// Only a bound remote identity may derive transport ciphers.
|
||||||
val cipherPair = activeHandshake.split()
|
val cipherPair = activeHandshake.split()
|
||||||
@ -492,8 +425,7 @@ class NoiseSession(
|
|||||||
replayWindow = ByteArray(REPLAY_WINDOW_BYTES)
|
replayWindow = ByteArray(REPLAY_WINDOW_BYTES)
|
||||||
|
|
||||||
state = NoiseSessionState.Established
|
state = NoiseSessionState.Established
|
||||||
Log.d(TAG, "Handshake completed with $peerID as isInitiator: $isInitiator - transport keys derived")
|
Log.i(TAG, "Handshake established with $peerID (${if (isInitiator) "initiator" else "responder"})")
|
||||||
Log.d(TAG, "✅ XX handshake completed with $peerID")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state = NoiseSessionState.Failed(e)
|
state = NoiseSessionState.Failed(e)
|
||||||
Log.e(TAG, "Failed to complete handshake: ${e.message}")
|
Log.e(TAG, "Failed to complete handshake: ${e.message}")
|
||||||
@ -556,20 +488,13 @@ class NoiseSession(
|
|||||||
|
|
||||||
// Log high nonce values that might indicate issues
|
// Log high nonce values that might indicate issues
|
||||||
if (currentNonce > HIGH_NONCE_WARNING_THRESHOLD) {
|
if (currentNonce > HIGH_NONCE_WARNING_THRESHOLD) {
|
||||||
Log.w(TAG, "High nonce value detected: $currentNonce - consider rekeying")
|
Log.w(TAG, "High send nonce $currentNonce for $peerID - rekey recommended")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✅ ANDROID ENCRYPT: ${data.size} → ${combinedPayload.size} bytes (nonce: $currentNonce, ciphertextLength+TAG: ${ciphertextLength}) for $peerID (msg #$messagesSent, role: ${if (isInitiator) "INITIATOR" else "RESPONDER"})")
|
|
||||||
return combinedPayload
|
return combinedPayload
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Real encryption failed - exception: ${e.message}")
|
Log.e(TAG, "Encryption failed for $peerID: ${e.message}")
|
||||||
|
|
||||||
// ENHANCED: Log cipher state for debugging
|
|
||||||
if (sendCipher != null) {
|
|
||||||
Log.e(TAG, "Send cipher state: ${sendCipher!!.javaClass.simpleName}")
|
|
||||||
}
|
|
||||||
|
|
||||||
throw SessionError.EncryptionFailed
|
throw SessionError.EncryptionFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -630,23 +555,13 @@ class NoiseSession(
|
|||||||
|
|
||||||
// Log high nonce values that might indicate issues
|
// Log high nonce values that might indicate issues
|
||||||
if (extractedNonce > HIGH_NONCE_WARNING_THRESHOLD) {
|
if (extractedNonce > HIGH_NONCE_WARNING_THRESHOLD) {
|
||||||
Log.w(TAG, "High nonce value detected: $extractedNonce - consider rekeying")
|
Log.w(TAG, "High receive nonce $extractedNonce for $peerID - rekey recommended")
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = plaintext.copyOf(plaintextLength)
|
return plaintext.copyOf(plaintextLength)
|
||||||
Log.d(TAG, "✅ ANDROID DECRYPT: ${combinedPayload.size} → ${result.size} bytes from $peerID (nonce: $extractedNonce, highest: $highestReceivedNonce, role: ${if (isInitiator) "INITIATOR" else "RESPONDER"})")
|
|
||||||
return result
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Decryption failed - exception: ${e.message}")
|
Log.w(TAG, "Decrypt failed for $peerID: ${e.message} (state=$state, highestNonce=$highestReceivedNonce)")
|
||||||
|
|
||||||
// ENHANCED: Log cipher state and session details for debugging
|
|
||||||
if (receiveCipher != null) {
|
|
||||||
Log.e(TAG, "Receive cipher state: ${receiveCipher!!.javaClass.simpleName}")
|
|
||||||
}
|
|
||||||
Log.e(TAG, "Session state: $state, highest received nonce: $highestReceivedNonce")
|
|
||||||
Log.e(TAG, "Input data size: ${combinedPayload.size} bytes")
|
|
||||||
|
|
||||||
throw SessionError.DecryptionFailed
|
throw SessionError.DecryptionFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -747,9 +662,7 @@ class NoiseSession(
|
|||||||
if (state !is NoiseSessionState.Failed) {
|
if (state !is NoiseSessionState.Failed) {
|
||||||
state = NoiseSessionState.Failed(Exception("Session destroyed"))
|
state = NoiseSessionState.Failed(Exception("Session destroyed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Session destroyed for $peerID")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error during session cleanup: ${e.message}")
|
Log.w(TAG, "Error during session cleanup: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,6 @@ class NoiseSessionManager(
|
|||||||
fun addSession(peerID: String, session: NoiseSession) {
|
fun addSession(peerID: String, session: NoiseSession) {
|
||||||
val previous = sessions.put(peerID, session)
|
val previous = sessions.put(peerID, session)
|
||||||
if (previous != null && previous !== session) previous.destroy()
|
if (previous != null && previous !== session) previous.destroy()
|
||||||
Log.d(TAG, "Added new session for $peerID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +90,6 @@ class NoiseSessionManager(
|
|||||||
fun removeSession(peerID: String) {
|
fun removeSession(peerID: String) {
|
||||||
sessions.remove(peerID)?.destroy()
|
sessions.remove(peerID)?.destroy()
|
||||||
responderCandidates.remove(peerID)?.destroy()
|
responderCandidates.remove(peerID)?.destroy()
|
||||||
Log.d(TAG, "Removed session for $peerID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,15 +97,12 @@ class NoiseSessionManager(
|
|||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun initiateHandshake(peerID: String, replaceEstablished: Boolean = false): ByteArray? {
|
fun initiateHandshake(peerID: String, replaceEstablished: Boolean = false): ByteArray? {
|
||||||
Log.d(TAG, "initiateHandshake($peerID)")
|
|
||||||
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val existing = getSession(peerID)
|
val existing = getSession(peerID)
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
when {
|
when {
|
||||||
existing.isEstablished() -> {
|
existing.isEstablished() -> {
|
||||||
if (!replaceEstablished) {
|
if (!replaceEstablished) {
|
||||||
Log.d(TAG, "Handshake already established with $peerID, skipping initiate")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val candidate = createSession(peerID, isInitiator = true)
|
val candidate = createSession(peerID, isInitiator = true)
|
||||||
@ -123,10 +118,9 @@ class NoiseSessionManager(
|
|||||||
}
|
}
|
||||||
existing.isHandshaking() -> {
|
existing.isHandshaking() -> {
|
||||||
if (!isHandshakeStale(existing, now)) {
|
if (!isHandshakeStale(existing, now)) {
|
||||||
Log.d(TAG, "Handshake already in progress with $peerID, not restarting")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Handshake with $peerID is stale; restarting")
|
Log.d(TAG, "Restarting stale handshake with $peerID")
|
||||||
removeSession(peerID)
|
removeSession(peerID)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@ -142,13 +136,10 @@ class NoiseSessionManager(
|
|||||||
localStaticPrivateKey = localStaticPrivateKey,
|
localStaticPrivateKey = localStaticPrivateKey,
|
||||||
localStaticPublicKey = localStaticPublicKey
|
localStaticPublicKey = localStaticPublicKey
|
||||||
)
|
)
|
||||||
Log.d(TAG, "Storing new INITIATOR session for $peerID")
|
|
||||||
addSession(peerID, session)
|
addSession(peerID, session)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val handshakeData = session.startHandshake()
|
return session.startHandshake()
|
||||||
Log.d(TAG, "Started handshake with $peerID as INITIATOR")
|
|
||||||
return handshakeData
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (sessions.remove(peerID, session)) session.destroy()
|
if (sessions.remove(peerID, session)) session.destroy()
|
||||||
throw e
|
throw e
|
||||||
@ -167,8 +158,6 @@ class NoiseSessionManager(
|
|||||||
peerID: String,
|
peerID: String,
|
||||||
message: ByteArray
|
message: ByteArray
|
||||||
): NoiseHandshakeProcessingResult {
|
): NoiseHandshakeProcessingResult {
|
||||||
Log.d(TAG, "processHandshakeMessage($peerID, ${message.size} bytes)")
|
|
||||||
|
|
||||||
var activeSession: NoiseSession? = null
|
var activeSession: NoiseSession? = null
|
||||||
var isReplacementCandidate = false
|
var isReplacementCandidate = false
|
||||||
var establishedRemoteKey: ByteArray? = null
|
var establishedRemoteKey: ByteArray? = null
|
||||||
@ -182,19 +171,12 @@ class NoiseSessionManager(
|
|||||||
if (existingCandidate.isInitiatorRole()) {
|
if (existingCandidate.isInitiatorRole()) {
|
||||||
val shouldYield = localPeerID > peerID
|
val shouldYield = localPeerID > peerID
|
||||||
if (!shouldYield) {
|
if (!shouldYield) {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"Replacement handshake collision with $peerID; keeping initiator role"
|
|
||||||
)
|
|
||||||
return NoiseHandshakeProcessingResult(
|
return NoiseHandshakeProcessingResult(
|
||||||
response = null,
|
response = null,
|
||||||
establishedNow = false
|
establishedNow = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Log.d(
|
Log.d(TAG, "Replacement collision with $peerID; yielding to responder")
|
||||||
TAG,
|
|
||||||
"Replacement handshake collision with $peerID; yielding to responder role"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
responderCandidates.remove(peerID, existingCandidate)
|
responderCandidates.remove(peerID, existingCandidate)
|
||||||
existingCandidate.destroy()
|
existingCandidate.destroy()
|
||||||
@ -216,25 +198,19 @@ class NoiseSessionManager(
|
|||||||
) {
|
) {
|
||||||
val shouldYield = localPeerID > peerID
|
val shouldYield = localPeerID > peerID
|
||||||
if (shouldYield) {
|
if (shouldYield) {
|
||||||
Log.d(TAG, "Handshake collision with $peerID; yielding to responder role")
|
Log.d(TAG, "Handshake collision with $peerID; yielding to responder")
|
||||||
if (sessions.remove(peerID, session)) session.destroy()
|
if (sessions.remove(peerID, session)) session.destroy()
|
||||||
session = null
|
session = null
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Handshake collision with $peerID; keeping initiator role")
|
|
||||||
return NoiseHandshakeProcessingResult(response = null, establishedNow = false)
|
return NoiseHandshakeProcessingResult(response = null, establishedNow = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeSession = when {
|
activeSession = when {
|
||||||
session == null -> {
|
session == null -> {
|
||||||
Log.d(TAG, "Creating new RESPONDER session for $peerID")
|
|
||||||
createSession(peerID, isInitiator = false).also { sessions[peerID] = it }
|
createSession(peerID, isInitiator = false).also { sessions[peerID] = it }
|
||||||
}
|
}
|
||||||
session.isEstablished() -> {
|
session.isEstablished() -> {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"Validating replacement handshake for $peerID while preserving active session"
|
|
||||||
)
|
|
||||||
isReplacementCandidate = true
|
isReplacementCandidate = true
|
||||||
createSession(peerID, isInitiator = false).also {
|
createSession(peerID, isInitiator = false).also {
|
||||||
responderCandidates[peerID] = it
|
responderCandidates[peerID] = it
|
||||||
@ -276,7 +252,6 @@ class NoiseSessionManager(
|
|||||||
|
|
||||||
establishedRemoteKey = remoteStaticKey
|
establishedRemoteKey = remoteStaticKey
|
||||||
establishedSessionToken = sessionToken
|
establishedSessionToken = sessionToken
|
||||||
Log.d(TAG, "✅ Session ESTABLISHED with bound identity $peerID")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val session = activeSession
|
val session = activeSession
|
||||||
@ -368,9 +343,7 @@ class NoiseSessionManager(
|
|||||||
* Check if session is established with peer
|
* Check if session is established with peer
|
||||||
*/
|
*/
|
||||||
fun hasEstablishedSession(peerID: String): Boolean {
|
fun hasEstablishedSession(peerID: String): Boolean {
|
||||||
val hasSession = getSession(peerID)?.isEstablished() ?: false
|
return getSession(peerID)?.isEstablished() ?: false
|
||||||
Log.d(TAG, "hasEstablishedSession($peerID): $hasSession")
|
|
||||||
return hasSession
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -22,8 +22,6 @@
|
|||||||
|
|
||||||
package com.bitchat.android.noise.southernstorm.protocol;
|
package com.bitchat.android.noise.southernstorm.protocol;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@ -35,8 +33,6 @@ import javax.crypto.ShortBufferException;
|
|||||||
*/
|
*/
|
||||||
public class HandshakeState implements Destroyable {
|
public class HandshakeState implements Destroyable {
|
||||||
|
|
||||||
private static final String TAG = "AndroidHandshake";
|
|
||||||
|
|
||||||
private SymmetricState symmetric;
|
private SymmetricState symmetric;
|
||||||
private boolean isInitiator;
|
private boolean isInitiator;
|
||||||
private DHState localKeyPair;
|
private DHState localKeyPair;
|
||||||
@ -467,17 +463,6 @@ public class HandshakeState implements Destroyable {
|
|||||||
// Empty value for when the prologue is not supplied.
|
// Empty value for when the prologue is not supplied.
|
||||||
private static final byte[] emptyPrologue = new byte [0];
|
private static final byte[] emptyPrologue = new byte [0];
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a byte array to hex string for logging (matching iOS hex format)
|
|
||||||
*/
|
|
||||||
private static String bytesToHex(byte[] bytes) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (byte b : bytes) {
|
|
||||||
sb.append(String.format("%02x", b));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the handshake running.
|
* Starts the handshake running.
|
||||||
*
|
*
|
||||||
@ -524,27 +509,18 @@ public class HandshakeState implements Destroyable {
|
|||||||
throw new IllegalStateException("Pre-shared key required");
|
throw new IllegalStateException("Pre-shared key required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the symmetric state BEFORE any mixing operations (matching iOS)
|
|
||||||
Log.d(TAG, "=== ANDROID HANDSHAKE START - INITIAL STATE ===");
|
|
||||||
Log.d(TAG, "Protocol: " + symmetric.getProtocolName());
|
|
||||||
Log.d(TAG, "Role: " + (isInitiator ? "INITIATOR" : "RESPONDER"));
|
|
||||||
Log.d(TAG, "Initial symmetric hash: " + bytesToHex(symmetric.getHandshakeHash()));
|
|
||||||
|
|
||||||
// Hash the prologue value.
|
// Hash the prologue value.
|
||||||
Log.d(TAG, "Mixing empty prologue");
|
|
||||||
if (prologue != null)
|
if (prologue != null)
|
||||||
symmetric.mixHash(prologue, 0, prologue.length);
|
symmetric.mixHash(prologue, 0, prologue.length);
|
||||||
else
|
else
|
||||||
symmetric.mixHash(emptyPrologue, 0, 0);
|
symmetric.mixHash(emptyPrologue, 0, 0);
|
||||||
Log.d(TAG, "Hash after empty prologue: " + bytesToHex(symmetric.getHandshakeHash()));
|
|
||||||
|
|
||||||
// Hash the pre-shared key into the chaining key and handshake hash.
|
// Hash the pre-shared key into the chaining key and handshake hash.
|
||||||
if (preSharedKey != null)
|
if (preSharedKey != null)
|
||||||
symmetric.mixPreSharedKey(preSharedKey);
|
symmetric.mixPreSharedKey(preSharedKey);
|
||||||
|
|
||||||
// Mix the pre-supplied public keys into the handshake hash.
|
// Mix the pre-supplied public keys into the handshake hash.
|
||||||
if (isInitiator) {
|
if (isInitiator) {
|
||||||
Log.d(TAG, "XX pattern - no pre-message keys to mix");
|
|
||||||
if ((requirements & LOCAL_PREMSG) != 0)
|
if ((requirements & LOCAL_PREMSG) != 0)
|
||||||
symmetric.mixPublicKey(localKeyPair);
|
symmetric.mixPublicKey(localKeyPair);
|
||||||
if ((requirements & FALLBACK_PREMSG) != 0) {
|
if ((requirements & FALLBACK_PREMSG) != 0) {
|
||||||
@ -557,7 +533,6 @@ public class HandshakeState implements Destroyable {
|
|||||||
if ((requirements & REMOTE_PREMSG) != 0)
|
if ((requirements & REMOTE_PREMSG) != 0)
|
||||||
symmetric.mixPublicKey(remotePublicKey);
|
symmetric.mixPublicKey(remotePublicKey);
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "XX pattern - no pre-message keys to mix");
|
|
||||||
if ((requirements & REMOTE_PREMSG) != 0)
|
if ((requirements & REMOTE_PREMSG) != 0)
|
||||||
symmetric.mixPublicKey(remotePublicKey);
|
symmetric.mixPublicKey(remotePublicKey);
|
||||||
if ((requirements & FALLBACK_PREMSG) != 0) {
|
if ((requirements & FALLBACK_PREMSG) != 0) {
|
||||||
@ -571,11 +546,6 @@ public class HandshakeState implements Destroyable {
|
|||||||
symmetric.mixPublicKey(localKeyPair);
|
symmetric.mixPublicKey(localKeyPair);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log final state after all initialization (matching iOS)
|
|
||||||
Log.d(TAG, "=== ANDROID HANDSHAKE START - FINAL STATE ===");
|
|
||||||
Log.d(TAG, "Final symmetric hash after mixPreMessageKeys(): " + bytesToHex(symmetric.getHandshakeHash()));
|
|
||||||
Log.d(TAG, "===========================================");
|
|
||||||
|
|
||||||
// The handshake has officially started - set the first action.
|
// The handshake has officially started - set the first action.
|
||||||
if (isInitiator)
|
if (isInitiator)
|
||||||
action = WRITE_MESSAGE;
|
action = WRITE_MESSAGE;
|
||||||
|
|||||||
@ -22,8 +22,6 @@
|
|||||||
|
|
||||||
package com.bitchat.android.noise.southernstorm.protocol;
|
package com.bitchat.android.noise.southernstorm.protocol;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.security.DigestException;
|
import java.security.DigestException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@ -37,9 +35,7 @@ import javax.crypto.ShortBufferException;
|
|||||||
* Symmetric state for helping manage a Noise handshake.
|
* Symmetric state for helping manage a Noise handshake.
|
||||||
*/
|
*/
|
||||||
class SymmetricState implements Destroyable {
|
class SymmetricState implements Destroyable {
|
||||||
|
|
||||||
private static final String TAG = "AndroidSymmetric";
|
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private CipherState cipher;
|
private CipherState cipher;
|
||||||
private MessageDigest hash;
|
private MessageDigest hash;
|
||||||
@ -47,17 +43,6 @@ class SymmetricState implements Destroyable {
|
|||||||
private byte[] h;
|
private byte[] h;
|
||||||
private byte[] prev_h;
|
private byte[] prev_h;
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a byte array to hex string for logging (matching iOS hex format)
|
|
||||||
*/
|
|
||||||
private static String bytesToHex(byte[] bytes) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (byte b : bytes) {
|
|
||||||
sb.append(String.format("%02x", b));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new symmetric state object.
|
* Constructs a new symmetric state object.
|
||||||
*
|
*
|
||||||
@ -94,14 +79,6 @@ class SymmetricState implements Destroyable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
System.arraycopy(h, 0, ck, 0, hashLength);
|
System.arraycopy(h, 0, ck, 0, hashLength);
|
||||||
|
|
||||||
// LOGGING: Initial symmetric state after protocol name initialization (matching iOS)
|
|
||||||
Log.d(TAG, "=== ANDROID SYMMETRIC STATE INITIALIZED ===");
|
|
||||||
Log.d(TAG, "Protocol: " + protocolName);
|
|
||||||
Log.d(TAG, "Initial hash (h): " + bytesToHex(h));
|
|
||||||
Log.d(TAG, "Initial chaining key (ck): " + bytesToHex(ck));
|
|
||||||
Log.d(TAG, "Hash length: " + h.length);
|
|
||||||
Log.d(TAG, "=========================================");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,14 +111,6 @@ class SymmetricState implements Destroyable {
|
|||||||
*/
|
*/
|
||||||
public void mixKey(byte[] data, int offset, int length)
|
public void mixKey(byte[] data, int offset, int length)
|
||||||
{
|
{
|
||||||
// LOGGING: Before mixKey operation (matching iOS)
|
|
||||||
byte[] inputData = new byte[length];
|
|
||||||
System.arraycopy(data, offset, inputData, 0, length);
|
|
||||||
Log.d(TAG, "*** Android mixKey() BEFORE ***");
|
|
||||||
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData));
|
|
||||||
Log.d(TAG, "Current CK: " + bytesToHex(ck));
|
|
||||||
Log.d(TAG, "Current Hash: " + bytesToHex(h));
|
|
||||||
|
|
||||||
int keyLength = cipher.getKeyLength();
|
int keyLength = cipher.getKeyLength();
|
||||||
byte[] tempKey = new byte [keyLength];
|
byte[] tempKey = new byte [keyLength];
|
||||||
try {
|
try {
|
||||||
@ -150,12 +119,6 @@ class SymmetricState implements Destroyable {
|
|||||||
} finally {
|
} finally {
|
||||||
Noise.destroy(tempKey);
|
Noise.destroy(tempKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOGGING: After mixKey operation (matching iOS)
|
|
||||||
Log.d(TAG, "*** Android mixKey() AFTER ***");
|
|
||||||
Log.d(TAG, "New CK: " + bytesToHex(ck));
|
|
||||||
Log.d(TAG, "Hash unchanged: " + bytesToHex(h));
|
|
||||||
Log.d(TAG, "Cipher now has key: " + (cipher.getMACLength() > 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -167,18 +130,7 @@ class SymmetricState implements Destroyable {
|
|||||||
*/
|
*/
|
||||||
public void mixHash(byte[] data, int offset, int length)
|
public void mixHash(byte[] data, int offset, int length)
|
||||||
{
|
{
|
||||||
// LOGGING: Before mixHash operation (matching iOS)
|
|
||||||
byte[] inputData = new byte[length];
|
|
||||||
System.arraycopy(data, offset, inputData, 0, length);
|
|
||||||
Log.d(TAG, "*** Android mixHash() BEFORE ***");
|
|
||||||
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData));
|
|
||||||
Log.d(TAG, "Current Hash: " + bytesToHex(h));
|
|
||||||
|
|
||||||
hashTwo(h, 0, h.length, data, offset, length, h, 0, h.length);
|
hashTwo(h, 0, h.length, data, offset, length, h, 0, h.length);
|
||||||
|
|
||||||
// LOGGING: After mixHash operation (matching iOS)
|
|
||||||
Log.d(TAG, "*** Android mixHash() AFTER ***");
|
|
||||||
Log.d(TAG, "New Hash: " + bytesToHex(h));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -406,7 +406,7 @@ class LocationNotesManager private constructor() {
|
|||||||
val currentNotes = _notes.value ?: emptyList()
|
val currentNotes = _notes.value ?: emptyList()
|
||||||
_notes.value = (currentNotes + note).sortedByDescending { it.createdAt }
|
_notes.value = (currentNotes + note).sortedByDescending { it.createdAt }
|
||||||
|
|
||||||
Log.d(TAG, "📥 Added note: ${note.displayName} - ${note.content.take(50)}")
|
Log.d(TAG, "Added note from ${note.displayName}")
|
||||||
|
|
||||||
// Trim if exceeds max
|
// Trim if exceeds max
|
||||||
if (noteIDs.size > MAX_NOTES_IN_MEMORY) {
|
if (noteIDs.size > MAX_NOTES_IN_MEMORY) {
|
||||||
|
|||||||
@ -274,8 +274,6 @@ object NostrProtocol {
|
|||||||
giftWrap: NostrEvent,
|
giftWrap: NostrEvent,
|
||||||
recipientPrivateKey: String
|
recipientPrivateKey: String
|
||||||
): NostrEvent? {
|
): NostrEvent? {
|
||||||
Log.d(TAG, "Unwrapping gift wrap; content prefix='${giftWrap.content.take(3)}' length=${giftWrap.content.length}")
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val decrypted = NostrCrypto.decryptNIP44(
|
val decrypted = NostrCrypto.decryptNIP44(
|
||||||
ciphertext = giftWrap.content,
|
ciphertext = giftWrap.content,
|
||||||
|
|||||||
@ -139,7 +139,7 @@ class NostrRelayManager private constructor() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
geohashToRelays[geohash] = selected
|
geohashToRelays[geohash] = selected
|
||||||
Log.i(TAG, "🌐 Geohash $geohash using ${selected.size} relays: ${selected.joinToString()}")
|
Log.d(TAG, "Geohash $geohash using ${selected.size} relays")
|
||||||
ensureConnectionsFor(selected)
|
ensureConnectionsFor(selected)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to ensure relays for $geohash: ${e.message}")
|
Log.e(TAG, "Failed to ensure relays for $geohash: ${e.message}")
|
||||||
@ -166,7 +166,6 @@ class NostrRelayManager private constructor() {
|
|||||||
): String {
|
): String {
|
||||||
ensureGeohashRelaysConnected(geohash, nRelays, includeDefaults)
|
ensureGeohashRelaysConnected(geohash, nRelays, includeDefaults)
|
||||||
val relayUrls = getRelaysForGeohash(geohash)
|
val relayUrls = getRelaysForGeohash(geohash)
|
||||||
Log.d(TAG, "📡 Subscribing id=$id for geohash=$geohash on ${relayUrls.size} relays")
|
|
||||||
return subscribe(
|
return subscribe(
|
||||||
filter = filter,
|
filter = filter,
|
||||||
id = id,
|
id = id,
|
||||||
@ -191,7 +190,6 @@ class NostrRelayManager private constructor() {
|
|||||||
sendEvent(event, Companion.defaultRelays())
|
sendEvent(event, Companion.defaultRelays())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.v(TAG, "📤 Sending event kind=${event.kind} to ${relayUrls.size} relays for geohash=$geohash")
|
|
||||||
sendEvent(event, relayUrls)
|
sendEvent(event, relayUrls)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,7 +227,6 @@ class NostrRelayManager private constructor() {
|
|||||||
relaysList.addAll(defaultRelayUrls.map { Relay(it) })
|
relaysList.addAll(defaultRelayUrls.map { Relay(it) })
|
||||||
_relays.value = relaysList.toList()
|
_relays.value = relaysList.toList()
|
||||||
updateConnectionStatus()
|
updateConnectionStatus()
|
||||||
Log.d(TAG, "✅ NostrRelayManager initialized with ${relaysList.size} default relays")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize NostrRelayManager: ${e.message}", e)
|
Log.e(TAG, "Failed to initialize NostrRelayManager: ${e.message}", e)
|
||||||
// Initialize with empty list as fallback
|
// Initialize with empty list as fallback
|
||||||
@ -242,7 +239,7 @@ class NostrRelayManager private constructor() {
|
|||||||
* Connect to all configured relays
|
* Connect to all configured relays
|
||||||
*/
|
*/
|
||||||
fun connect() {
|
fun connect() {
|
||||||
Log.d(TAG, "🌐 Connecting to ${relaysList.size} Nostr relays")
|
Log.i(TAG, "Connecting to ${relaysList.size} Nostr relays")
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
relaysList.forEach { relay ->
|
relaysList.forEach { relay ->
|
||||||
@ -260,7 +257,7 @@ class NostrRelayManager private constructor() {
|
|||||||
* Disconnect from all relays
|
* Disconnect from all relays
|
||||||
*/
|
*/
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
Log.d(TAG, "Disconnecting from all relays")
|
Log.i(TAG, "Disconnecting from all Nostr relays")
|
||||||
|
|
||||||
// Stop subscription validation
|
// Stop subscription validation
|
||||||
stopSubscriptionValidation()
|
stopSubscriptionValidation()
|
||||||
@ -318,9 +315,7 @@ class NostrRelayManager private constructor() {
|
|||||||
|
|
||||||
activeSubscriptions[id] = subscriptionInfo
|
activeSubscriptions[id] = subscriptionInfo
|
||||||
messageHandlers[id] = handler
|
messageHandlers[id] = handler
|
||||||
|
|
||||||
Log.d(TAG, "📡 Subscribing to Nostr filter id=$id ${filter.getDebugDescription()}")
|
|
||||||
|
|
||||||
// Send subscription to appropriate relays
|
// Send subscription to appropriate relays
|
||||||
sendSubscriptionToRelays(subscriptionInfo)
|
sendSubscriptionToRelays(subscriptionInfo)
|
||||||
|
|
||||||
@ -333,10 +328,7 @@ class NostrRelayManager private constructor() {
|
|||||||
private fun sendSubscriptionToRelays(subscriptionInfo: SubscriptionInfo) {
|
private fun sendSubscriptionToRelays(subscriptionInfo: SubscriptionInfo) {
|
||||||
val request = NostrRequest.Subscribe(subscriptionInfo.id, listOf(subscriptionInfo.filter))
|
val request = NostrRequest.Subscribe(subscriptionInfo.id, listOf(subscriptionInfo.filter))
|
||||||
val message = gson.toJson(request, NostrRequest::class.java)
|
val message = gson.toJson(request, NostrRequest::class.java)
|
||||||
|
|
||||||
// DEBUG: Log the actual serialized message format
|
|
||||||
Log.v(TAG, "🔍 DEBUG: Serialized subscription message: $message")
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val targetRelays = subscriptionInfo.targetRelayUrls?.toList() ?: connections.keys.toList()
|
val targetRelays = subscriptionInfo.targetRelayUrls?.toList() ?: connections.keys.toList()
|
||||||
|
|
||||||
@ -349,21 +341,17 @@ class NostrRelayManager private constructor() {
|
|||||||
// Track subscription for this relay
|
// Track subscription for this relay
|
||||||
val currentSubs = subscriptions[relayUrl] ?: emptySet()
|
val currentSubs = subscriptions[relayUrl] ?: emptySet()
|
||||||
subscriptions[relayUrl] = currentSubs + subscriptionInfo.id
|
subscriptions[relayUrl] = currentSubs + subscriptionInfo.id
|
||||||
|
|
||||||
Log.v(TAG, "✅ Subscription '${subscriptionInfo.id}' sent to relay: $relayUrl")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "❌ Failed to send subscription to $relayUrl: WebSocket send failed")
|
Log.w(TAG, "Failed to send subscription to $relayUrl: WebSocket send failed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to send subscription to $relayUrl: ${e.message}")
|
Log.e(TAG, "Failed to send subscription to $relayUrl: ${e.message}")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.v(TAG, "⏳ Relay $relayUrl not connected, subscription will be sent on reconnection")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connections.isEmpty()) {
|
if (connections.isEmpty()) {
|
||||||
Log.w(TAG, "⚠️ No relay connections available for subscription, will retry on reconnection")
|
Log.w(TAG, "No relay connections available for subscription, will retry on reconnection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,12 +365,10 @@ class NostrRelayManager private constructor() {
|
|||||||
messageHandlers.remove(id)
|
messageHandlers.remove(id)
|
||||||
|
|
||||||
if (subscriptionInfo == null) {
|
if (subscriptionInfo == null) {
|
||||||
Log.w(TAG, "⚠️ Attempted to unsubscribe from unknown subscription: $id")
|
Log.w(TAG, "Attempted to unsubscribe from unknown subscription: $id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "🚫 Unsubscribing from subscription: $id")
|
|
||||||
|
|
||||||
val request = NostrRequest.Close(id)
|
val request = NostrRequest.Close(id)
|
||||||
val message = gson.toJson(request, NostrRequest::class.java)
|
val message = gson.toJson(request, NostrRequest::class.java)
|
||||||
|
|
||||||
@ -393,7 +379,6 @@ class NostrRelayManager private constructor() {
|
|||||||
try {
|
try {
|
||||||
webSocket.send(message)
|
webSocket.send(message)
|
||||||
subscriptions[relayUrl] = currentSubs - id
|
subscriptions[relayUrl] = currentSubs - id
|
||||||
Log.v(TAG, "Unsubscribed '$id' from relay: $relayUrl")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to unsubscribe from $relayUrl: ${e.message}")
|
Log.e(TAG, "Failed to unsubscribe from $relayUrl: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -445,8 +430,6 @@ class NostrRelayManager private constructor() {
|
|||||||
* Useful for ensuring subscription consistency after network issues
|
* Useful for ensuring subscription consistency after network issues
|
||||||
*/
|
*/
|
||||||
fun reestablishAllSubscriptions() {
|
fun reestablishAllSubscriptions() {
|
||||||
Log.d(TAG, "🔄 Force re-establishing all ${activeSubscriptions.size} active subscriptions")
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
connections.forEach { (relayUrl, webSocket) ->
|
connections.forEach { (relayUrl, webSocket) ->
|
||||||
restoreSubscriptionsForRelay(relayUrl, webSocket)
|
restoreSubscriptionsForRelay(relayUrl, webSocket)
|
||||||
@ -473,7 +456,7 @@ class NostrRelayManager private constructor() {
|
|||||||
messageQueue.clear()
|
messageQueue.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "🧹 Cleared all Nostr subscriptions and routing caches")
|
Log.i(TAG, "Cleared all Nostr subscriptions and routing caches")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to clear subscriptions: ${e.message}")
|
Log.e(TAG, "Failed to clear subscriptions: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -498,7 +481,6 @@ class NostrRelayManager private constructor() {
|
|||||||
*/
|
*/
|
||||||
fun clearDeduplicationCache() {
|
fun clearDeduplicationCache() {
|
||||||
eventDeduplicator.clear()
|
eventDeduplicator.clear()
|
||||||
Log.i(TAG, "🧹 Cleared event deduplication cache")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -570,7 +552,7 @@ class NostrRelayManager private constructor() {
|
|||||||
try {
|
try {
|
||||||
val report = validateSubscriptionConsistency()
|
val report = validateSubscriptionConsistency()
|
||||||
if (!report.isConsistent && report.connectedRelayCount > 0) {
|
if (!report.isConsistent && report.connectedRelayCount > 0) {
|
||||||
Log.w(TAG, "⚠️ Subscription inconsistencies detected: ${report.inconsistencies}")
|
Log.w(TAG, "Subscription inconsistencies detected: ${report.inconsistencies}")
|
||||||
|
|
||||||
// Auto-repair: re-establish subscriptions for relays with missing ones
|
// Auto-repair: re-establish subscriptions for relays with missing ones
|
||||||
connections.forEach { (relayUrl, webSocket) ->
|
connections.forEach { (relayUrl, webSocket) ->
|
||||||
@ -582,7 +564,7 @@ class NostrRelayManager private constructor() {
|
|||||||
|
|
||||||
val missingSubs = expectedSubs - currentSubs
|
val missingSubs = expectedSubs - currentSubs
|
||||||
if (missingSubs.isNotEmpty()) {
|
if (missingSubs.isNotEmpty()) {
|
||||||
Log.i(TAG, "🔧 Auto-repairing ${missingSubs.size} missing subscriptions for $relayUrl")
|
Log.i(TAG, "Auto-repairing ${missingSubs.size} missing subscriptions for $relayUrl")
|
||||||
restoreSubscriptionsForRelay(relayUrl, webSocket)
|
restoreSubscriptionsForRelay(relayUrl, webSocket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -592,8 +574,6 @@ class NostrRelayManager private constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "🔄 Started periodic subscription validation (${SUBSCRIPTION_VALIDATION_INTERVAL / 1000}s interval)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -602,7 +582,6 @@ class NostrRelayManager private constructor() {
|
|||||||
private fun stopSubscriptionValidation() {
|
private fun stopSubscriptionValidation() {
|
||||||
subscriptionValidationJob?.cancel()
|
subscriptionValidationJob?.cancel()
|
||||||
subscriptionValidationJob = null
|
subscriptionValidationJob = null
|
||||||
Log.v(TAG, "⏹️ Stopped subscription validation")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
@ -612,9 +591,7 @@ class NostrRelayManager private constructor() {
|
|||||||
if (connections.containsKey(urlString)) {
|
if (connections.containsKey(urlString)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.v(TAG, "Attempting to connect to Nostr relay: $urlString")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(urlString)
|
.url(urlString)
|
||||||
@ -624,7 +601,7 @@ class NostrRelayManager private constructor() {
|
|||||||
connections[urlString] = webSocket
|
connections[urlString] = webSocket
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to create WebSocket connection to $urlString: ${e.message}")
|
Log.e(TAG, "Failed to create WebSocket connection to $urlString: ${e.message}")
|
||||||
handleDisconnection(urlString, e)
|
handleDisconnection(urlString, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -633,9 +610,7 @@ class NostrRelayManager private constructor() {
|
|||||||
try {
|
try {
|
||||||
val request = NostrRequest.Event(event)
|
val request = NostrRequest.Event(event)
|
||||||
val message = gson.toJson(request, NostrRequest::class.java)
|
val message = gson.toJson(request, NostrRequest::class.java)
|
||||||
|
|
||||||
Log.v(TAG, "📤 Sending Nostr event (kind: ${event.kind}) to relay: $relayUrl")
|
|
||||||
|
|
||||||
val success = webSocket.send(message)
|
val success = webSocket.send(message)
|
||||||
if (success) {
|
if (success) {
|
||||||
// Update relay stats
|
// Update relay stats
|
||||||
@ -643,10 +618,10 @@ class NostrRelayManager private constructor() {
|
|||||||
relay?.messagesSent = (relay?.messagesSent ?: 0) + 1
|
relay?.messagesSent = (relay?.messagesSent ?: 0) + 1
|
||||||
updateRelaysList()
|
updateRelaysList()
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "❌ Failed to send event to $relayUrl: WebSocket send failed")
|
Log.e(TAG, "Failed to send event to $relayUrl: WebSocket send failed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to send event to $relayUrl: ${e.message}")
|
Log.e(TAG, "Failed to send event to $relayUrl: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,24 +646,13 @@ class NostrRelayManager private constructor() {
|
|||||||
activeSubscriptions[response.subscriptionId]?.let { subInfo ->
|
activeSubscriptions[response.subscriptionId]?.let { subInfo ->
|
||||||
val matches = try { subInfo.filter.matches(response.event) } catch (e: Exception) { true }
|
val matches = try { subInfo.filter.matches(response.event) } catch (e: Exception) { true }
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
Log.v(TAG, "🚫 Dropping event ${response.event.id.take(16)}... not matching filter for sub=${response.subscriptionId}")
|
|
||||||
// Do NOT call deduplicator here to allow the correct subscription to process it later
|
// Do NOT call deduplicator here to allow the correct subscription to process it later
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEDUPLICATION: Check if we've already processed this event
|
// DEDUPLICATION: Check if we've already processed this event
|
||||||
val wasProcessed = eventDeduplicator.processEvent(response.event) { event ->
|
eventDeduplicator.processEvent(response.event) { event ->
|
||||||
// Only log non-gift-wrap events to reduce noise
|
|
||||||
if (event.kind != NostrKind.GIFT_WRAP) {
|
|
||||||
val originGeo = activeSubscriptions[response.subscriptionId]?.originGeohash
|
|
||||||
if (originGeo != null) {
|
|
||||||
Log.v(TAG, "📥 Processing event (kind=${event.kind}) from relay=$relayUrl geo=$originGeo sub=${response.subscriptionId}")
|
|
||||||
} else {
|
|
||||||
Log.v(TAG, "📥 Processing event (kind=${event.kind}) from relay=$relayUrl sub=${response.subscriptionId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call handler for new events only
|
// Call handler for new events only
|
||||||
val handler = messageHandlers[response.subscriptionId]
|
val handler = messageHandlers[response.subscriptionId]
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
@ -696,35 +660,29 @@ class NostrRelayManager private constructor() {
|
|||||||
handler(event)
|
handler(event)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ No handler for subscription ${response.subscriptionId}")
|
Log.d(TAG, "No handler for subscription ${response.subscriptionId}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wasProcessed) {
|
|
||||||
//Log.v(TAG, "🔄 Duplicate event ${response.event.id.take(16)}... from relay: $relayUrl")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is NostrResponse.EndOfStoredEvents -> {
|
is NostrResponse.EndOfStoredEvents -> {
|
||||||
Log.v(TAG, "End of stored events for subscription: ${response.subscriptionId}")
|
// No action needed
|
||||||
}
|
}
|
||||||
|
|
||||||
is NostrResponse.Ok -> {
|
is NostrResponse.Ok -> {
|
||||||
val wasGiftWrap = pendingGiftWrapIDs.remove(response.eventId)
|
val wasGiftWrap = pendingGiftWrapIDs.remove(response.eventId)
|
||||||
if (response.accepted) {
|
if (!response.accepted) {
|
||||||
Log.d(TAG, "✅ Event accepted id=${response.eventId.take(16)}... by relay: $relayUrl")
|
|
||||||
} else {
|
|
||||||
val level = if (wasGiftWrap) Log.WARN else Log.ERROR
|
val level = if (wasGiftWrap) Log.WARN else Log.ERROR
|
||||||
Log.println(level, TAG, "📮 Event ${response.eventId.take(16)}... rejected by relay: ${response.message ?: "no reason"}")
|
Log.println(level, TAG, "Event rejected by relay $relayUrl: ${response.message ?: "no reason"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NostrResponse.Notice -> {
|
is NostrResponse.Notice -> {
|
||||||
Log.i(TAG, "📢 Notice from $relayUrl: ${response.message}")
|
Log.d(TAG, "Notice from $relayUrl: ${response.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NostrResponse.Unknown -> {
|
is NostrResponse.Unknown -> {
|
||||||
Log.v(TAG, "Unknown message type from $relayUrl: ${response.raw}")
|
Log.d(TAG, "Unknown message type from $relayUrl")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -820,29 +778,26 @@ class NostrRelayManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionsToRestore.isEmpty()) {
|
if (subscriptionsToRestore.isEmpty()) {
|
||||||
Log.v(TAG, "🔄 No subscriptions to restore for relay: $relayUrl")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "🔄 Restoring ${subscriptionsToRestore.size} subscriptions for relay: $relayUrl")
|
Log.d(TAG, "Restoring ${subscriptionsToRestore.size} subscriptions for relay: $relayUrl")
|
||||||
|
|
||||||
subscriptionsToRestore.forEach { subscriptionInfo ->
|
subscriptionsToRestore.forEach { subscriptionInfo ->
|
||||||
try {
|
try {
|
||||||
val request = NostrRequest.Subscribe(subscriptionInfo.id, listOf(subscriptionInfo.filter))
|
val request = NostrRequest.Subscribe(subscriptionInfo.id, listOf(subscriptionInfo.filter))
|
||||||
val message = gson.toJson(request, NostrRequest::class.java)
|
val message = gson.toJson(request, NostrRequest::class.java)
|
||||||
|
|
||||||
val success = webSocket.send(message)
|
val success = webSocket.send(message)
|
||||||
if (success) {
|
if (success) {
|
||||||
// Track subscription for this relay
|
// Track subscription for this relay
|
||||||
val currentSubs = subscriptions[relayUrl] ?: emptySet()
|
val currentSubs = subscriptions[relayUrl] ?: emptySet()
|
||||||
subscriptions[relayUrl] = currentSubs + subscriptionInfo.id
|
subscriptions[relayUrl] = currentSubs + subscriptionInfo.id
|
||||||
|
|
||||||
Log.v(TAG, "✅ Restored subscription '${subscriptionInfo.id}' to relay: $relayUrl")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "❌ Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: WebSocket send failed")
|
Log.w(TAG, "Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: WebSocket send failed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: ${e.message}")
|
Log.e(TAG, "Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -853,7 +808,7 @@ class NostrRelayManager private constructor() {
|
|||||||
private inner class RelayWebSocketListener(private val relayUrl: String) : WebSocketListener() {
|
private inner class RelayWebSocketListener(private val relayUrl: String) : WebSocketListener() {
|
||||||
|
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
Log.d(TAG, "✅ Connected to Nostr relay: $relayUrl")
|
Log.i(TAG, "Connected to Nostr relay: $relayUrl")
|
||||||
updateRelayStatus(relayUrl, true)
|
updateRelayStatus(relayUrl, true)
|
||||||
|
|
||||||
// Restore all active subscriptions for this relay
|
// Restore all active subscriptions for this relay
|
||||||
@ -876,17 +831,17 @@ class NostrRelayManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
Log.d(TAG, "WebSocket closing for $relayUrl: $code $reason")
|
// Server-initiated close; onClosed will follow
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
Log.d(TAG, "WebSocket closed for $relayUrl: $code $reason")
|
Log.i(TAG, "Disconnected from Nostr relay $relayUrl: $code $reason")
|
||||||
val error = Exception("WebSocket closed: $code $reason")
|
val error = Exception("WebSocket closed: $code $reason")
|
||||||
handleDisconnection(relayUrl, error)
|
handleDisconnection(relayUrl, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
Log.e(TAG, "❌ WebSocket failure for $relayUrl: ${t.message}")
|
Log.e(TAG, "WebSocket failure for $relayUrl: ${t.message}")
|
||||||
handleDisconnection(relayUrl, t)
|
handleDisconnection(relayUrl, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,7 +165,7 @@ class NostrTestManager(private val context: Context) {
|
|||||||
|
|
||||||
// Subscribe to private messages (won't receive any in test, but tests the subscription mechanism)
|
// Subscribe to private messages (won't receive any in test, but tests the subscription mechanism)
|
||||||
nostrClient.subscribeToPrivateMessages { content, senderNpub, timestamp ->
|
nostrClient.subscribeToPrivateMessages { content, senderNpub, timestamp ->
|
||||||
Log.d(TAG, "📥 Received test private message from $senderNpub: $content")
|
Log.d(TAG, "Received test private message (${content.length} chars)")
|
||||||
messageReceived = true
|
messageReceived = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,20 +68,18 @@ class NostrTransport(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "NostrTransport: preparing PM to ${recipientNostrPubkey.take(16)}... for peerID ${to.take(8)}... id=${messageID.take(8)}...")
|
|
||||||
|
|
||||||
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
||||||
if (recipientHex == null) {
|
if (recipientHex == null) {
|
||||||
Log.e(TAG, "NostrTransport: recipient key is not a valid Nostr pubkey")
|
Log.e(TAG, "NostrTransport: recipient key is not a valid Nostr pubkey")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val recipientPeerIDForEmbed = try {
|
val recipientPeerIDForEmbed = try {
|
||||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared
|
||||||
.findPeerIDForNostrPubkey(recipientNostrPubkey)
|
.findPeerIDForNostrPubkey(recipientNostrPubkey)
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
if (recipientPeerIDForEmbed.isNullOrBlank()) {
|
if (recipientPeerIDForEmbed.isNullOrBlank()) {
|
||||||
Log.e(TAG, "NostrTransport: no peerID stored for recipient npub; cannot embed PM. npub=${recipientNostrPubkey.take(16)}...")
|
Log.e(TAG, "NostrTransport: no peerID stored for recipient npub; cannot embed PM")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val embedded = NostrEmbeddedBitChat.encodePMForNostr(
|
val embedded = NostrEmbeddedBitChat.encodePMForNostr(
|
||||||
@ -104,11 +102,10 @@ class NostrTransport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
giftWraps.forEach { event ->
|
giftWraps.forEach { event ->
|
||||||
Log.d(TAG, "NostrTransport: sending PM giftWrap id=${event.id.take(16)}...")
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(event.id)
|
NostrRelayManager.registerPendingGiftWrap(event.id)
|
||||||
NostrRelayManager.getInstance(context).sendEvent(event)
|
NostrRelayManager.getInstance(context).sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send private message via Nostr: ${e.message}")
|
Log.e(TAG, "Failed to send private message via Nostr: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -153,8 +150,6 @@ class NostrTransport(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "NostrTransport: preparing READ ack for id=${item.receipt.originalMessageID.take(8)}... to ${recipientNostrPubkey.take(16)}...")
|
|
||||||
|
|
||||||
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
||||||
if (recipientHex == null) {
|
if (recipientHex == null) {
|
||||||
scheduleNextReadAck()
|
scheduleNextReadAck()
|
||||||
@ -181,11 +176,10 @@ class NostrTransport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
giftWraps.forEach { event ->
|
giftWraps.forEach { event ->
|
||||||
Log.d(TAG, "NostrTransport: sending READ ack giftWrap id=${event.id.take(16)}...")
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(event.id)
|
NostrRelayManager.registerPendingGiftWrap(event.id)
|
||||||
NostrRelayManager.getInstance(context).sendEvent(event)
|
NostrRelayManager.getInstance(context).sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleNextReadAck()
|
scheduleNextReadAck()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -220,9 +214,7 @@ class NostrTransport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val content = FavoriteControlMessage.encode(isFavorite, senderIdentity.npub)
|
val content = FavoriteControlMessage.encode(isFavorite, senderIdentity.npub)
|
||||||
|
|
||||||
Log.d(TAG, "NostrTransport: preparing FAVORITE($isFavorite) to ${recipientNostrPubkey.take(16)}...")
|
|
||||||
|
|
||||||
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
||||||
if (recipientHex == null) {
|
if (recipientHex == null) {
|
||||||
return@launch
|
return@launch
|
||||||
@ -247,11 +239,10 @@ class NostrTransport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
giftWraps.forEach { event ->
|
giftWraps.forEach { event ->
|
||||||
Log.d(TAG, "NostrTransport: sending favorite giftWrap id=${event.id.take(16)}...")
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(event.id)
|
NostrRelayManager.registerPendingGiftWrap(event.id)
|
||||||
NostrRelayManager.getInstance(context).sendEvent(event)
|
NostrRelayManager.getInstance(context).sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send favorite notification via Nostr: ${e.message}")
|
Log.e(TAG, "Failed to send favorite notification via Nostr: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -274,13 +265,11 @@ class NostrTransport(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "NostrTransport: preparing DELIVERED ack for id=${messageID.take(8)}... to ${recipientNostrPubkey.take(16)}...")
|
|
||||||
|
|
||||||
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey)
|
||||||
if (recipientHex == null) {
|
if (recipientHex == null) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val ack = NostrEmbeddedBitChat.encodeAckForNostr(
|
val ack = NostrEmbeddedBitChat.encodeAckForNostr(
|
||||||
type = NoisePayloadType.DELIVERED,
|
type = NoisePayloadType.DELIVERED,
|
||||||
messageID = messageID,
|
messageID = messageID,
|
||||||
@ -300,11 +289,10 @@ class NostrTransport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
giftWraps.forEach { event ->
|
giftWraps.forEach { event ->
|
||||||
Log.d(TAG, "NostrTransport: sending DELIVERED ack giftWrap id=${event.id.take(16)}...")
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(event.id)
|
NostrRelayManager.registerPendingGiftWrap(event.id)
|
||||||
NostrRelayManager.getInstance(context).sendEvent(event)
|
NostrRelayManager.getInstance(context).sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send delivery ack via Nostr: ${e.message}")
|
Log.e(TAG, "Failed to send delivery ack via Nostr: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -320,8 +308,6 @@ class NostrTransport(
|
|||||||
) {
|
) {
|
||||||
transportScope.launch {
|
transportScope.launch {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "GeoDM: send DELIVERED -> recip=${toRecipientHex.take(8)}... mid=${messageID.take(8)}... from=${fromIdentity.publicKeyHex.take(8)}...")
|
|
||||||
|
|
||||||
val embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(
|
val embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(
|
||||||
type = NoisePayloadType.DELIVERED,
|
type = NoisePayloadType.DELIVERED,
|
||||||
messageID = messageID,
|
messageID = messageID,
|
||||||
@ -355,8 +341,6 @@ class NostrTransport(
|
|||||||
) {
|
) {
|
||||||
transportScope.launch {
|
transportScope.launch {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "GeoDM: send READ -> recip=${toRecipientHex.take(8)}... mid=${messageID.take(8)}... from=${fromIdentity.publicKeyHex.take(8)}...")
|
|
||||||
|
|
||||||
val embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(
|
val embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(
|
||||||
type = NoisePayloadType.READ_RECEIPT,
|
type = NoisePayloadType.READ_RECEIPT,
|
||||||
messageID = messageID,
|
messageID = messageID,
|
||||||
@ -414,11 +398,6 @@ class NostrTransport(
|
|||||||
try {
|
try {
|
||||||
if (toRecipientHex.isEmpty()) return@launch
|
if (toRecipientHex.isEmpty()) return@launch
|
||||||
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"GeoDM: send PM -> recip=${toRecipientHex.take(8)}... mid=${messageID.take(8)}... from=${fromIdentity.publicKeyHex.take(8)}... geohash=$geohash"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Build embedded BitChat packet without recipient peer ID
|
// Build embedded BitChat packet without recipient peer ID
|
||||||
val embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(
|
val embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(
|
||||||
content = content,
|
content = content,
|
||||||
@ -436,7 +415,6 @@ class NostrTransport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
giftWraps.forEach { event ->
|
giftWraps.forEach { event ->
|
||||||
Log.d(TAG, "NostrTransport: sending geohash PM giftWrap id=${event.id.take(16)}...")
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(event.id)
|
NostrRelayManager.registerPendingGiftWrap(event.id)
|
||||||
NostrRelayManager.getInstance(context).sendEvent(event)
|
NostrRelayManager.getInstance(context).sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,13 +99,12 @@ class MediaSendingManager(
|
|||||||
val filePacket = withContext(mediaWorkDispatcher) {
|
val filePacket = withContext(mediaWorkDispatcher) {
|
||||||
val file = java.io.File(filePath)
|
val file = java.io.File(filePath)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.e(TAG, "❌ File does not exist: $filePath")
|
Log.e(TAG, "Voice note file does not exist")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
|
||||||
|
|
||||||
if (file.length() > MAX_FILE_SIZE) {
|
if (file.length() > MAX_FILE_SIZE) {
|
||||||
Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)")
|
Log.e(TAG, "File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,16 +142,14 @@ class MediaSendingManager(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val filePacket = withContext(mediaWorkDispatcher) {
|
val filePacket = withContext(mediaWorkDispatcher) {
|
||||||
Log.d(TAG, "🔄 Starting image send: $filePath")
|
|
||||||
val file = java.io.File(filePath)
|
val file = java.io.File(filePath)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.e(TAG, "❌ File does not exist: $filePath")
|
Log.e(TAG, "Image file does not exist")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
|
||||||
|
|
||||||
if (file.length() > MAX_FILE_SIZE) {
|
if (file.length() > MAX_FILE_SIZE) {
|
||||||
Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)")
|
Log.e(TAG, "File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,10 +167,7 @@ class MediaSendingManager(
|
|||||||
sendPublicFile(channelOrNull, filePacket, filePath, BitchatMessageType.Image)
|
sendPublicFile(channelOrNull, filePacket, filePath, BitchatMessageType.Image)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ CRITICAL: Image send failed completely", e)
|
Log.e(TAG, "Image send failed: ${e.message}", e)
|
||||||
Log.e(TAG, "❌ Image path: $filePath")
|
|
||||||
Log.e(TAG, "❌ Error details: ${e.message}")
|
|
||||||
Log.e(TAG, "❌ Error type: ${e.javaClass.simpleName}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,16 +187,14 @@ class MediaSendingManager(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val filePacket = withContext(mediaWorkDispatcher) {
|
val filePacket = withContext(mediaWorkDispatcher) {
|
||||||
Log.d(TAG, "🔄 Starting file send: $filePath")
|
|
||||||
val file = java.io.File(filePath)
|
val file = java.io.File(filePath)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.e(TAG, "❌ File does not exist: $filePath")
|
Log.e(TAG, "File does not exist")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
|
||||||
|
|
||||||
if (file.length() > MAX_FILE_SIZE) {
|
if (file.length() > MAX_FILE_SIZE) {
|
||||||
Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)")
|
Log.e(TAG, "File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +204,6 @@ class MediaSendingManager(
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
}
|
}
|
||||||
Log.d(TAG, "🏷️ MIME type: $mimeType")
|
|
||||||
|
|
||||||
// Try to preserve the original file name if our copier prefixed it earlier
|
// Try to preserve the original file name if our copier prefixed it earlier
|
||||||
val originalName = run {
|
val originalName = run {
|
||||||
@ -228,7 +219,6 @@ class MediaSendingManager(
|
|||||||
?: base
|
?: base
|
||||||
stripped + ext
|
stripped + ext
|
||||||
}
|
}
|
||||||
Log.d(TAG, "📝 Original filename: $originalName")
|
|
||||||
|
|
||||||
BitchatFilePacket(
|
BitchatFilePacket(
|
||||||
fileName = originalName,
|
fileName = originalName,
|
||||||
@ -237,7 +227,6 @@ class MediaSendingManager(
|
|||||||
content = file.readBytes()
|
content = file.readBytes()
|
||||||
)
|
)
|
||||||
} ?: return
|
} ?: return
|
||||||
Log.d(TAG, "📦 Created file packet successfully")
|
|
||||||
|
|
||||||
val messageType = when {
|
val messageType = when {
|
||||||
filePacket.mimeType.lowercase().startsWith("image/") -> BitchatMessageType.Image
|
filePacket.mimeType.lowercase().startsWith("image/") -> BitchatMessageType.Image
|
||||||
@ -251,10 +240,7 @@ class MediaSendingManager(
|
|||||||
sendPublicFile(channelOrNull, filePacket, filePath, messageType)
|
sendPublicFile(channelOrNull, filePacket, filePath, messageType)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ CRITICAL: File send failed completely", e)
|
Log.e(TAG, "File send failed: ${e.message}", e)
|
||||||
Log.e(TAG, "❌ File path: $filePath")
|
|
||||||
Log.e(TAG, "❌ Error details: ${e.message}")
|
|
||||||
Log.e(TAG, "❌ Error type: ${e.javaClass.simpleName}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,17 +255,14 @@ class MediaSendingManager(
|
|||||||
) {
|
) {
|
||||||
val payload = withContext(mediaWorkDispatcher) { filePacket.encode() }
|
val payload = withContext(mediaWorkDispatcher) { filePacket.encode() }
|
||||||
?: run {
|
?: run {
|
||||||
Log.e(TAG, "❌ Failed to encode file packet for private send")
|
Log.e(TAG, "Failed to encode file packet for private send")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d(TAG, "🔒 Encoded private packet: ${payload.size} bytes")
|
|
||||||
|
|
||||||
val (transferId, contentHash) = withContext(mediaWorkDispatcher) {
|
val transferId = withContext(mediaWorkDispatcher) {
|
||||||
sha256Hex(payload) to sha256Hex(filePacket.content)
|
sha256Hex(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📤 FILE_TRANSFER send (private): name='${filePacket.fileName}', size=${filePacket.fileSize}, mime='${filePacket.mimeType}', sha256=$contentHash, to=${toPeerID.take(8)} transferId=${transferId.take(16)}…")
|
|
||||||
|
|
||||||
val pending = PendingAutomaticPrivateMedia(
|
val pending = PendingAutomaticPrivateMedia(
|
||||||
requestId = UUID.randomUUID().toString(),
|
requestId = UUID.randomUUID().toString(),
|
||||||
peerID = toPeerID,
|
peerID = toPeerID,
|
||||||
@ -465,7 +448,7 @@ class MediaSendingManager(
|
|||||||
|
|
||||||
PrivateMediaPreparation.NeedsHandshake -> {
|
PrivateMediaPreparation.NeedsHandshake -> {
|
||||||
ensureAutomaticPendingTimeout(pending)
|
ensureAutomaticPendingTimeout(pending)
|
||||||
Log.i(TAG, "Private media needs a Noise handshake; retaining first-send intent")
|
Log.d(TAG, "Private media needs a Noise handshake; retaining first-send intent")
|
||||||
try {
|
try {
|
||||||
meshService.initiateNoiseHandshake(pending.peerID)
|
meshService.initiateNoiseHandshake(pending.peerID)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -475,7 +458,7 @@ class MediaSendingManager(
|
|||||||
|
|
||||||
PrivateMediaPreparation.AwaitingPeerState -> {
|
PrivateMediaPreparation.AwaitingPeerState -> {
|
||||||
ensureAutomaticPendingTimeout(pending)
|
ensureAutomaticPendingTimeout(pending)
|
||||||
Log.i(TAG, "Private media is waiting for authenticated peer state; first-send intent retained")
|
Log.d(TAG, "Private media is waiting for authenticated peer state; first-send intent retained")
|
||||||
}
|
}
|
||||||
|
|
||||||
is PrivateMediaPreparation.Rejected -> {
|
is PrivateMediaPreparation.Rejected -> {
|
||||||
@ -614,7 +597,6 @@ class MediaSendingManager(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d(TAG, "✅ Private media committed using ${preparation.transfer.wireMode}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -628,16 +610,13 @@ class MediaSendingManager(
|
|||||||
) {
|
) {
|
||||||
val payload = withContext(mediaWorkDispatcher) { filePacket.encode() }
|
val payload = withContext(mediaWorkDispatcher) { filePacket.encode() }
|
||||||
?: run {
|
?: run {
|
||||||
Log.e(TAG, "❌ Failed to encode file packet for broadcast send")
|
Log.e(TAG, "Failed to encode file packet for broadcast send")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d(TAG, "🔓 Encoded broadcast packet: ${payload.size} bytes")
|
|
||||||
|
|
||||||
val (transferId, contentHash) = withContext(mediaWorkDispatcher) {
|
val transferId = withContext(mediaWorkDispatcher) {
|
||||||
sha256Hex(payload) to sha256Hex(filePacket.content)
|
sha256Hex(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📤 FILE_TRANSFER send (broadcast): name='${filePacket.fileName}', size=${filePacket.fileSize}, mime='${filePacket.mimeType}', sha256=$contentHash, transferId=${transferId.take(16)}…")
|
|
||||||
|
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
id = java.util.UUID.randomUUID().toString().uppercase(), // Generate unique ID for each message
|
id = java.util.UUID.randomUUID().toString().uppercase(), // Generate unique ID for each message
|
||||||
@ -667,11 +646,9 @@ class MediaSendingManager(
|
|||||||
com.bitchat.android.model.DeliveryStatus.PartiallyDelivered(0, 100)
|
com.bitchat.android.model.DeliveryStatus.PartiallyDelivered(0, 100)
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d(TAG, "📤 Calling meshService.sendFileBroadcast")
|
|
||||||
withContext(mediaWorkDispatcher) {
|
withContext(mediaWorkDispatcher) {
|
||||||
meshService.sendFileBroadcast(filePacket)
|
meshService.sendFileBroadcast(filePacket)
|
||||||
}
|
}
|
||||||
Log.d(TAG, "✅ File broadcast completed successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -199,10 +199,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
registerProvisionalWifiClaim(pid, claim)
|
registerProvisionalWifiClaim(pid, claim)
|
||||||
if (!meshCore.initiateNoiseHandshakeOnLink(pid, relay, linkID)) {
|
if (!meshCore.initiateNoiseHandshakeOnLink(pid, relay, linkID)) {
|
||||||
provisionalWifiClaims.remove(pid, claim)
|
provisionalWifiClaims.remove(pid, claim)
|
||||||
Log.w(
|
Log.w(TAG, "Could not send Noise challenge on Wi-Fi link for ${pid.take(8)}")
|
||||||
TAG,
|
|
||||||
"Could not send Noise challenge on exact Wi-Fi link for ${pid.take(8)}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,16 +252,13 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
* Broadcasts raw bytes to currently connected peer.
|
* Broadcasts raw bytes to currently connected peer.
|
||||||
*/
|
*/
|
||||||
private fun broadcastRaw(bytes: ByteArray) {
|
private fun broadcastRaw(bytes: ByteArray) {
|
||||||
var sent = 0
|
|
||||||
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
||||||
try {
|
try {
|
||||||
sock.write(bytes)
|
sock.write(bytes)
|
||||||
sent++
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "TX: write failed to ${pid.take(8)}: ${e.message}")
|
Log.e(TAG, "TX: write failed to ${pid.take(8)}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(TAG, "TX: broadcast via Wi-Fi Aware to $sent peers (bytes=${bytes.size})")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransportLayer implementation
|
// TransportLayer implementation
|
||||||
@ -282,22 +276,17 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
* Broadcasts routed packet to currently connected peers.
|
* Broadcasts routed packet to currently connected peers.
|
||||||
*/
|
*/
|
||||||
private fun broadcastPacket(routed: RoutedPacket) {
|
private fun broadcastPacket(routed: RoutedPacket) {
|
||||||
Log.d(TAG, "TX: packet type=${routed.packet.type} broadcast (ttl=${routed.packet.ttl})")
|
|
||||||
|
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) {
|
if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) {
|
||||||
val firstHop = packet.route!![0].toHexString()
|
val firstHop = packet.route!![0].toHexString()
|
||||||
if (sendRoutedPacketToPeer(firstHop, routed)) {
|
if (sendRoutedPacketToPeer(firstHop, routed)) {
|
||||||
Log.d(TAG, "TX: source-routed packet sent only to first Wi-Fi hop ${firstHop.take(8)}")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.w(TAG, "TX: first Wi-Fi source-route hop ${firstHop.take(8)} unavailable; falling back to broadcast")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val recipientId = packet.recipientID?.toHexString()
|
val recipientId = packet.recipientID?.toHexString()
|
||||||
if (recipientId != null && !packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) {
|
if (recipientId != null && !packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) {
|
||||||
if (sendRoutedPacketToPeer(recipientId, routed)) {
|
if (sendRoutedPacketToPeer(recipientId, routed)) {
|
||||||
Log.d(TAG, "TX: addressed packet sent directly to Wi-Fi peer ${recipientId.take(8)}")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,7 +310,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
|
|
||||||
private fun sendRoutedPacketToPeer(peerID: String, routed: RoutedPacket): Boolean {
|
private fun sendRoutedPacketToPeer(peerID: String, routed: RoutedPacket): Boolean {
|
||||||
if (connectionTracker.getSocketForPeer(peerID) == null) {
|
if (connectionTracker.getSocketForPeer(peerID) == null) {
|
||||||
Log.w(TAG, "TX: no socket for ${peerID.take(8)}")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return fragmentingSender.send(routed, "Wi-Fi Aware peer ${peerID.take(8)}") { single ->
|
return fragmentingSender.send(routed, "Wi-Fi Aware peer ${peerID.take(8)}") { single ->
|
||||||
@ -339,12 +327,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val data = packet.toBinaryData() ?: return false
|
val data = packet.toBinaryData() ?: return false
|
||||||
val sock = connectionTracker.getSocketForPeer(peerID)
|
val sock = connectionTracker.getSocketForPeer(peerID)
|
||||||
if (sock == null) {
|
if (sock == null) {
|
||||||
Log.w(TAG, "TX: no socket for ${peerID.take(8)}")
|
Log.d(TAG, "TX: no socket for ${peerID.take(8)}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
sock.write(data)
|
sock.write(data)
|
||||||
Log.d(TAG, "TX: packet type=${packet.type} to ${peerID.take(8)} (bytes=${data.size})")
|
|
||||||
return true
|
return true
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "TX: write to ${peerID.take(8)} failed: ${e.message}")
|
Log.e(TAG, "TX: write to ${peerID.take(8)} failed: ${e.message}")
|
||||||
@ -409,7 +396,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
wifiAwareSession = session
|
wifiAwareSession = session
|
||||||
Log.i(TAG, "Wi-Fi Aware attached; starting publish & subscribe (peerID=$myPeerID)")
|
Log.i(TAG, "Wi-Fi Aware attached; starting publish & subscribe")
|
||||||
|
|
||||||
// PUBLISH (server role)
|
// PUBLISH (server role)
|
||||||
session.publish(
|
session.publish(
|
||||||
@ -424,7 +411,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
publishSession = pub
|
publishSession = pub
|
||||||
Log.d(TAG, "PUBLISH: onPublishStarted()")
|
Log.d(TAG, "Wi-Fi Aware publish started")
|
||||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi-Fi Aware Publish Started")) } catch (_: Exception) {}
|
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(
|
override fun onServiceDiscovered(
|
||||||
@ -438,12 +425,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (peerId.isNotBlank()) {
|
if (peerId.isNotBlank()) {
|
||||||
rememberDiscoveredPeer(peerId)
|
rememberDiscoveredPeer(peerId)
|
||||||
publishHandles[peerId] = peerHandle
|
publishHandles[peerId] = peerHandle
|
||||||
Log.i(TAG, "PUBLISH: Discovered subscriber '$peerId' via Aware")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
offerServerPathIfAppropriate(peerId, peerHandle, "publish discovery")
|
offerServerPathIfAppropriate(peerId, peerHandle, "publish discovery")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "PUBLISH: onServiceDiscovered ssi='${peerId.take(16)}' len=${serviceSpecificInfo.size}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@ -466,18 +451,16 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
rememberDiscoveredPeer(subscriberId)
|
rememberDiscoveredPeer(subscriberId)
|
||||||
publishHandles[subscriberId] = peerHandle
|
publishHandles[subscriberId] = peerHandle
|
||||||
}
|
}
|
||||||
Log.i(TAG, "PUBLISH: Received discovery ping from subscriber '$subscriberId'")
|
|
||||||
handleSubscriberPing(publishSession!!, peerHandle)
|
handleSubscriberPing(publishSession!!, peerHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionTerminated() {
|
override fun onSessionTerminated() {
|
||||||
if (!isCurrentSession(generation)) return
|
if (!isCurrentSession(generation)) return
|
||||||
Log.e(TAG, "PUBLISH: onSessionTerminated()")
|
|
||||||
publishSession = null
|
publishSession = null
|
||||||
val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
||||||
|
Log.w(TAG, "Wi-Fi Aware publish session terminated (restart=$shouldRestart)")
|
||||||
handleUnexpectedStop(generation)
|
handleUnexpectedStop(generation)
|
||||||
if (shouldRestart) {
|
if (shouldRestart) {
|
||||||
Log.i(TAG, "PUBLISH: Scheduling Wi-Fi Aware restart")
|
|
||||||
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000)
|
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,7 +481,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
subscribeSession = sub
|
subscribeSession = sub
|
||||||
Log.d(TAG, "SUBSCRIBE: onSubscribeStarted()")
|
Log.d(TAG, "Wi-Fi Aware subscribe started")
|
||||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi-Fi Aware Subscribe Started")) } catch (_: Exception) {}
|
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(
|
override fun onServiceDiscovered(
|
||||||
@ -528,12 +511,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
|
|
||||||
override fun onSessionTerminated() {
|
override fun onSessionTerminated() {
|
||||||
if (!isCurrentSession(generation)) return
|
if (!isCurrentSession(generation)) return
|
||||||
Log.e(TAG, "SUBSCRIBE: onSessionTerminated()")
|
|
||||||
subscribeSession = null
|
subscribeSession = null
|
||||||
val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
||||||
|
Log.w(TAG, "Wi-Fi Aware subscribe session terminated (restart=$shouldRestart)")
|
||||||
handleUnexpectedStop(generation)
|
handleUnexpectedStop(generation)
|
||||||
if (shouldRestart) {
|
if (shouldRestart) {
|
||||||
Log.i(TAG, "SUBSCRIBE: Scheduling Wi-Fi Aware restart")
|
|
||||||
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000)
|
com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -552,7 +534,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
|
|
||||||
override fun onAwareSessionTerminated() {
|
override fun onAwareSessionTerminated() {
|
||||||
if (!isCurrentSession(generation)) return
|
if (!isCurrentSession(generation)) return
|
||||||
Log.e(TAG, "Aware Session Terminated unexpectedly")
|
Log.e(TAG, "Wi-Fi Aware session terminated unexpectedly")
|
||||||
wifiAwareSession = null
|
wifiAwareSession = null
|
||||||
val shouldRestart = com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
val shouldRestart = com.bitchat.android.wifiaware.WifiAwareController.enabled.value
|
||||||
handleUnexpectedStop(generation)
|
handleUnexpectedStop(generation)
|
||||||
@ -679,7 +661,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (peerId.isBlank() || peerId == myPeerID || !amIServerFor(peerId)) return
|
if (peerId.isBlank() || peerId == myPeerID || !amIServerFor(peerId)) return
|
||||||
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) return
|
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) return
|
||||||
|
|
||||||
Log.d(TAG, "PUBLISH: offering server path to ${peerId.take(8)} after $reason")
|
|
||||||
handleSubscriberPing(pubSession, peerHandle)
|
handleSubscriberPing(pubSession, peerHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -691,7 +672,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if ((now - lastRefresh) < DISCOVERY_SESSION_REFRESH_MIN_INTERVAL_MS) return false
|
if ((now - lastRefresh) < DISCOVERY_SESSION_REFRESH_MIN_INTERVAL_MS) return false
|
||||||
if (!lastDiscoveryRefreshAt.compareAndSet(lastRefresh, now)) return false
|
if (!lastDiscoveryRefreshAt.compareAndSet(lastRefresh, now)) return false
|
||||||
|
|
||||||
Log.i(TAG, "Maintenance: refreshing Wi-Fi Aware discovery sessions ($reason)")
|
Log.i(TAG, "Refreshing Wi-Fi Aware discovery sessions ($reason)")
|
||||||
handleUnexpectedStop(sessionGeneration.get())
|
handleUnexpectedStop(sessionGeneration.get())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -701,7 +682,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
*/
|
*/
|
||||||
private fun startPeriodicConnectionMaintenance() {
|
private fun startPeriodicConnectionMaintenance() {
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Starting periodic connection maintenance loop")
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
delay(15_000) // Check every 15 seconds
|
delay(15_000) // Check every 15 seconds
|
||||||
@ -719,7 +699,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
handleToPeerId.entries.removeIf { it.value in staleIds }
|
handleToPeerId.entries.removeIf { it.value in staleIds }
|
||||||
staleIds.forEach { subscribeHandles.remove(it) }
|
staleIds.forEach { subscribeHandles.remove(it) }
|
||||||
staleIds.forEach { publishHandles.remove(it) }
|
staleIds.forEach { publishHandles.remove(it) }
|
||||||
Log.d(TAG, "Maintenance: pruned ${staleIds.size} stale discovery entries")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Identify peers that are discovered (recently seen) but not currently connected
|
// 1. Identify peers that are discovered (recently seen) but not currently connected
|
||||||
@ -745,7 +724,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
}
|
}
|
||||||
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
Log.i(TAG, "Maintenance: offering Wi-Fi Aware server path to ${peerId.take(8)}")
|
|
||||||
offerServerPathIfAppropriate(peerId, handle, "maintenance")
|
offerServerPathIfAppropriate(peerId, handle, "maintenance")
|
||||||
attemptedReconnect = true
|
attemptedReconnect = true
|
||||||
}
|
}
|
||||||
@ -763,7 +741,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
// Check tracker policy
|
// Check tracker policy
|
||||||
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
||||||
|
|
||||||
Log.i(TAG, "Maintenance: attempting Wi-Fi Aware reconnect to ${peerId.take(8)}")
|
|
||||||
sendSubscribePing(peerId, handle, "maintenance")
|
sendSubscribePing(peerId, handle, "maintenance")
|
||||||
attemptedReconnect = true
|
attemptedReconnect = true
|
||||||
}
|
}
|
||||||
@ -795,9 +772,8 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
||||||
try {
|
try {
|
||||||
subscribeSession?.sendMessage(peerHandle, msgId, myPeerID.toByteArray())
|
subscribeSession?.sendMessage(peerHandle, msgId, myPeerID.toByteArray())
|
||||||
Log.d(TAG, "SUBSCRIBE: sent $reason ping to '${peerId.take(16)}' (msgId=$msgId)")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to send $reason ping to ${peerId.take(8)}: ${e.message}")
|
Log.d(TAG, "Failed to send $reason ping to ${peerId.take(8)}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -809,7 +785,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
|
|
||||||
val handle = subscribeHandles[peerId]
|
val handle = subscribeHandles[peerId]
|
||||||
if (handle == null) {
|
if (handle == null) {
|
||||||
Log.i(TAG, "CLIENT: role reversal queued for ${peerId.take(8)} until subscribe handle is available")
|
Log.d(TAG, "CLIENT: role reversal queued for ${peerId.take(8)} until subscribe handle is available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,7 +793,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val payload = "$ROLE_REVERSAL_PREFIX$myPeerID".toByteArray()
|
val payload = "$ROLE_REVERSAL_PREFIX$myPeerID".toByteArray()
|
||||||
try {
|
try {
|
||||||
subscribeSession?.sendMessage(handle, msgId, payload)
|
subscribeSession?.sendMessage(handle, msgId, payload)
|
||||||
Log.i(TAG, "CLIENT: requested Wi-Fi Aware role reversal with ${peerId.take(8)} (msgId=$msgId)")
|
Log.d(TAG, "CLIENT: requested role reversal with ${peerId.take(8)}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "CLIENT: failed to request role reversal with ${peerId.take(8)}: ${e.message}")
|
Log.w(TAG, "CLIENT: failed to request role reversal with ${peerId.take(8)}: ${e.message}")
|
||||||
}
|
}
|
||||||
@ -830,9 +806,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val shouldReverse = failures >= CLIENT_ROLE_REVERSAL_FAILURES
|
val shouldReverse = failures >= CLIENT_ROLE_REVERSAL_FAILURES
|
||||||
if (shouldReverse) {
|
if (shouldReverse) {
|
||||||
clientSocketFailures.remove(peerId)
|
clientSocketFailures.remove(peerId)
|
||||||
Log.i(TAG, "CLIENT: ${peerId.take(8)} failed $failures client socket attempts; requesting role reversal")
|
Log.d(TAG, "CLIENT: ${peerId.take(8)} failed $failures client socket attempts; requesting role reversal")
|
||||||
} else {
|
|
||||||
Log.d(TAG, "CLIENT: ${peerId.take(8)} failed client socket attempt $failures/$CLIENT_ROLE_REVERSAL_FAILURES; retrying same role")
|
|
||||||
}
|
}
|
||||||
return shouldReverse
|
return shouldReverse
|
||||||
}
|
}
|
||||||
@ -843,7 +817,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
discoveredTimestamps[requesterId] = System.currentTimeMillis()
|
discoveredTimestamps[requesterId] = System.currentTimeMillis()
|
||||||
forcedClientPeers.add(requesterId)
|
forcedClientPeers.add(requesterId)
|
||||||
forcedServerPeers.remove(requesterId)
|
forcedServerPeers.remove(requesterId)
|
||||||
Log.i(TAG, "PUBLISH: role reversal requested by ${requesterId.take(8)}; switching to client role")
|
Log.i(TAG, "Role reversal requested by ${requesterId.take(8)}; switching to client role")
|
||||||
|
|
||||||
subscribeHandles[requesterId]?.let { handle ->
|
subscribeHandles[requesterId]?.let { handle ->
|
||||||
sendSubscribePing(requesterId, handle, "role-reversal")
|
sendSubscribePing(requesterId, handle, "role-reversal")
|
||||||
@ -865,16 +839,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (!amIServerFor(peerId)) return
|
if (!amIServerFor(peerId)) return
|
||||||
|
|
||||||
if (connectionTracker.isConnected(peerId)) {
|
if (connectionTracker.isConnected(peerId)) {
|
||||||
Log.v(TAG, "↪ already connected to $peerId, skipping serve")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (connectionTracker.hasOpenServerSocket(peerId)) {
|
if (connectionTracker.hasOpenServerSocket(peerId)) {
|
||||||
Log.v(TAG, "↪ already serving $peerId, skipping")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (connectionTracker.hasPendingDataPathRequest(peerId)) {
|
if (connectionTracker.hasPendingDataPathRequest(peerId)) {
|
||||||
val pending = connectionTracker.pendingDataPathPeerIds(peerId).joinToString(", ") { it.take(8) }
|
|
||||||
Log.d(TAG, "SERVER: deferring serve for ${peerId.take(8)}; pending Aware data path(s): $pending")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!connectionTracker.addPendingConnection(peerId)) {
|
if (!connectionTracker.addPendingConnection(peerId)) {
|
||||||
@ -895,8 +865,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
connectionTracker.addServerSocket(peerId, ss)
|
connectionTracker.addServerSocket(peerId, ss)
|
||||||
val port = ss.localPort
|
val port = ss.localPort
|
||||||
|
|
||||||
Log.d(TAG, "SERVER: listening for ${peerId.take(8)} on ${ss.localSocketAddress}")
|
|
||||||
|
|
||||||
val spec = WifiAwareNetworkSpecifier.Builder(pubSession, peerHandle)
|
val spec = WifiAwareNetworkSpecifier.Builder(pubSession, peerHandle)
|
||||||
.setPskPassphrase(PSK)
|
.setPskPassphrase(PSK)
|
||||||
.setPort(port)
|
.setPort(port)
|
||||||
@ -914,7 +882,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
private val acceptStarted = AtomicBoolean(false)
|
private val acceptStarted = AtomicBoolean(false)
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
Log.i(TAG, "SERVER: onAvailable() - Aware network is ready for ${peerId.take(8)}")
|
|
||||||
// Only accept once per network request
|
// Only accept once per network request
|
||||||
if (!acceptStarted.compareAndSet(false, true)) return
|
if (!acceptStarted.compareAndSet(false, true)) return
|
||||||
// Offload the blocking accept() off the callback thread so we never stall
|
// Offload the blocking accept() off the callback thread so we never stall
|
||||||
@ -923,10 +890,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
try {
|
try {
|
||||||
try { ss.soTimeout = ACCEPT_TIMEOUT_MS } catch (_: Exception) {}
|
try { ss.soTimeout = ACCEPT_TIMEOUT_MS } catch (_: Exception) {}
|
||||||
val client = ss.accept()
|
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}") }
|
try { network.bindSocket(client) } catch (e: Exception) { Log.w(TAG, "Server bindSocket EPERM: ${e.message}") }
|
||||||
client.keepAlive = true
|
client.keepAlive = true
|
||||||
Log.i(TAG, "SERVER: Bound and established TCP with ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}")
|
Log.i(TAG, "Connected to ${peerId.take(8)} (server)")
|
||||||
val synced = SyncedSocket(client)
|
val synced = SyncedSocket(client)
|
||||||
activeSocket = synced
|
activeSocket = synced
|
||||||
connectionTracker.onClientConnected(peerId, synced)
|
connectionTracker.onClientConnected(peerId, synced)
|
||||||
@ -943,14 +909,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
// Kick off Noise handshake for this logical peer
|
// Kick off Noise handshake for this logical peer
|
||||||
if (myPeerID < peerId) {
|
if (myPeerID < peerId) {
|
||||||
meshCore.initiateNoiseHandshake(peerId)
|
meshCore.initiateNoiseHandshake(peerId)
|
||||||
Log.i(TAG, "SERVER: Initiating Noise handshake to ${peerId.take(8)}")
|
|
||||||
}
|
}
|
||||||
// Ensure fast presence even before handshake settles
|
// Ensure fast presence even before handshake settles
|
||||||
serviceScope.launch { delay(150); sendBroadcastAnnounce() }
|
serviceScope.launch { delay(150); sendBroadcastAnnounce() }
|
||||||
} catch (ioe: IOException) {
|
} catch (ioe: IOException) {
|
||||||
if (ss.isClosed || !isActive) {
|
if (!ss.isClosed && isActive) {
|
||||||
Log.d(TAG, "SERVER: accept stopped for ${peerId.take(8)} after socket cleanup")
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "SERVER: accept failed for ${peerId.take(8)}", ioe)
|
Log.e(TAG, "SERVER: accept failed for ${peerId.take(8)}", ioe)
|
||||||
handleNetworkFailure(peerId)
|
handleNetworkFailure(peerId)
|
||||||
}
|
}
|
||||||
@ -959,18 +922,17 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnavailable() {
|
override fun onUnavailable() {
|
||||||
Log.e(TAG, "SERVER: onUnavailable() - Failed to acquire Aware network for ${peerId.take(8)} (timeout or refused)")
|
Log.e(TAG, "SERVER: failed to acquire Aware network for ${peerId.take(8)}")
|
||||||
handleNetworkFailure(peerId)
|
handleNetworkFailure(peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
handlePeerDisconnection(peerId, activeSocket)
|
handlePeerDisconnection(peerId, activeSocket)
|
||||||
Log.i(TAG, "SERVER: WiFi Aware network lost for ${peerId.take(8)}")
|
Log.i(TAG, "Disconnected from ${peerId.take(8)} (server: network lost)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionTracker.addNetworkCallback(peerId, cb)
|
connectionTracker.addNetworkCallback(peerId, cb)
|
||||||
Log.i(TAG, "SERVER: [Calling requestNetwork] for ${peerId.take(8)} with port $port")
|
|
||||||
try {
|
try {
|
||||||
// use requestNetwork with a timeout to trigger onUnavailable if it fails
|
// use requestNetwork with a timeout to trigger onUnavailable if it fails
|
||||||
cm.requestNetwork(req, cb, NETWORK_REQUEST_TIMEOUT_MS)
|
cm.requestNetwork(req, cb, NETWORK_REQUEST_TIMEOUT_MS)
|
||||||
@ -983,10 +945,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val readyPayload = buildServerReadyPayload(port)
|
val readyPayload = buildServerReadyPayload(port)
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
try {
|
try {
|
||||||
val sent = pubSession.sendMessage(peerHandle, readyId, readyPayload)
|
pubSession.sendMessage(peerHandle, readyId, readyPayload)
|
||||||
Log.d(TAG, "PUBLISH: server-ready sent=$sent (msgId=$readyId, port=$port)")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "PUBLISH: Exception sending server-ready to $peerHandle", e)
|
Log.e(TAG, "PUBLISH: failed to send server-ready to ${peerId.take(8)}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1054,15 +1015,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
sock.tcpNoDelay = true
|
sock.tcpNoDelay = true
|
||||||
sock.keepAlive = true
|
sock.keepAlive = true
|
||||||
sock.connect(java.net.InetSocketAddress(scopedAddr, port), CLIENT_CONNECT_TIMEOUT_MS)
|
sock.connect(java.net.InetSocketAddress(scopedAddr, port), CLIENT_CONNECT_TIMEOUT_MS)
|
||||||
if (attempt > 1) {
|
|
||||||
Log.i(TAG, "CLIENT: socket connect succeeded for ${peerId.take(8)} on attempt $attempt")
|
|
||||||
}
|
|
||||||
return sock
|
return sock
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
lastFailure = e
|
lastFailure = e
|
||||||
try { sock?.close() } catch (_: Exception) { }
|
try { sock?.close() } catch (_: Exception) { }
|
||||||
if (attempt < CLIENT_SOCKET_ATTEMPTS) {
|
if (attempt < CLIENT_SOCKET_ATTEMPTS) {
|
||||||
Log.w(TAG, "CLIENT: socket attempt $attempt/$CLIENT_SOCKET_ATTEMPTS failed for ${peerId.take(8)}: ${e.message}; retrying")
|
Log.d(TAG, "CLIENT: socket attempt $attempt/$CLIENT_SOCKET_ATTEMPTS failed for ${peerId.take(8)}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1096,16 +1054,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val mappedPeerId = handleToPeerId[peerHandle]?.takeIf { it.isNotBlank() }
|
val mappedPeerId = handleToPeerId[peerHandle]?.takeIf { it.isNotBlank() }
|
||||||
val peerId = advertisedPeerId ?: mappedPeerId
|
val peerId = advertisedPeerId ?: mappedPeerId
|
||||||
if (peerId == null) {
|
if (peerId == null) {
|
||||||
Log.w(TAG, "SUBSCRIBE: dropped server-ready with no peer mapping and no peer ID payload (payload=${payload.size}B)")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
handleToPeerId[peerHandle] = peerId
|
handleToPeerId[peerHandle] = peerId
|
||||||
subscribeHandles[peerId] = peerHandle
|
subscribeHandles[peerId] = peerHandle
|
||||||
rememberDiscoveredPeer(peerId)
|
rememberDiscoveredPeer(peerId)
|
||||||
if (advertisedPeerId != null && mappedPeerId != null && advertisedPeerId != mappedPeerId) {
|
|
||||||
Log.d(TAG, "SUBSCRIBE: server-ready remapped handle ${mappedPeerId.take(8)} -> ${advertisedPeerId.take(8)}")
|
|
||||||
}
|
|
||||||
return peerId
|
return peerId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1118,7 +1072,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
payload: ByteArray
|
payload: ByteArray
|
||||||
) {
|
) {
|
||||||
if (payload.size < Int.SIZE_BYTES) {
|
if (payload.size < Int.SIZE_BYTES) {
|
||||||
Log.w(TAG, "handleServerReady called with invalid payload size=${payload.size}, dropping")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1126,17 +1079,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (peerId == myPeerID) return
|
if (peerId == myPeerID) return
|
||||||
if (amIServerFor(peerId)) return
|
if (amIServerFor(peerId)) return
|
||||||
if (connectionTracker.peerSockets.containsKey(peerId)) {
|
if (connectionTracker.peerSockets.containsKey(peerId)) {
|
||||||
Log.v(TAG, "↪ already client-connected to $peerId, skipping")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val cancelledServerOffers = connectionTracker.cancelPendingServerDataPaths(peerId)
|
val cancelledServerOffers = connectionTracker.cancelPendingServerDataPaths(peerId)
|
||||||
if (cancelledServerOffers.isNotEmpty()) {
|
if (cancelledServerOffers.isNotEmpty()) {
|
||||||
val cancelled = cancelledServerOffers.joinToString(", ") { it.take(8) }
|
val cancelled = cancelledServerOffers.joinToString(", ") { it.take(8) }
|
||||||
Log.i(TAG, "CLIENT: preempted pending server offer(s) for $cancelled to connect ${peerId.take(8)}")
|
Log.d(TAG, "CLIENT: preempted pending server offer(s) for $cancelled to connect ${peerId.take(8)}")
|
||||||
}
|
}
|
||||||
if (connectionTracker.hasPendingDataPathRequest(peerId)) {
|
if (connectionTracker.hasPendingDataPathRequest(peerId)) {
|
||||||
val pending = connectionTracker.pendingDataPathPeerIds(peerId).joinToString(", ") { it.take(8) }
|
|
||||||
Log.d(TAG, "CLIENT: deferring server-ready for ${peerId.take(8)}; pending Aware data path(s): $pending")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!connectionTracker.addPendingConnection(peerId)) {
|
if (!connectionTracker.addPendingConnection(peerId)) {
|
||||||
@ -1144,10 +1094,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
}
|
}
|
||||||
|
|
||||||
val port = ByteBuffer.wrap(payload, 0, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int
|
val port = ByteBuffer.wrap(payload, 0, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int
|
||||||
Log.i(TAG, "CLIENT: Received server-ready from ${peerId.take(8)} on port $port (payload=${payload.size}B). Requesting network...")
|
|
||||||
|
|
||||||
val subSession = subscribeSession ?: run {
|
val subSession = subscribeSession ?: run {
|
||||||
Log.w(TAG, "CLIENT: subscribe session missing for server-ready from ${peerId.take(8)}")
|
Log.d(TAG, "CLIENT: subscribe session missing for server-ready from ${peerId.take(8)}")
|
||||||
connectionTracker.removePendingConnection(peerId)
|
connectionTracker.removePendingConnection(peerId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1164,12 +1113,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
private val connectStarted = AtomicBoolean(false)
|
private val connectStarted = AtomicBoolean(false)
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
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
|
// Do not bind process for Aware; use per-socket binding instead
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnavailable() {
|
override fun onUnavailable() {
|
||||||
Log.e(TAG, "CLIENT: onUnavailable() - Failed to acquire Aware network for ${peerId.take(8)}")
|
Log.e(TAG, "CLIENT: failed to acquire Aware network for ${peerId.take(8)}")
|
||||||
if (shouldRequestRoleReversalAfterClientFailure(peerId)) {
|
if (shouldRequestRoleReversalAfterClientFailure(peerId)) {
|
||||||
requestRoleReversal(peerId, allowForcedClientOverride = true)
|
requestRoleReversal(peerId, allowForcedClientOverride = true)
|
||||||
}
|
}
|
||||||
@ -1183,7 +1131,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val connectPort = if (info.port > 0) info.port else port
|
val connectPort = if (info.port > 0) info.port else port
|
||||||
// onCapabilitiesChanged can fire multiple times; only connect once
|
// onCapabilitiesChanged can fire multiple times; only connect once
|
||||||
if (!connectStarted.compareAndSet(false, true)) return
|
if (!connectStarted.compareAndSet(false, true)) return
|
||||||
Log.i(TAG, "CLIENT: onCapabilitiesChanged() - Peer IPv6 discovered: $addr port=$connectPort")
|
|
||||||
|
|
||||||
val lp = cm.getLinkProperties(network)
|
val lp = cm.getLinkProperties(network)
|
||||||
val iface = lp?.interfaceName
|
val iface = lp?.interfaceName
|
||||||
@ -1203,7 +1150,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
}
|
}
|
||||||
|
|
||||||
val sock = connectAwareClientSocket(network, scopedAddr, connectPort, peerId)
|
val sock = connectAwareClientSocket(network, scopedAddr, connectPort, peerId)
|
||||||
Log.i(TAG, "CLIENT: TCP connected to ${peerId.take(8)} at $scopedAddr:$connectPort")
|
Log.i(TAG, "Connected to ${peerId.take(8)} (client)")
|
||||||
|
|
||||||
val synced = SyncedSocket(sock)
|
val synced = SyncedSocket(sock)
|
||||||
activeSocket = synced
|
activeSocket = synced
|
||||||
@ -1217,7 +1164,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
// Kick off Noise handshake for this logical peer
|
// Kick off Noise handshake for this logical peer
|
||||||
if (myPeerID < peerId) {
|
if (myPeerID < peerId) {
|
||||||
meshCore.initiateNoiseHandshake(peerId)
|
meshCore.initiateNoiseHandshake(peerId)
|
||||||
Log.i(TAG, "CLIENT: Initiating Noise handshake to ${peerId.take(8)}")
|
|
||||||
}
|
}
|
||||||
// Ensure fast presence even before handshake settles
|
// Ensure fast presence even before handshake settles
|
||||||
serviceScope.launch { delay(150); sendBroadcastAnnounce() }
|
serviceScope.launch { delay(150); sendBroadcastAnnounce() }
|
||||||
@ -1232,12 +1178,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
}
|
}
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
handlePeerDisconnection(peerId, activeSocket)
|
handlePeerDisconnection(peerId, activeSocket)
|
||||||
Log.i(TAG, "CLIENT: WiFi Aware network lost for ${peerId.take(8)}")
|
Log.i(TAG, "Disconnected from ${peerId.take(8)} (client: network lost)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionTracker.addNetworkCallback(peerId, cb)
|
connectionTracker.addNetworkCallback(peerId, cb)
|
||||||
Log.i(TAG, "CLIENT: [Calling requestNetwork] for ${peerId.take(8)}")
|
|
||||||
try {
|
try {
|
||||||
cm.requestNetwork(req, cb, NETWORK_REQUEST_TIMEOUT_MS)
|
cm.requestNetwork(req, cb, NETWORK_REQUEST_TIMEOUT_MS)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -1305,10 +1250,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
ingressLinkID
|
ingressLinkID
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Log.w(
|
Log.w(TAG, "Ignoring unsolicited or cross-link Noise promotion for ${canonicalPeerId.take(8)}")
|
||||||
TAG,
|
|
||||||
"Ignoring unsolicited or cross-link Noise promotion for ${canonicalPeerId.take(8)}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
provisionalWifiClaims.remove(canonicalPeerId, expectedClaim)
|
provisionalWifiClaims.remove(canonicalPeerId, expectedClaim)
|
||||||
@ -1332,18 +1274,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (existingCanonical != provisionalPeerId) {
|
if (existingCanonical != provisionalPeerId) {
|
||||||
Log.w(
|
Log.w(TAG, "Refusing authenticated Wi-Fi rebind ${existingCanonical.take(8)} -> ${canonicalPeerId.take(8)} on existing alias")
|
||||||
TAG,
|
|
||||||
"Refusing authenticated Wi-Fi rebind ${existingCanonical.take(8)} -> ${canonicalPeerId.take(8)} on an existing alias"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connectionTracker.rebindPeerIdIfCurrent(provisionalPeerId, canonicalPeerId, link.transport)) {
|
if (!connectionTracker.rebindPeerIdIfCurrent(provisionalPeerId, canonicalPeerId, link.transport)) {
|
||||||
Log.w(
|
Log.w(TAG, "Ignoring Noise link promotion for ${canonicalPeerId.take(8)}: provisional socket changed")
|
||||||
TAG,
|
|
||||||
"Ignoring Noise link promotion for ${canonicalPeerId.take(8)}: provisional socket changed before rebind"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
authenticatedWifiLinks[canonicalPeerId] =
|
authenticatedWifiLinks[canonicalPeerId] =
|
||||||
@ -1362,10 +1298,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
try { meshCore.setDirectConnection(canonicalPeerId, true) } catch (_: Exception) { }
|
try { meshCore.setDirectConnection(canonicalPeerId, true) } catch (_: Exception) { }
|
||||||
try { meshCore.gossipSyncManager.scheduleInitialSyncToPeer(canonicalPeerId, 1_000) } catch (_: Exception) { }
|
try { meshCore.gossipSyncManager.scheduleInitialSyncToPeer(canonicalPeerId, 1_000) } catch (_: Exception) { }
|
||||||
|
|
||||||
Log.i(
|
Log.i(TAG, "Noise-authenticated Wi-Fi peer ${provisionalPeerId.take(8)} -> ${canonicalPeerId.take(8)}")
|
||||||
TAG,
|
|
||||||
"Noise-authenticated Wi-Fi peer ${provisionalPeerId.take(8)} -> ${canonicalPeerId.take(8)} on exact ingress link"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1396,16 +1329,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
// The socket's discovery identity remains provisional until Noise proves possession
|
// The socket's discovery identity remains provisional until Noise proves possession
|
||||||
// of the claimed static key on this link. A canonical self-signed announcement is
|
// of the claimed static key on this link. A canonical self-signed announcement is
|
||||||
// only TOFU and cannot safely rebind/remove transport state on its own.
|
// only TOFU and cannot safely rebind/remove transport state on its own.
|
||||||
Log.w(
|
Log.d(TAG, "RX: deferred Wi-Fi peer rebind ${logicalPeerId.take(8)} -> ${senderPeerHex.take(8)} pending Noise proof")
|
||||||
TAG,
|
|
||||||
"RX: deferred Wi-Fi peer rebind ${logicalPeerId.take(8)} -> ${senderPeerHex.take(8)} pending Noise proof"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route the packet:
|
// Route the packet:
|
||||||
// - peerID = Originator (who signed it)
|
// - peerID = Originator (who signed it)
|
||||||
// - relayAddress = Neighbor (who sent it to us over this socket)
|
// - relayAddress = Neighbor (who sent it to us over this socket)
|
||||||
Log.d(TAG, "RX: packet type=${pkt.type} from ${senderPeerHex.take(8)} via ${logicalPeerId.take(8)} (bytes=${raw.size})")
|
|
||||||
meshCore.processIncoming(pkt, senderPeerHex, logicalPeerId, ingressLinkID)
|
meshCore.processIncoming(pkt, senderPeerHex, logicalPeerId, ingressLinkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1413,7 +1342,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
clearProvisionalWifiClaimsForLink(logicalPeerId, ingressLinkID)
|
clearProvisionalWifiClaimsForLink(logicalPeerId, ingressLinkID)
|
||||||
|
|
||||||
// Breaking out of the loop means the socket is dead or service is stopping.
|
// Breaking out of the loop means the socket is dead or service is stopping.
|
||||||
Log.i(TAG, "Socket loop terminated for ${logicalPeerId.take(8)} removing peer.")
|
Log.i(TAG, "Disconnected from ${logicalPeerId.take(8)} (socket closed)")
|
||||||
handlePeerDisconnection(logicalPeerId, socket)
|
handlePeerDisconnection(logicalPeerId, socket)
|
||||||
socket.close()
|
socket.close()
|
||||||
}
|
}
|
||||||
@ -1427,8 +1356,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
delay(WIFI_AUTHENTICATION_TIMEOUT_MS)
|
delay(WIFI_AUTHENTICATION_TIMEOUT_MS)
|
||||||
if (provisionalWifiClaims.remove(peerID, claim)) {
|
if (provisionalWifiClaims.remove(peerID, claim)) {
|
||||||
Log.d(TAG, "Expired provisional Wi-Fi authentication claim for ${peerID.take(8)}")
|
Log.d(TAG, "Expired provisional Wi-Fi authentication claim for ${peerID.take(8)}")
|
||||||
}
|
} }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearProvisionalWifiClaimsForLink(relayAddress: String, linkID: String) {
|
private fun clearProvisionalWifiClaimsForLink(relayAddress: String, linkID: String) {
|
||||||
@ -1442,7 +1370,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
|
|
||||||
private fun handleNetworkFailure(peerId: String) {
|
private fun handleNetworkFailure(peerId: String) {
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Network failure cleanup for: $peerId")
|
|
||||||
if (!connectionTracker.isConnected(peerId)) {
|
if (!connectionTracker.isConnected(peerId)) {
|
||||||
val canonicalPeerId = connectionTracker.canonicalPeerId(peerId)
|
val canonicalPeerId = connectionTracker.canonicalPeerId(peerId)
|
||||||
connectionTracker.disconnect(peerId)
|
connectionTracker.disconnect(peerId)
|
||||||
@ -1450,8 +1377,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (canonicalPeerId != peerId) {
|
if (canonicalPeerId != peerId) {
|
||||||
meshCore.removePeer(peerId)
|
meshCore.removePeer(peerId)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Network failure ignored for $peerId - another socket is active")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1462,7 +1387,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val currentSocket = connectionTracker.getSocketForPeer(initialId)
|
val currentSocket = connectionTracker.getSocketForPeer(initialId)
|
||||||
val canonicalPeerId = connectionTracker.canonicalPeerId(initialId)
|
val canonicalPeerId = connectionTracker.canonicalPeerId(initialId)
|
||||||
if (currentSocket === socket) {
|
if (currentSocket === socket) {
|
||||||
Log.d(TAG, "Cleaning up peer: $canonicalPeerId (active socket)")
|
|
||||||
connectionTracker.disconnect(initialId)
|
connectionTracker.disconnect(initialId)
|
||||||
meshCore.removePeer(canonicalPeerId)
|
meshCore.removePeer(canonicalPeerId)
|
||||||
if (canonicalPeerId != initialId) {
|
if (canonicalPeerId != initialId) {
|
||||||
@ -1470,16 +1394,13 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
}
|
}
|
||||||
} else if (socket == null && currentSocket == null) {
|
} else if (socket == null && currentSocket == null) {
|
||||||
// Fallback: If we don't have a specific socket context but we are already disconnected, ensure cleanup
|
// Fallback: If we don't have a specific socket context but we are already disconnected, ensure cleanup
|
||||||
Log.d(TAG, "Cleaning up peer: $initialId (no active socket)")
|
|
||||||
connectionTracker.disconnect(initialId)
|
connectionTracker.disconnect(initialId)
|
||||||
meshCore.removePeer(canonicalPeerId)
|
meshCore.removePeer(canonicalPeerId)
|
||||||
if (canonicalPeerId != initialId) {
|
if (canonicalPeerId != initialId) {
|
||||||
meshCore.removePeer(initialId)
|
meshCore.removePeer(initialId)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Ignored disconnection for $initialId - socket replaced or inactive")
|
|
||||||
// Do not remove peer/session, as a new socket has likely taken over
|
|
||||||
}
|
}
|
||||||
|
// Else: socket replaced or inactive; do not remove peer/session, as a new socket has likely taken over
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
182
docs/security-review-jul-27.md
Normal file
182
docs/security-review-jul-27.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# Bitchat Android — Security Review
|
||||||
|
|
||||||
|
**Commit:** 92d07b22 (worktree: `~/.opencode/worktrees/bitchat-security-review`)
|
||||||
|
**Scope:** Exploitable bugs, privacy violations, tracking opportunities, DoS vectors.
|
||||||
|
**Method:** Read-only static review of `crypto/`, `noise/`, `identity/`, `mesh/`, `protocol/`, `net/`, `service/`, `nostr/`, `geohash/`, `ui/`, `features/`, manifest & build config. All findings verified against source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL
|
||||||
|
|
||||||
|
### C1. Noise session key material (DH shared secrets, chaining keys) written to logcat — ships in release
|
||||||
|
`noise/southernstorm/protocol/SymmetricState.java:135-159` (also 98-104, 168-182)
|
||||||
|
`mixKey()` logs the raw X25519 shared secret ("Input data"), current and new chaining keys in hex. `proguard-rules.pro` has **no** `assumenosideeffects` for `android.util.Log`, so this reaches the release APK. Anyone with the logcat transcript of a handshake can recompute `split()` outputs and fully decrypt the transport session. **Release-blocking.**
|
||||||
|
|
||||||
|
### C2. Remote decompression bomb — pre-auth memory exhaustion via ~30-byte BLE write
|
||||||
|
`protocol/BinaryProtocol.kt:429-454`, `protocol/CompressionUtil.kt:75-118`
|
||||||
|
For compressed v2 packets the decoder reads a 4-byte attacker-controlled `originalSize` and immediately does `ByteArray(originalSize)` — up to 2 GiB per packet. The ratio guard is skipped when `compressedSize == 0` and is 50,000:1 anyway (real deflate max ≈ 1032:1). Runs inline on the GATT callback path; any nearby unauthenticated BLE peer can OOM/crash the foreground service with tiny writes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HIGH
|
||||||
|
|
||||||
|
### H1. Replay-window bit shift direction is wrong — Noise transport replay protection broken
|
||||||
|
`noise/NoiseSession.kt:69-106`
|
||||||
|
Bit indexing is LSB-first, but the window-shift code right-shifts the byte string. A replayed captured frame (nonce 0) tests a zero bit and is accepted; only the single most-recent nonce is reliably blocked. A passive BLE observer can replay DMs, ACKs, and receipts.
|
||||||
|
|
||||||
|
### H2. Announcement signing key not bound to Noise key — "first announce wins" peer-ID impersonation
|
||||||
|
`mesh/AnnouncementIdentityValidator.kt:13-36`, `mesh/SecurityManager.kt:282-313`, `mesh/MessageHandler.kt:265-299`
|
||||||
|
An attacker can replay a victim's Noise public key (→ victim's peerID) inside an announcement signed with the *attacker's* Ed25519 key. On fresh installs or for never-authenticated peers the forged binding wins, and the genuine victim's later announce is rejected as "key replacement".
|
||||||
|
|
||||||
|
### H3. Decrypted private message content written to logcat
|
||||||
|
`mesh/MessageHandler.kt:88` (also 125; `nostr/NostrProtocol.kt:277`)
|
||||||
|
`Log.d` logs the first 30 chars of every decrypted E2E private message plus sender peerID. Not stripped in release. Defeats E2E guarantees for any logcat reader. Related: `ui/DataManager.kt:161-196` dumps the full favorite/fingerprint social graph at startup (Low).
|
||||||
|
|
||||||
|
### H4. E2E-encrypted DM content exposed in system notifications without lock-screen redaction
|
||||||
|
`ui/NotificationManager.kt:224-246`
|
||||||
|
Decrypted DM bodies (mesh + Nostr) are posted with full text in `BigTextStyle`/`InboxStyle`; no `VISIBILITY_SECRET` / lockscreen visibility set. Content renders on lock screen and to any notification-listener app; no opt-out.
|
||||||
|
|
||||||
|
### H5. No signature verification on incoming public Nostr events — relay can forge any user
|
||||||
|
`nostr/GeohashMessageHandler.kt:44-105`, `nostr/LocationNotesManager.kt:358-414`, `nostr/NostrRelayManager.kt:663-706`
|
||||||
|
`NostrEvent.isValidSignature()` (exists, `NostrEvent.kt:171`) is never called for kind 20000/20001/1. A malicious/compromised relay (auto-selected from a daily third-party list, see M9) can inject events with arbitrary pubkeys — impersonating any user in any geohash channel, spoofing participants, caching attacker-chosen nicknames, registering DM aliases.
|
||||||
|
|
||||||
|
### H6. Automatic delivery/read receipts over Nostr = online-presence & location oracle
|
||||||
|
`nostr/NostrDirectMessageHandler.kt:134-138,165-175`, `nostr/NostrTransport.kt:316-384`
|
||||||
|
Any decryptable gift-wrapped DM triggers an automatic signed DELIVERED ack. For geohash DMs the ack uses the public deterministic per-geohash identity, so anyone can probe a target's known geohash pubkey and confirm the device is online *now*, and chart activity patterns. No setting gates this.
|
||||||
|
|
||||||
|
### H7. Building-precision geohash published publicly and permanently in location notes
|
||||||
|
`geohash/LocationChannel.kt:8` (precision 8 ≈ 19×38 m), `nostr/LocationNotesManager.kt:119-165`
|
||||||
|
Kind-1 (relay-archived, persistent) notes carry the 8-char geohash, exact `created_at`, and an optional plaintext nickname, signed by a stable pubkey — a permanent public record "pubkey X was within ~30 m of this spot at time T". REQ `#g` filters also disclose precise location to relays.
|
||||||
|
|
||||||
|
### H8. Deterministic per-geohash identities are stable forever — long-term passive location tracking
|
||||||
|
`nostr/NostrIdentity.kt:137-177`, `ui/GeohashViewModel.kt:184-193`
|
||||||
|
Geohash identity = HMAC(deviceSeed, geohash), never rotated. A passive observer of a channel can recognize returning users months later and assemble their full history in that channel; the plaintext `["n", nickname]` tag (`nostr/NostrProtocol.kt:167-169`) links the same person across channels.
|
||||||
|
|
||||||
|
### H9. Unbounded per-peer actor map and unlimited packet queues — memory/coroutine exhaustion
|
||||||
|
`mesh/PacketProcessor.kt:43-95`, `mesh/BluetoothPacketBroadcaster.kt:122-124`
|
||||||
|
A new coroutine actor with `Channel.UNLIMITED` is created per attacker-chosen `senderID` *before* any security validation, with no eviction. Spraying packets with random sender IDs grows live coroutines + unbounded channels forever.
|
||||||
|
|
||||||
|
### H10. Unauthenticated packet types relayed mesh-wide with attacker-controlled TTL — flood/amplification
|
||||||
|
`mesh/SecurityManager.kt:268-280`, `mesh/PacketRelayManager.kt:59-107,134-163`
|
||||||
|
Signature verification enforced only for ANNOUNCE/MESSAGE/FILE_TRANSFER/LEAVE. FRAGMENT, REQUEST_SYNC, NOISE_* verify unconditionally and are relayed (unconditionally at TTL ≥ 4) with no rate limit. One BLE radio can make the entire mesh re-broadcast forged traffic, draining bandwidth and battery.
|
||||||
|
|
||||||
|
### H11. Unsigned REQUEST_SYNC forces bulk re-broadcast — amplification
|
||||||
|
`mesh/BluetoothMeshService.kt:671-676`, `sync/GossipSyncManager.kt:168-199`
|
||||||
|
A spoofed REQUEST_SYNC with an empty (forgeable) GCS filter makes a victim dump its entire sync cache onto the radio; repeating keeps neighbors transmitting continuously. No rate limit or response budget.
|
||||||
|
|
||||||
|
### H12. ANNOUNCE replay with TTL=7 forces Noise session teardown + fake "direct neighbor"
|
||||||
|
`mesh/SecurityManager.kt:78-90`, `mesh/BluetoothMeshService.kt:586-621`
|
||||||
|
Duplicate ANNOUNCEs are re-accepted at TTL ≥ 7; direct-link is inferred from TTL alone (excluded from the signature, attacker-settable). Replaying a victim's recent ANNOUNCE at TTL=7 repeatedly tears down the victim's Noise sessions, breaking in-flight DMs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MEDIUM
|
||||||
|
|
||||||
|
### M1. No low-order point / all-zero DH rejection — Noise spec violation, key-compromise downgrade
|
||||||
|
`noise/southernstorm/protocol/HandshakeState.java:750-762,1034-1040,1050-1068`
|
||||||
|
Only the all-zero ephemeral key is rejected; other low-order Curve25519 points pass, DH outputs never checked, remote static key unvalidated. A malicious identity with low-order keys yields all-zero DH outputs → publicly derivable session keys. `SecureIdentityStateManager.validatePublicKey()` (identity/…:403-417) is debug-only and its blocklist misses real low-order points.
|
||||||
|
|
||||||
|
### M2. Unbounded crypto work + session state per forged handshake — crypto/memory DoS
|
||||||
|
`noise/NoiseSessionManager.kt:228-252,312-316`
|
||||||
|
Any frame from any spoofed peerID creates a responder session (~3 X25519 scalar mults) inside a `@Synchronized` method, with no cap on half-open responder sessions (staleness check only on the initiate path).
|
||||||
|
|
||||||
|
### M3. Stable 8-byte peerID in BLE scan response — passive long-term tracking
|
||||||
|
`mesh/BluetoothGattServerManager.kt:400-412`, `mesh/BluetoothMeshService.kt:54`
|
||||||
|
The scan response embeds the truncated fingerprint of the *persistent* static Noise identity, surviving restarts and MAC rotation (by design, for dedup). Any passive sniffer can track a user across time/place. Nickname is broadcast in every ANNOUNCE; ANNOUNCE gossip TLVs disclose the user's neighbor graph.
|
||||||
|
|
||||||
|
### M4. Fragment reassembly poisoning — cross-sender fragment-ID collision
|
||||||
|
`mesh/FragmentManager.kt:32,202-263`
|
||||||
|
Reassembly keyed by 8-byte fragment ID only (not sender). Fragments are unsigned; an attacker injects one colliding fragment to destroy a victim's in-flight (up to 1 MB) transfer. Bounded by existing caps; DoS/corruption, not forgery.
|
||||||
|
|
||||||
|
### M5. Legacy plaintext Ed25519 private key can persist after "migration"
|
||||||
|
`crypto/EncryptionService.kt:505-524`
|
||||||
|
Old plaintext key is deleted only if the encrypted store doesn't already have one; otherwise the plaintext private key remains on disk indefinitely.
|
||||||
|
|
||||||
|
### M6. Attacker-controlled image decode bombs / main-thread decode
|
||||||
|
`ui/media/ImageMessageItem.kt:74`, `ui/media/FullScreenImageViewer.kt:75`, `ui/MessageComponents.kt:294-304`
|
||||||
|
Received images decoded with `BitmapFactory.decodeFile` — no bounds check, no `inSampleSize`, on the main thread during composition. A small PNG with huge dimensions → instant OOM when the chat renders. `readBytes()` re-reads whole files on every recomposition.
|
||||||
|
|
||||||
|
### M7. Received files auto-downloaded unencrypted; size limit only on send path
|
||||||
|
`features/file/FileUtils.kt:194-263`, `nostr/NostrDirectMessageHandler.kt:191-194`
|
||||||
|
Nostr DM path accepts up to 10 MB per message (`AppConstants.kt:71`); a malicious contact can fill `cacheDir` indefinitely. Filenames are sanitized (path traversal verified not exploitable).
|
||||||
|
|
||||||
|
### M8. Exported MainActivity acts on attacker-supplied intent extras
|
||||||
|
`AndroidManifest.xml:98-115`, `MainActivity.kt:828-894`
|
||||||
|
Any app can fire intents with `EXTRA_OPEN_PRIVATE_CHAT`/`EXTRA_PEER_ID` to open arbitrary chat sheets and silently clear the victim's pending notifications, or trigger the verification sheet UI. QR payload itself is cryptographically validated — no verification forgery.
|
||||||
|
|
||||||
|
### M9. Relay directory auto-updates daily from third-party GitHub CSV, unsigned/unpinned
|
||||||
|
`nostr/RelayDirectory.kt:29,152-191`
|
||||||
|
Compromise of `permissionlesstech/georelays` steers all users to attacker relays → precise `#g` filters + forged-event injection (H5).
|
||||||
|
|
||||||
|
### M10. Geohash subscription filters & geo-nearest relay selection leak location to relays/observers
|
||||||
|
`nostr/NostrRelayManager.kt:131-147`, `nostr/RelayDirectory.kt:88-106`, `nostr/NostrFilter.kt:37-71`
|
||||||
|
Each relay learns subscribed cells; `#p` DM filters reveal owned pubkeys. Mitigated: Tor ON by default, fail-closed proxy config (`net/ArtiTorManager.kt:151,224`).
|
||||||
|
|
||||||
|
### M11. Unbounded outbound Nostr message queue — never drained, re-sent on reconnect
|
||||||
|
`nostr/NostrRelayManager.kt:107,286-288,862-871`
|
||||||
|
`messageQueue` entries are never removed after send; every relay reconnect re-sends full history (duplicate gift wraps, memory growth, extra metadata).
|
||||||
|
|
||||||
|
### M12. Unbounded identity-keyed caches — memory DoS by malicious relay (compounds H5)
|
||||||
|
`nostr/GeohashRepository.kt:22-29,61,104-118`
|
||||||
|
`geohashParticipants`, `geoNicknames`, etc. grow per unique pubkey with no eviction; forged events from unlimited fresh pubkeys exhaust memory. No WebSocket frame size cap (`NostrRelayManager.kt:874-876`).
|
||||||
|
|
||||||
|
### M13. "NIP-44" DM encryption omits padding — exact plaintext length leakage; not real NIP-44
|
||||||
|
`nostr/NostrCrypto.kt:211-260,267-293`
|
||||||
|
Raw XChaCha20-Poly1305 over unpadded UTF-8; relays/observers see exact DM lengths (receipt vs message vs file) and it's incompatible with real NIP-44 clients. AEAD itself sound.
|
||||||
|
|
||||||
|
### M14. Peer-table flooding with throwaway identities
|
||||||
|
`mesh/PeerManager.kt:93,228-240,551-564`
|
||||||
|
No cap on the peers map; cheap self-signed ANNOUNCEs create verified entries and can each trigger Noise handshakes. 3-minute sweep is the only bound.
|
||||||
|
|
||||||
|
### M15. No rate limiting on GATT writes / inbound packet processing
|
||||||
|
`mesh/BluetoothGattServerManager.kt:232-277`, `mesh/BluetoothGattClientManager.kt:644-656`
|
||||||
|
Every write is fully decoded/verified with no per-connection or global rate limit; combined with H9 guarantees backlog growth.
|
||||||
|
|
||||||
|
### M16. Two independent persistent Ed25519 signing identities per device
|
||||||
|
`crypto/EncryptionService.kt:485-503` vs `identity/SecureIdentityStateManager.kt:145-195`
|
||||||
|
Different keys in different pref files; same device presents two signing identities; panic wipe doesn't rotate both. Also: `EncryptionService.sign()` returns an empty signature and `verify()` ignores its inputs entirely (EncryptionService.kt:226-242) — a latent trap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LOW
|
||||||
|
|
||||||
|
- **L1. Metadata leakage:** only NOISE_* frames padded; public MESSAGE/ANNOUNCE/FILE_TRANSFER leak exact sizes; every packet carries ms wall-clock timestamps (`mesh/BLEPacketPaddingPolicy.kt:11-17`, `protocol/BinaryProtocol.kt:42-43,76`).
|
||||||
|
- **L2. Signed MESSAGE/FILE_TRANSFER replayable after 5-min dedup expiry** — no freshness check (`mesh/SecurityManager.kt:55-73,101`).
|
||||||
|
- **L3. Silent identity regeneration on key-store corruption** — masks tampering, destroys identity on transient Keystore failure (`crypto/EncryptionService.kt:466-483`, `noise/NoiseEncryptionService.kt:85-119`).
|
||||||
|
- **L4. Channel KDF salt = channel name; PBKDF2 mislabeled as "Argon2id" in comments; passwords retained in memory; dead plaintext channel-key-sharing packet code** (`noise/NoiseChannelEncryption.kt:148-168,203-243`).
|
||||||
|
- **L5. Reverse geocoding sends precise GPS to OSM Nominatim (or Google Fused) with app-identifying UA** — rides Tor when on (`geohash/OpenStreetMapGeocoderProvider.kt:21-29`).
|
||||||
|
- **L6. Persistent account Nostr identity links all DM activity; `#p` filters announce pubkey ownership to relays** — mitigated by randomized gift-wrap timestamps (`nostr/NostrIdentity.kt:108-128`).
|
||||||
|
- **L7. Predictable subscription IDs (`sub-<millis>-<rand>`) aid per-session correlation** (`nostr/NostrRelayManager.kt:809-811`).
|
||||||
|
- **L8. NIP-17 validation gaps:** rumor kind not checked to be 14; replay window uses attacker-controlled `created_at`. Sender spoofing inside gift wraps *is* correctly prevented (`nostr/NostrProtocol.kt:77-96`).
|
||||||
|
- **L9. No FLAG_SECURE anywhere; Recents preview leaks chats on API < 33** (`MainActivity.kt:84-86`).
|
||||||
|
- **L10. Clipboard copies not marked sensitive** (`ui/ChatUserSheet.kt:87`, `ui/SecurityVerificationSheet.kt:411`).
|
||||||
|
- **L11. WebView with JS + file access + unescaped geohash interpolation into `evaluateJavascript`** — activity not exported, limited impact (`ui/GeohashPickerActivity.kt:104-148`).
|
||||||
|
- **L12. `packet.recipientID != SpecialRecipients.BROADCAST` is ByteArray reference comparison — always true** (`mesh/BluetoothPacketBroadcaster.kt:340`).
|
||||||
|
- **L13. GATT server ignores preparedWrite/offset; force-unwrap NPEs on hot paths degrade gracefully** (`mesh/BluetoothGattServerManager.kt:232-239`, `mesh/PacketProcessor.kt:127`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verified positives
|
||||||
|
|
||||||
|
- SecureRandom everywhere; no weak RNG.
|
||||||
|
- Keys in EncryptedSharedPreferences (AES256-GCM Keystore master key); `allowBackup="false"` + backup rules.
|
||||||
|
- No plaintext message DB; messages in-memory only; message IDs in encrypted prefs.
|
||||||
|
- PeerID = SHA-256(static key)[:8] with session establishment refusing mismatches; session-generation binding defeats re-handshake downgrade.
|
||||||
|
- AEAD verify-before-decrypt, constant-time tag check, nonce-wraparound enforcement, rekey limits, zeroization on destroy.
|
||||||
|
- NIP-17 seal signature verified + `seal.pubkey == rumor.pubkey` enforced; Schnorr nonces use SecureRandom.
|
||||||
|
- Tor fail-closed (proxy set before bootstrap; clients rebuilt on mode change); no telemetry/analytics SDKs; all traffic wss/https.
|
||||||
|
- Filename sanitization blocks path traversal; FileProvider per-URI grants; no auto-opening URLs; links require explicit tap and are coerced to https.
|
||||||
|
- Fragment caps (256/ID, 1 MB/set, 4 MB global, 30 s timeout); dedup caches bounded; connection limits + backoff; presence heartbeats only at city-level precision with jitter.
|
||||||
|
|
||||||
|
## Top remediations (priority order)
|
||||||
|
|
||||||
|
1. Strip all key/content logging (`SymmetricState`, `MessageHandler.kt:88`); add ProGuard `assumenosideeffects` for `android.util.Log`. *(C1, H3)*
|
||||||
|
2. Hard-cap `originalSize` in `CompressionUtil.decompress` (~1–2 MB); tighten ratio to ≈1032:1. *(C2)*
|
||||||
|
3. Fix replay-window shift direction + unit tests; reject low-order points / all-zero DH in `mixDH`. *(H1, M1)*
|
||||||
|
4. Verify `isValidSignature()` on all incoming Nostr events; pin/sign the relay directory. *(H5, M9)*
|
||||||
|
5. Cross-sign Ed key with Noise key (proof-of-possession) in announcements. *(H2)*
|
||||||
|
6. Bound PacketProcessor actor map (validate before actor creation), bound queues, authenticate/rate-limit FRAGMENT & REQUEST_SYNC relay, budget sync responses. *(H9–H11)*
|
||||||
|
7. Key fragment reassembly by (senderID, fragmentID); rotate the advertised peerID. *(M4, M3)*
|
||||||
|
8. Gate auto delivery/read receipts behind a privacy setting; warn about location-note precision/permanence. *(H6, H7)*
|
||||||
|
9. Lock-screen-redact DM notifications; image decode bounds checks off main thread. *(H4, M6)*
|
||||||
|
10. Bound Nostr `messageQueue` (drain after send) and `GeohashRepository` caches. *(M11, M12)*
|
||||||
Loading…
x
Reference in New Issue
Block a user