@offline-protocol/mesh-sdk 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +1 -1
  2. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  3. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -1
  4. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +1 -1
  5. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +1 -1
  6. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  7. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values +0 -0
  8. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  9. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.s +1 -1
  10. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  11. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
  12. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
  13. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
  14. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  15. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
  16. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  17. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
  18. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
  19. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
  20. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values +0 -0
  21. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  22. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
  23. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/inline-functions.tab.values.at +0 -0
  24. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  25. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
  26. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
  27. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
  28. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values +0 -0
  29. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  30. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.s +1 -1
  31. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
  32. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  33. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
  34. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
  35. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
  36. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values +0 -0
  37. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  38. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s +1 -1
  39. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
  40. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  41. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  42. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  43. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  44. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  45. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  46. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values +0 -0
  47. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  48. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.s +1 -1
  49. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  50. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  51. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  52. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  53. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  54. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$AdvertisementCacheEntry.class +0 -0
  55. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$Companion.class +0 -0
  56. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$MeshObservation.class +0 -0
  57. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$PendingFragment.class +0 -0
  58. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$fragmentPollingRunnable$1.class +0 -0
  59. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$gattClientCallback$1.class +0 -0
  60. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$gattServerCallback$1.class +0 -0
  61. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$resolveTargetAddress$$inlined$sortedBy$1.class +0 -0
  62. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$routingCleanupRunnable$1.class +0 -0
  63. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$scanWatchdogRunnable$1.class +0 -0
  64. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$startAdvertising$1.class +0 -0
  65. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$startConnectionMonitor$1.class +0 -0
  66. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$startScanning$1.class +0 -0
  67. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager.class +0 -0
  68. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$Companion.class +0 -0
  69. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$messagePollingRunnable$1.class +0 -0
  70. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$pingRunnable$1.class +0 -0
  71. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$webSocketListener$1.class +0 -0
  72. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager.class +0 -0
  73. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/LogThrottler.class +0 -0
  74. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$Companion.class +0 -0
  75. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$Constants.class +0 -0
  76. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$ParsedConfig.class +0 -0
  77. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$create$1.class +0 -0
  78. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$create$3$1.class +0 -0
  79. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$create$5$1.class +0 -0
  80. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule.class +0 -0
  81. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolPackage.class +0 -0
  82. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$AlreadyRunning.class +0 -0
  83. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$InvalidState.class +0 -0
  84. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$NotAvailable.class +0 -0
  85. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$NotRunning.class +0 -0
  86. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$PermissionDenied.class +0 -0
  87. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$PlatformError.class +0 -0
  88. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$StartFailed.class +0 -0
  89. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException.class +0 -0
  90. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportManager.class +0 -0
  91. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportManagerListener.class +0 -0
  92. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportState.class +0 -0
  93. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$Companion.class +0 -0
  94. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$messagePollingRunnable$1.class +0 -0
  95. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$p2pReceiver$1.class +0 -0
  96. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$startPeerDiscovery$1.class +0 -0
  97. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$stopPeerDiscovery$1.class +0 -0
  98. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager.class +0 -0
  99. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/ble/MeshConnectionRegistry.class +0 -0
  100. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshAdvertisementData$Companion.class +0 -0
  101. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshAdvertisementData.class +0 -0
  102. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$Companion.class +0 -0
  103. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$ConnectionIntent.class +0 -0
  104. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$MeshConfig.class +0 -0
  105. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$MeshDecision.class +0 -0
  106. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$MeshRole.class +0 -0
  107. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$PeerMetrics.class +0 -0
  108. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$PeerState.class +0 -0
  109. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$RebalanceDirective.class +0 -0
  110. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$RemoteCandidate.class +0 -0
  111. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$ScoreWeights.class +0 -0
  112. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController.class +0 -0
  113. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/AckConfig$Companion.class +0 -0
  114. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/AckConfig.class +0 -0
  115. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/BleFragment$Companion.class +0 -0
  116. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/BleFragment.class +0 -0
  117. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupConfig$Companion.class +0 -0
  118. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupConfig.class +0 -0
  119. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupStats$Companion.class +0 -0
  120. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupStats.class +0 -0
  121. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/Disposable$Companion.class +0 -0
  122. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/Disposable.class +0 -0
  123. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DorsConfig$Companion.class +0 -0
  124. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DorsConfig.class +0 -0
  125. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/EventCallback$Companion.class +0 -0
  126. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/EventCallback.class +0 -0
  127. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverter$DefaultImpls.class +0 -0
  128. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverter.class +0 -0
  129. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterBoolean.class +0 -0
  130. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterCallbackInterface.class +0 -0
  131. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterFloat.class +0 -0
  132. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalShort.class +0 -0
  133. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalString.class +0 -0
  134. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeBleFragment.class +0 -0
  135. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeFileProgress.class +0 -0
  136. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeInternetMessage.class +0 -0
  137. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeRouteEntry.class +0 -0
  138. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeTransportMetrics.class +0 -0
  139. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeWifiDirectMessage.class +0 -0
  140. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalUByte.class +0 -0
  141. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalULong.class +0 -0
  142. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterRustBuffer.class +0 -0
  143. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceString.class +0 -0
  144. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeMessageStats.class +0 -0
  145. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeNetworkLink.class +0 -0
  146. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeNetworkNode.class +0 -0
  147. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeRouteEntry.class +0 -0
  148. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceUByte.class +0 -0
  149. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterShort.class +0 -0
  150. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterString.class +0 -0
  151. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeAckConfig.class +0 -0
  152. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeBleFragment.class +0 -0
  153. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeDedupConfig.class +0 -0
  154. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeDedupStats.class +0 -0
  155. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeDorsConfig.class +0 -0
  156. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeEventCallback.class +0 -0
  157. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeFileProgress.class +0 -0
  158. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeGradientRoutingConfig.class +0 -0
  159. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeInternetMessage.class +0 -0
  160. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeMessagePriority.class +0 -0
  161. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeMessageStats.class +0 -0
  162. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeNetworkLink.class +0 -0
  163. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeNetworkNode.class +0 -0
  164. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeNetworkTopology.class +0 -0
  165. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeOfflineProtocol.class +0 -0
  166. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypePathConfig.class +0 -0
  167. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypePeerDevice.class +0 -0
  168. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolConfig.class +0 -0
  169. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolConfigExtended.class +0 -0
  170. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolError.class +0 -0
  171. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolState.class +0 -0
  172. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRelayConfig.class +0 -0
  173. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRelayPriority.class +0 -0
  174. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeReliabilityConfig.class +0 -0
  175. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRetryConfig.class +0 -0
  176. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRouteEntry.class +0 -0
  177. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRoutingStats.class +0 -0
  178. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeTransportConfig.class +0 -0
  179. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeTransportMetrics.class +0 -0
  180. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeTransportType.class +0 -0
  181. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeWifiDirectMessage.class +0 -0
  182. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterUByte.class +0 -0
  183. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterUInt.class +0 -0
  184. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterULong.class +0 -0
  185. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterUShort.class +0 -0
  186. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FileProgress$Companion.class +0 -0
  187. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FileProgress.class +0 -0
  188. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ForeignBytes$ByValue.class +0 -0
  189. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ForeignBytes.class +0 -0
  190. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/GradientRoutingConfig$Companion.class +0 -0
  191. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/GradientRoutingConfig.class +0 -0
  192. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/IntegrityCheckingUniffiLib.class +0 -0
  193. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/InternalException.class +0 -0
  194. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/InternetMessage$Companion.class +0 -0
  195. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/InternetMessage.class +0 -0
  196. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/JavaLangRefCleanable.class +0 -0
  197. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/JavaLangRefCleaner.class +0 -0
  198. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessagePriority$Companion.class +0 -0
  199. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessagePriority.class +0 -0
  200. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessageStats$Companion.class +0 -0
  201. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessageStats.class +0 -0
  202. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkLink$Companion.class +0 -0
  203. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkLink.class +0 -0
  204. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkNode$Companion.class +0 -0
  205. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkNode.class +0 -0
  206. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkTopology$Companion.class +0 -0
  207. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkTopology.class +0 -0
  208. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NoHandle.class +0 -0
  209. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocol$Companion.class +0 -0
  210. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocol$UniffiCleanAction.class +0 -0
  211. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocol.class +0 -0
  212. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocolInterface$Companion.class +0 -0
  213. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocolInterface.class +0 -0
  214. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/Offline_protocolKt.class +0 -0
  215. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PathConfig$Companion.class +0 -0
  216. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PathConfig.class +0 -0
  217. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PeerDevice$Companion.class +0 -0
  218. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PeerDevice.class +0 -0
  219. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfig$Companion.class +0 -0
  220. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfig.class +0 -0
  221. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfigExtended$Companion.class +0 -0
  222. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfigExtended.class +0 -0
  223. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$AlreadyStarted.class +0 -0
  224. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$ErrorHandler.class +0 -0
  225. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$InvalidConfiguration.class +0 -0
  226. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$InvalidState.class +0 -0
  227. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$NotStarted.class +0 -0
  228. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$Other.class +0 -0
  229. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$SendFailed.class +0 -0
  230. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException.class +0 -0
  231. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolState$Companion.class +0 -0
  232. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolState.class +0 -0
  233. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayConfig$Companion.class +0 -0
  234. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayConfig.class +0 -0
  235. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayPriority$Companion.class +0 -0
  236. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayPriority.class +0 -0
  237. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ReliabilityConfig$Companion.class +0 -0
  238. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ReliabilityConfig.class +0 -0
  239. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RetryConfig$Companion.class +0 -0
  240. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RetryConfig.class +0 -0
  241. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RouteEntry$Companion.class +0 -0
  242. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RouteEntry.class +0 -0
  243. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RoutingStats$Companion.class +0 -0
  244. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RoutingStats.class +0 -0
  245. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer$ByReference.class +0 -0
  246. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer$ByValue.class +0 -0
  247. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer$Companion.class +0 -0
  248. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer.class +0 -0
  249. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportConfig$Companion.class +0 -0
  250. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportConfig.class +0 -0
  251. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportMetrics$Companion.class +0 -0
  252. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportMetrics.class +0 -0
  253. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportType$Companion.class +0 -0
  254. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportType.class +0 -0
  255. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCallbackInterfaceClone.class +0 -0
  256. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCallbackInterfaceEventCallbackMethod0.class +0 -0
  257. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCallbackInterfaceFree.class +0 -0
  258. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCleaner$Cleanable.class +0 -0
  259. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCleaner$Companion.class +0 -0
  260. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCleaner.class +0 -0
  261. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteF32.class +0 -0
  262. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteF64.class +0 -0
  263. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI16.class +0 -0
  264. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI32.class +0 -0
  265. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI64.class +0 -0
  266. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI8.class +0 -0
  267. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteRustBuffer.class +0 -0
  268. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU16.class +0 -0
  269. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU32.class +0 -0
  270. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU64.class +0 -0
  271. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU8.class +0 -0
  272. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteVoid.class +0 -0
  273. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureDroppedCallback.class +0 -0
  274. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureDroppedCallbackStruct$UniffiByValue.class +0 -0
  275. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureDroppedCallbackStruct.class +0 -0
  276. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF32$UniffiByValue.class +0 -0
  277. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF32.class +0 -0
  278. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF64$UniffiByValue.class +0 -0
  279. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF64.class +0 -0
  280. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI16$UniffiByValue.class +0 -0
  281. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI16.class +0 -0
  282. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI32$UniffiByValue.class +0 -0
  283. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI32.class +0 -0
  284. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI64$UniffiByValue.class +0 -0
  285. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI64.class +0 -0
  286. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI8$UniffiByValue.class +0 -0
  287. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI8.class +0 -0
  288. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultRustBuffer$UniffiByValue.class +0 -0
  289. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultRustBuffer.class +0 -0
  290. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU16$UniffiByValue.class +0 -0
  291. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU16.class +0 -0
  292. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU32$UniffiByValue.class +0 -0
  293. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU32.class +0 -0
  294. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU64$UniffiByValue.class +0 -0
  295. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU64.class +0 -0
  296. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU8$UniffiByValue.class +0 -0
  297. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU8.class +0 -0
  298. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultVoid$UniffiByValue.class +0 -0
  299. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultVoid.class +0 -0
  300. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiHandleMap.class +0 -0
  301. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiJnaCleanable.class +0 -0
  302. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiJnaCleaner.class +0 -0
  303. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiLib.class +0 -0
  304. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiNullRustCallStatusErrorHandler.class +0 -0
  305. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatus$ByValue.class +0 -0
  306. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatus$Companion.class +0 -0
  307. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatus.class +0 -0
  308. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatusErrorHandler.class +0 -0
  309. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustFutureContinuationCallback.class +0 -0
  310. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiVTableCallbackInterfaceEventCallback$UniffiByValue.class +0 -0
  311. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiVTableCallbackInterfaceEventCallback.class +0 -0
  312. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiWithHandle.class +0 -0
  313. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/WifiDirectMessage$Companion.class +0 -0
  314. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/WifiDirectMessage.class +0 -0
  315. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback$onEvent.class +0 -0
  316. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback$uniffiClone.class +0 -0
  317. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback$uniffiFree.class +0 -0
  318. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback.class +0 -0
  319. package/android/src/main/java/com/offlineprotocol/BleManager.kt +551 -33
  320. package/ios/BleManager.swift +323 -17
  321. package/package.json +1 -1
  322. /package/android/build/tmp/kotlin-classes/debug/META-INF/{offlineprotocol_react-native_debug.kotlin_module → offline-protocol_mesh-sdk_debug.kotlin_module} +0 -0
@@ -71,16 +71,21 @@ class BleManager(
71
71
  private const val FRAGMENT_POLL_INTERVAL_MS = 100L // 100ms
72
72
  private const val MAX_FRAGMENT_SIZE = 185
73
73
  private const val CONNECTION_TIMEOUT_MS = 10000L
74
- private const val SCAN_WATCHDOG_INTERVAL_MS = 20000L
74
+ private const val SCAN_WATCHDOG_INTERVAL_MS = 30000L // Match iOS timing
75
75
  private const val SCAN_WATCHDOG_HEARTBEAT_MS = 10000L
76
76
  private const val MAX_CONNECTIONS_PER_DEVICE = 4
77
77
  private const val ADVERTISE_RESTART_MIN_MS = 200L
78
78
  private const val ADVERTISE_RESTART_MAX_MS = 1200L
79
79
  private const val MIN_ADVERTISE_INTERVAL_MS = 1500L
80
+ private const val MIN_RECONNECT_INTERVAL_MS = 5_000L // Match iOS timing
81
+ private const val MAX_RECONNECT_INTERVAL_MS = 60_000L
82
+ private const val MAX_CONNECTION_RETRIES = 5
80
83
 
81
84
  // Adaptive Scan Configuration
82
- /** Minimum RSSI to consider for connection (filter weak signals early) */
85
+ /** Minimum RSSI to consider for connection (filter weak signals early) - matches iOS */
83
86
  private const val ADAPTIVE_MIN_RSSI = -85
87
+ /** Absolute minimum RSSI below which we refuse to connect - matches iOS */
88
+ private const val MINIMUM_RSSI_TO_CONNECT = -90
84
89
  /** Peer count threshold below which we process all advertisements */
85
90
  private const val ADAPTIVE_LOW_DENSITY_THRESHOLD = 10
86
91
  /** Peer count threshold above which we apply maximum throttling */
@@ -91,6 +96,24 @@ class BleManager(
91
96
  private const val ADAPTIVE_COOLDOWN_PER_DEVICE_MS = 30_000L
92
97
  /** Interval for updating visible peer count estimate (ms) */
93
98
  private const val ADAPTIVE_PEER_COUNT_WINDOW_MS = 5_000L
99
+ /** Rate limit for unknown device GATT verification attempts (ms) */
100
+ private const val UNKNOWN_DEVICE_RATE_LIMIT_MS = 5_000L // More aggressive for cross-platform discovery
101
+ /** Minimum RSSI for unknown device to be considered for GATT verification */
102
+ private const val UNKNOWN_DEVICE_MIN_RSSI = -80 // More lenient for cross-platform discovery
103
+ /** Maximum unknown device verification attempts per minute */
104
+ private const val MAX_UNKNOWN_DEVICE_ATTEMPTS_PER_MINUTE = 10
105
+ /** Proactive scan refresh interval even when discoveries are occurring (ms) */
106
+ private const val PROACTIVE_SCAN_REFRESH_MS = 60_000L
107
+ /** Force a complete BLE stack refresh periodically even when things seem healthy (ms) */
108
+ private const val FORCED_BLE_REFRESH_MS = 120_000L
109
+ /** Maximum consecutive scan restarts before resetting BLE adapter */
110
+ private const val MAX_CONSECUTIVE_SCAN_RESTARTS = 3
111
+ /** Backoff period after resetting BLE adapter (ms) */
112
+ private const val ADAPTER_RESET_BACKOFF_MS = 45_000L
113
+ /** Connection monitor interval for periodic reconnection attempts (ms) */
114
+ private const val CONNECTION_MONITOR_INTERVAL_MS = 5_000L
115
+ /** Initial aggressive discovery phase duration (ms) - more frequent scanning initially */
116
+ private const val AGGRESSIVE_DISCOVERY_PHASE_MS = 30_000L
94
117
  }
95
118
 
96
119
  // MARK: - Properties
@@ -129,6 +152,7 @@ class BleManager(
129
152
  private data class MeshObservation(val advertisement: MeshAdvertisementData, val rssi: Int?, val timestamp: Long)
130
153
  private val pendingFragments = ConcurrentHashMap<String, MutableList<PendingFragment>>()
131
154
  private val PENDING_FRAGMENT_TIMEOUT_MS = 5000L
155
+ private val connectionRetryCount = ConcurrentHashMap<String, Int>()
132
156
  private val LOAD_SATURATION_COUNT = 20
133
157
  private val MESH_OBSERVATION_TTL_MS = 120_000L
134
158
  private val deviceIdResolutionAttempts = ConcurrentHashMap<String, Long>()
@@ -147,6 +171,23 @@ class BleManager(
147
171
  /** Last time we updated the peer count estimate */
148
172
  @Volatile private var lastPeerCountUpdate: Long = 0L
149
173
  @Volatile private var lastMeshAdvertisement: MeshAdvertisementData? = null
174
+ /** Rate limiting for unknown connectable devices that need GATT verification */
175
+ private val unknownDeviceAttempts = ConcurrentHashMap<String, Long>()
176
+ /** Last time we proactively refreshed the scan */
177
+ @Volatile private var lastProactiveScanRefresh: Long = 0L
178
+ /** Last time we performed a forced BLE refresh */
179
+ @Volatile private var lastForcedBleRefresh: Long = 0L
180
+ /** Tracks recently seen advertisements to avoid duplicate processing (hash, timestamp) */
181
+ private data class AdvertisementCacheEntry(val hash: Int, val timestamp: Long)
182
+ private val recentAdvertisementHashes = ConcurrentHashMap<String, AdvertisementCacheEntry>()
183
+ /** Tracks connected centrals (devices that connected to our GATT server) for connection count */
184
+ private val connectedCentrals = ConcurrentHashMap<String, Long>()
185
+ /** Counter for consecutive scan restarts without discoveries */
186
+ @Volatile private var scanRestartCount = 0
187
+ /** Last time we reset the BLE adapter */
188
+ @Volatile private var lastAdapterReset: Long = 0L
189
+ /** Connection monitor runnable for periodic reconnection attempts */
190
+ private var connectionMonitorRunnable: Runnable? = null
150
191
 
151
192
  // Fragment polling
152
193
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -214,13 +255,58 @@ class BleManager(
214
255
  Log.w(TAG, "Restarting BLE scan after ${idleMs}ms of inactivity")
215
256
  emitDiagnostic("warning", "Restarting BLE scan due to inactivity", mapOf("idleMs" to idleMs))
216
257
  }
258
+ scanRestartCount++
217
259
  restartScanning("watchdog")
260
+ evaluateBleHealthAfterRestart()
218
261
  return
219
262
  }
220
263
  mainHandler.postDelayed(this, SCAN_WATCHDOG_HEARTBEAT_MS)
221
264
  }
222
265
  }
223
266
 
267
+ /**
268
+ * Evaluates BLE stack health after consecutive restarts and resets adapter if needed.
269
+ * This mirrors iOS's evaluateCentralHealthAfterRestart mechanism.
270
+ */
271
+ private fun evaluateBleHealthAfterRestart() {
272
+ if (scanRestartCount < MAX_CONSECUTIVE_SCAN_RESTARTS) {
273
+ return
274
+ }
275
+
276
+ val now = System.currentTimeMillis()
277
+ if (lastAdapterReset > 0 && now - lastAdapterReset < ADAPTER_RESET_BACKOFF_MS) {
278
+ return
279
+ }
280
+
281
+ Log.w(TAG, "Resetting BLE stack due to repeated scan stalls (restartCount=$scanRestartCount)")
282
+ emitDiagnostic("warning", "Resetting BLE stack due to repeated scan stalls", mapOf(
283
+ "restartCount" to scanRestartCount
284
+ ))
285
+
286
+ lastAdapterReset = now
287
+ scanRestartCount = 0
288
+
289
+ // Force stop and restart everything
290
+ mainHandler.post {
291
+ if (state == TransportState.RUNNING) {
292
+ stopScanning("ble_reset")
293
+ stopAdvertising()
294
+
295
+ // Re-initialize scanner and advertiser
296
+ bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
297
+ bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
298
+
299
+ // Restart after a brief delay
300
+ mainHandler.postDelayed({
301
+ if (state == TransportState.RUNNING) {
302
+ startScanning("ble_reset")
303
+ startAdvertising("ble_reset")
304
+ }
305
+ }, 1000)
306
+ }
307
+ }
308
+ }
309
+
224
310
  // Metrics
225
311
  private var bytesSent: Long = 0
226
312
  private var bytesReceived: Long = 0
@@ -409,7 +495,15 @@ class BleManager(
409
495
  pendingFragments.clear()
410
496
  pendingOutboundFragments.clear()
411
497
  lastSeenMeshAdvertisements.clear()
498
+ unknownDeviceAttempts.clear()
499
+ recentAdvertisementHashes.clear()
500
+ connectionRetryCount.clear()
501
+ connectedCentrals.clear()
502
+ scanRestartCount = 0
503
+ lastAdapterReset = 0L
412
504
  transportStartAt = 0L
505
+ lastProactiveScanRefresh = 0L
506
+ lastForcedBleRefresh = 0L
413
507
 
414
508
  // Close GATT server
415
509
  gattServer?.close()
@@ -570,9 +664,11 @@ class BleManager(
570
664
  .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
571
665
  .build()
572
666
 
573
- val scanFilter = ScanFilter.Builder()
574
- .setServiceUuid(ParcelUuid(SERVICE_UUID))
575
- .build()
667
+ // CRITICAL FIX: Scan without service UUID filter for iOS ↔ Android interoperability
668
+ // iOS's CoreBluetooth has known issues recognizing 128-bit service UUIDs from Android
669
+ // advertisements, and vice versa. Scanning without filter and filtering in software
670
+ // ensures we discover all mesh devices regardless of platform quirks.
671
+ // We filter in handleScanResult using shouldProcessDiscoveredDevice().
576
672
 
577
673
  scanCallback = object : ScanCallback() {
578
674
  override fun onScanResult(callbackType: Int, result: ScanResult) {
@@ -600,14 +696,28 @@ class BleManager(
600
696
  }
601
697
  }
602
698
 
603
- scanner.startScan(listOf(scanFilter), scanSettings, scanCallback)
699
+ // Scan without filter - we'll filter in software for cross-platform compatibility
700
+ scanner.startScan(null, scanSettings, scanCallback)
604
701
  isScanning = true
605
- lastDiscoveryAt = System.currentTimeMillis()
702
+ val now = System.currentTimeMillis()
703
+ lastDiscoveryAt = now
704
+ lastProactiveScanRefresh = now
705
+ // Reset restart count on non-watchdog starts
706
+ if (reason != "restart_watchdog") {
707
+ scanRestartCount = 0
708
+ }
606
709
  scheduleScanWatchdog()
710
+ startConnectionMonitor()
607
711
  if (logThrottler.shouldLog("scan_started")) {
608
- Log.i(TAG, "BLE scanning started successfully (reason: $reason)")
609
- emitDiagnostic("info", "BLE scanning started", mapOf("reason" to reason))
712
+ Log.i(TAG, "BLE scanning started (no filter, reason: $reason)")
713
+ emitDiagnostic("info", "BLE scanning started", mapOf(
714
+ "reason" to reason,
715
+ "filterless" to true
716
+ ))
610
717
  }
718
+
719
+ // Rehydrate previously connected devices to avoid waiting for advertisements
720
+ rehydratePreviouslyConnectedDevices()
611
721
  } catch (e: SecurityException) {
612
722
  Log.e(TAG, "Permission denied while starting scan", e)
613
723
  emitDiagnostic("error", "Permission denied while starting scan", mapOf("exception" to e.javaClass.simpleName, "message" to (e.message ?: "unknown")))
@@ -615,6 +725,28 @@ class BleManager(
615
725
  }
616
726
  }
617
727
 
728
+ /**
729
+ * Attempts to reconnect to previously known devices without waiting for advertisements.
730
+ * This speeds up rediscovery after app restart or Bluetooth toggle.
731
+ */
732
+ private fun rehydratePreviouslyConnectedDevices() {
733
+ try {
734
+ val bondedDevices = bluetoothAdapter?.bondedDevices ?: return
735
+ for (device in bondedDevices) {
736
+ val address = device.address
737
+ // Only attempt if we previously had this device in our registry
738
+ if (connections.hasDeviceForAddress(address) && connections.getGatt(address) == null) {
739
+ if (logThrottler.shouldLog("rehydrate_$address", intervalMs = 30_000)) {
740
+ Log.d(TAG, "Rehydrating connection to bonded device: $address")
741
+ }
742
+ connectToDevice(device)
743
+ }
744
+ }
745
+ } catch (e: SecurityException) {
746
+ Log.w(TAG, "Cannot access bonded devices for rehydration", e)
747
+ }
748
+ }
749
+
618
750
  private fun stopScanning(reason: String = "manual") {
619
751
  if (!isScanning) return
620
752
 
@@ -623,6 +755,7 @@ class BleManager(
623
755
  scanCallback = null
624
756
  isScanning = false
625
757
  cancelScanWatchdog()
758
+ cancelConnectionMonitor()
626
759
  lastDiscoveryAt = 0L
627
760
  discoveryLogTimestamps.clear()
628
761
  if (logThrottler.shouldLog("scan_stopped")) {
@@ -649,6 +782,94 @@ class BleManager(
649
782
  mainHandler.removeCallbacks(scanWatchdogRunnable)
650
783
  }
651
784
 
785
+ /**
786
+ * Starts the connection monitor that periodically attempts to reconnect to discovered devices.
787
+ * This mirrors iOS's startConnectionMonitor mechanism for more reliable discovery.
788
+ */
789
+ private fun startConnectionMonitor() {
790
+ cancelConnectionMonitor()
791
+
792
+ connectionMonitorRunnable = object : Runnable {
793
+ override fun run() {
794
+ if (state != TransportState.RUNNING) {
795
+ return
796
+ }
797
+
798
+ val now = System.currentTimeMillis()
799
+
800
+ // Check for discovered devices that aren't connected
801
+ for ((address, observation) in lastSeenMeshAdvertisements) {
802
+ // Skip if already connected
803
+ if (connections.getGatt(address) != null) {
804
+ continue
805
+ }
806
+
807
+ // Skip if we've hit connection cap
808
+ if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
809
+ break
810
+ }
811
+
812
+ // Skip if observation is too old
813
+ if (now - observation.timestamp > MESH_OBSERVATION_TTL_MS) {
814
+ continue
815
+ }
816
+
817
+ // Skip if RSSI too weak
818
+ val rssi = observation.rssi ?: continue
819
+ if (rssi < MINIMUM_RSSI_TO_CONNECT) {
820
+ continue
821
+ }
822
+
823
+ // Rate limit attempts to this device
824
+ val lastAttempt = deviceConnectionAttempts[address]
825
+ if (lastAttempt != null && now - lastAttempt < MIN_RECONNECT_INTERVAL_MS) {
826
+ continue
827
+ }
828
+
829
+ // Try to connect
830
+ try {
831
+ val device = bluetoothAdapter?.getRemoteDevice(address) ?: continue
832
+ recordConnectionAttempt(address, now)
833
+ connectToDevice(device)
834
+ } catch (e: Exception) {
835
+ Log.w(TAG, "Connection monitor: failed to get remote device $address", e)
836
+ }
837
+ }
838
+
839
+ // Also check for pending fragments that need device ID resolution
840
+ for (address in pendingFragments.keys) {
841
+ if (connections.deviceIdForAddress(address) != null) {
842
+ continue
843
+ }
844
+
845
+ val lastAttempt = deviceIdResolutionAttempts[address]
846
+ if (lastAttempt != null && now - lastAttempt < MIN_RECONNECT_INTERVAL_MS) {
847
+ continue
848
+ }
849
+
850
+ deviceIdResolutionAttempts[address] = now
851
+ try {
852
+ val device = bluetoothAdapter?.getRemoteDevice(address)
853
+ if (device != null && connections.getGatt(address) == null) {
854
+ connectToDevice(device)
855
+ }
856
+ } catch (e: Exception) {
857
+ Log.w(TAG, "Connection monitor: failed to resolve device ID for $address", e)
858
+ }
859
+ }
860
+
861
+ mainHandler.postDelayed(this, CONNECTION_MONITOR_INTERVAL_MS)
862
+ }
863
+ }
864
+
865
+ mainHandler.postDelayed(connectionMonitorRunnable!!, CONNECTION_MONITOR_INTERVAL_MS)
866
+ }
867
+
868
+ private fun cancelConnectionMonitor() {
869
+ connectionMonitorRunnable?.let { mainHandler.removeCallbacks(it) }
870
+ connectionMonitorRunnable = null
871
+ }
872
+
652
873
  private fun startAdvertising(reason: String = "manual") {
653
874
  if (isAdvertising) return
654
875
 
@@ -674,7 +895,7 @@ class BleManager(
674
895
 
675
896
  advertiseCallback = object : AdvertiseCallback() {
676
897
  override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
677
- Log.i(TAG, "BLE advertising started successfully (reason=$reason)")
898
+ Log.i(TAG, "BLE advertising started successfully (reason=$reason)")
678
899
  isAdvertising = true
679
900
  lastAdvertiseRestartAt = System.currentTimeMillis()
680
901
  emitDiagnostic("info", "BLE advertising started", mapOf("reason" to reason))
@@ -788,9 +1009,42 @@ class BleManager(
788
1009
  val now = System.currentTimeMillis()
789
1010
  lastDiscoveryAt = now
790
1011
 
1012
+ // Duplicate advertisement detection - avoid processing identical advertisements
1013
+ // This improves performance in dense networks
1014
+ val advertHash = computeAdvertisementHash(result)
1015
+ val cached = recentAdvertisementHashes[address]
1016
+ if (cached != null && cached.hash == advertHash && now - cached.timestamp < 1000L) {
1017
+ return // Skip duplicate advertisement
1018
+ }
1019
+ recentAdvertisementHashes[address] = AdvertisementCacheEntry(advertHash, now)
1020
+
1021
+ // Prune old advertisement cache entries periodically
1022
+ if (recentAdvertisementHashes.size > 100) {
1023
+ val cutoff = now - 30_000L
1024
+ val iterator = recentAdvertisementHashes.entries.iterator()
1025
+ while (iterator.hasNext()) {
1026
+ if (iterator.next().value.timestamp < cutoff) {
1027
+ iterator.remove()
1028
+ }
1029
+ }
1030
+ }
1031
+
791
1032
  // Adaptive scanning: track discoveries for density estimation
792
1033
  recordDiscoveryForDensity(now)
793
1034
 
1035
+ // CRITICAL: Software-based filtering for iOS ↔ Android interoperability
1036
+ // Since we scan without a service UUID filter, we filter here instead.
1037
+ val scanRecord = result.scanRecord
1038
+ val isConnectable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1039
+ result.isConnectable
1040
+ } else {
1041
+ true // Assume connectable on older Android
1042
+ }
1043
+
1044
+ if (!shouldProcessDiscoveredDevice(address, scanRecord, rssi, isConnectable, now)) {
1045
+ return
1046
+ }
1047
+
794
1048
  // Adaptive scanning: early RSSI filtering in dense networks
795
1049
  if (shouldFilterByRssi(rssi)) {
796
1050
  if (logThrottler.shouldLog("adaptive_rssi_filter", intervalMs = 10000)) {
@@ -805,11 +1059,6 @@ class BleManager(
805
1059
  }
806
1060
 
807
1061
  val lastLog = discoveryLogTimestamps[address]
808
- val isConnectable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
809
- result.isConnectable
810
- } else {
811
- null
812
- }
813
1062
  if (lastLog == null || now - lastLog > 30000) {
814
1063
  discoveryLogTimestamps[address] = now
815
1064
  Log.d(TAG, "Discovered device $address RSSI=$rssi (density: $estimatedVisiblePeerCount)")
@@ -819,14 +1068,13 @@ class BleManager(
819
1068
  mapOf(
820
1069
  "address" to address,
821
1070
  "rssi" to rssi,
822
- "connectable" to (isConnectable ?: true),
1071
+ "connectable" to isConnectable,
823
1072
  "visiblePeers" to estimatedVisiblePeerCount
824
1073
  )
825
1074
  )
826
1075
  }
827
1076
  lastSeenRssi[address] = rssi.toShort()
828
1077
 
829
- val scanRecord = result.scanRecord
830
1078
  val serviceData = scanRecord?.getServiceData(ParcelUuid(SERVICE_UUID))
831
1079
  val meshMetadata = MeshAdvertisementData.decode(serviceData)
832
1080
  meshMetadata?.let {
@@ -856,11 +1104,23 @@ class BleManager(
856
1104
  }
857
1105
 
858
1106
  // Adaptive scanning: rate limit connection attempts
859
- if (shouldThrottleConnection(address, now)) {
860
- if (logThrottler.shouldLog("adaptive_throttle_$address", intervalMs = 30000)) {
861
- Log.d(TAG, "Adaptive: throttling connection to $address")
1107
+ // Skip throttling for first-time discoveries with strong signals for faster connection
1108
+ val isFirstDiscovery = !lastSeenMeshAdvertisements.containsKey(address) && connections.getGatt(address) == null
1109
+ val hasStrongSignal = rssi >= -70
1110
+
1111
+ if (!isFirstDiscovery || !hasStrongSignal) {
1112
+ if (shouldThrottleConnection(address, now)) {
1113
+ if (logThrottler.shouldLog("adaptive_throttle_$address", intervalMs = 30000)) {
1114
+ Log.d(TAG, "Adaptive: throttling connection to $address")
1115
+ }
1116
+ return
862
1117
  }
863
- return
1118
+ } else if (isFirstDiscovery && hasStrongSignal) {
1119
+ Log.d(TAG, "Fast-tracking first discovery with strong signal: $address RSSI=$rssi")
1120
+ emitDiagnostic("info", "Fast-tracking first discovery", mapOf(
1121
+ "address" to address,
1122
+ "rssi" to rssi
1123
+ ))
864
1124
  }
865
1125
 
866
1126
  if (!meshController.connectionBudgetAvailable() && decision.evictPeerId != null) {
@@ -898,19 +1158,209 @@ class BleManager(
898
1158
  }
899
1159
 
900
1160
  maybeHandleRebalance("scan")
1161
+
1162
+ // Check if we should proactively refresh the scan
1163
+ maybeProactivelyRefreshScan(now)
1164
+ }
1165
+
1166
+ /**
1167
+ * Determines if a discovered device should be processed.
1168
+ * Implements smart filtering since we scan without a service UUID filter
1169
+ * (required for iOS ↔ Android interoperability).
1170
+ *
1171
+ * Accepts:
1172
+ * - Devices advertising our service UUID
1173
+ * - Devices with our service data
1174
+ * - Previously discovered mesh devices
1175
+ * - Unknown connectable devices (rate-limited, verified via GATT)
1176
+ */
1177
+ private fun shouldProcessDiscoveredDevice(
1178
+ address: String,
1179
+ scanRecord: android.bluetooth.le.ScanRecord?,
1180
+ rssi: Int,
1181
+ isConnectable: Boolean,
1182
+ now: Long
1183
+ ): Boolean {
1184
+ // 1. Check if device is advertising our service UUID
1185
+ val serviceUuids = scanRecord?.serviceUuids
1186
+ if (serviceUuids != null) {
1187
+ for (uuid in serviceUuids) {
1188
+ if (uuid.uuid == SERVICE_UUID) {
1189
+ return true
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ // 2. Check for our service data
1195
+ val serviceData = scanRecord?.getServiceData(ParcelUuid(SERVICE_UUID))
1196
+ if (serviceData != null) {
1197
+ return true
1198
+ }
1199
+
1200
+ // 3. Check if this is a previously discovered mesh device
1201
+ if (lastSeenMeshAdvertisements.containsKey(address)) {
1202
+ return true
1203
+ }
1204
+
1205
+ // 4. Check if we already have a device ID mapping for this device
1206
+ if (connections.deviceIdForAddress(address) != null) {
1207
+ return true
1208
+ }
1209
+
1210
+ // 5. Check if we have an active GATT connection to this device
1211
+ if (connections.getGatt(address) != null) {
1212
+ return true
1213
+ }
1214
+
1215
+ // 6. For unknown connectable devices, allow with rate limiting
1216
+ // These will be verified via GATT service discovery after connection
1217
+ // This is CRITICAL for iOS ↔ Android cross-platform discovery since
1218
+ // each platform may not recognize the other's service UUID format in advertisements
1219
+ if (isConnectable) {
1220
+ // Rate limit unknown device connection attempts
1221
+ val lastAttempt = unknownDeviceAttempts[address]
1222
+ if (lastAttempt != null && now - lastAttempt < UNKNOWN_DEVICE_RATE_LIMIT_MS) {
1223
+ return false
1224
+ }
1225
+
1226
+ // Check global rate limit for unknown device attempts
1227
+ val recentUnknownAttempts = unknownDeviceAttempts.values.count {
1228
+ now - it < 60_000L
1229
+ }
1230
+ if (recentUnknownAttempts >= MAX_UNKNOWN_DEVICE_ATTEMPTS_PER_MINUTE) {
1231
+ return false
1232
+ }
1233
+
1234
+ // Only process signals above minimum threshold for unknown devices
1235
+ // Use a tiered approach: stronger signals get priority
1236
+ val shouldAttempt = when {
1237
+ rssi >= -70 -> true // Strong signal - always try
1238
+ rssi >= UNKNOWN_DEVICE_MIN_RSSI && recentUnknownAttempts < MAX_UNKNOWN_DEVICE_ATTEMPTS_PER_MINUTE / 2 -> true
1239
+ else -> false
1240
+ }
1241
+
1242
+ if (shouldAttempt) {
1243
+ unknownDeviceAttempts[address] = now
1244
+ if (logThrottler.shouldLog("unknown_connectable_$address", intervalMs = 30_000)) {
1245
+ Log.d(TAG, "Allowing unknown connectable device for GATT verification: $address RSSI=$rssi")
1246
+ emitDiagnostic("debug", "Allowing unknown device for GATT verification", mapOf(
1247
+ "address" to address,
1248
+ "rssi" to rssi,
1249
+ "recentAttempts" to recentUnknownAttempts
1250
+ ))
1251
+ }
1252
+ return true
1253
+ }
1254
+ }
1255
+
1256
+ // Filter out all other devices (not our mesh network)
1257
+ return false
1258
+ }
1259
+
1260
+ /**
1261
+ * Proactively refreshes the scan periodically to ensure we don't miss devices
1262
+ * due to BLE stack issues or cached state.
1263
+ */
1264
+ private fun maybeProactivelyRefreshScan(now: Long) {
1265
+ if (now - lastProactiveScanRefresh >= PROACTIVE_SCAN_REFRESH_MS) {
1266
+ lastProactiveScanRefresh = now
1267
+ if (logThrottler.shouldLog("proactive_scan_refresh", intervalMs = PROACTIVE_SCAN_REFRESH_MS)) {
1268
+ Log.d(TAG, "Proactively refreshing BLE scan")
1269
+ emitDiagnostic("info", "Proactive scan refresh")
1270
+ }
1271
+ restartScanning("proactive_refresh")
1272
+ }
1273
+
1274
+ // Forced complete BLE refresh - more aggressive than proactive refresh
1275
+ // This helps recover from edge cases where the BLE stack becomes stuck
1276
+ val lastForced = if (lastForcedBleRefresh == 0L) transportStartAt else lastForcedBleRefresh
1277
+ if (now - lastForced >= FORCED_BLE_REFRESH_MS) {
1278
+ lastForcedBleRefresh = now
1279
+ if (logThrottler.shouldLog("forced_ble_refresh", intervalMs = FORCED_BLE_REFRESH_MS)) {
1280
+ Log.i(TAG, "Performing forced BLE refresh for reliability")
1281
+ emitDiagnostic("info", "Forced BLE refresh for reliability", mapOf(
1282
+ "connectedPeers" to connections.connectionCount(),
1283
+ "discoveredPeers" to connections.discoveredPeerCount()
1284
+ ))
1285
+ }
1286
+ // Stop and restart both scanning and advertising
1287
+ stopScanning("forced_refresh")
1288
+ refreshAdvertising("forced_refresh")
1289
+ mainHandler.postDelayed({
1290
+ if (state == TransportState.RUNNING) {
1291
+ startScanning("forced_refresh")
1292
+ }
1293
+ }, 500)
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Computes a hash of the advertisement data for duplicate detection.
1299
+ * Uses device address, RSSI bucket, and key advertisement data.
1300
+ */
1301
+ private fun computeAdvertisementHash(result: ScanResult): Int {
1302
+ var hash = result.device.address.hashCode()
1303
+ // Use RSSI buckets of 5 dBm to avoid hash changes from minor signal fluctuations
1304
+ hash = 31 * hash + (result.rssi / 5)
1305
+
1306
+ val scanRecord = result.scanRecord
1307
+ if (scanRecord != null) {
1308
+ // Include service UUIDs
1309
+ scanRecord.serviceUuids?.forEach { uuid ->
1310
+ hash = 31 * hash + uuid.hashCode()
1311
+ }
1312
+
1313
+ // Include service data
1314
+ val serviceData = scanRecord.getServiceData(ParcelUuid(SERVICE_UUID))
1315
+ if (serviceData != null) {
1316
+ hash = 31 * hash + serviceData.contentHashCode()
1317
+ }
1318
+ }
1319
+
1320
+ return hash
901
1321
  }
902
1322
 
1323
+ /** Lock for atomic connection count check and connect operations */
1324
+ private val connectionLock = Any()
1325
+
903
1326
  private fun connectToDevice(device: BluetoothDevice) {
904
1327
  try {
905
- if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
906
- if (logThrottler.shouldLog("mesh_conn_cap", intervalMs = 10000)) {
907
- Log.d(TAG, "Connection cap reached, not connecting to ${device.address}")
1328
+ // Atomic check-and-connect to prevent race conditions
1329
+ synchronized(connectionLock) {
1330
+ // Check RSSI threshold - don't connect to devices with weak signals
1331
+ val rssi = lastSeenRssi[device.address]?.toInt() ?: -60
1332
+ if (rssi < MINIMUM_RSSI_TO_CONNECT) {
1333
+ if (logThrottler.shouldLog("rssi_skip_${device.address}", intervalMs = 10000)) {
1334
+ Log.d(TAG, "Skipping connection to ${device.address} due to weak RSSI ($rssi < $MINIMUM_RSSI_TO_CONNECT)")
1335
+ emitDiagnostic("debug", "Skipping BLE connect due to weak RSSI", mapOf(
1336
+ "address" to device.address,
1337
+ "rssi" to rssi,
1338
+ "threshold" to MINIMUM_RSSI_TO_CONNECT
1339
+ ))
1340
+ }
1341
+ connections.consumePendingRole(device.address)
1342
+ return
908
1343
  }
909
- connections.consumePendingRole(device.address)
910
- return
1344
+
1345
+ if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
1346
+ if (logThrottler.shouldLog("mesh_conn_cap", intervalMs = 10000)) {
1347
+ Log.d(TAG, "Connection cap reached, not connecting to ${device.address}")
1348
+ }
1349
+ connections.consumePendingRole(device.address)
1350
+ return
1351
+ }
1352
+
1353
+ // Double-check we don't already have a connection to this device
1354
+ if (connections.getGatt(device.address) != null) {
1355
+ if (logThrottler.shouldLog("already_connecting_${device.address}", intervalMs = 5000)) {
1356
+ Log.d(TAG, "Already have GATT client for ${device.address}")
1357
+ }
1358
+ return
1359
+ }
1360
+
1361
+ val gatt = device.connectGatt(context, false, gattClientCallback, BluetoothDevice.TRANSPORT_LE)
1362
+ connections.registerGatt(device.address, gatt)
911
1363
  }
912
- val gatt = device.connectGatt(context, false, gattClientCallback, BluetoothDevice.TRANSPORT_LE)
913
- connections.registerGatt(device.address, gatt)
914
1364
 
915
1365
  Log.i(TAG, "Connecting to device: ${device.address}")
916
1366
  emitDiagnostic("info", "Connecting to BLE device", mapOf("address" to device.address))
@@ -921,7 +1371,7 @@ class BleManager(
921
1371
  }
922
1372
  }
923
1373
 
924
- private fun currentConnectionCount(): Int = connections.connectionCount()
1374
+ private fun currentConnectionCount(): Int = connections.connectionCount() + connectedCentrals.size
925
1375
 
926
1376
  private fun refreshSelfMetrics() {
927
1377
  val rssiValues = lastSeenRssi.values.map { it.toInt() }
@@ -990,6 +1440,7 @@ class BleManager(
990
1440
  pendingFragments.remove(address)
991
1441
  pendingOutboundFragments.remove(peerId)
992
1442
  deviceIdResolutionAttempts.remove(address)
1443
+ connectionRetryCount.remove(address)
993
1444
  meshController.registerDisconnection(peerId)
994
1445
  refreshSelfMetrics()
995
1446
 
@@ -1381,6 +1832,13 @@ class BleManager(
1381
1832
 
1382
1833
  /** Checks if we should skip this device based on RSSI filtering. */
1383
1834
  private fun shouldFilterByRssi(rssi: Int): Boolean {
1835
+ // During aggressive discovery phase, don't apply density-based filtering
1836
+ val now = System.currentTimeMillis()
1837
+ if (transportStartAt > 0 && now - transportStartAt < AGGRESSIVE_DISCOVERY_PHASE_MS) {
1838
+ // Only filter out extremely weak signals during aggressive phase
1839
+ return rssi < MINIMUM_RSSI_TO_CONNECT
1840
+ }
1841
+
1384
1842
  // In dense networks, apply stricter RSSI filtering
1385
1843
  val threshold = when {
1386
1844
  estimatedVisiblePeerCount > ADAPTIVE_HIGH_DENSITY_THRESHOLD -> -70
@@ -1392,21 +1850,36 @@ class BleManager(
1392
1850
 
1393
1851
  /** Checks if we should throttle connection attempts based on rate limits. */
1394
1852
  private fun shouldThrottleConnection(address: String, now: Long): Boolean {
1853
+ // During aggressive discovery phase, use much shorter cooldowns
1854
+ val isAggressivePhase = transportStartAt > 0 && now - transportStartAt < AGGRESSIVE_DISCOVERY_PHASE_MS
1855
+
1395
1856
  // Prune old entries
1396
1857
  val oneMinuteAgo = now - 60_000L
1397
1858
  synchronized(globalConnectionAttempts) {
1398
1859
  globalConnectionAttempts.removeAll { it < oneMinuteAgo }
1399
1860
  }
1861
+
1862
+ val effectiveCooldown = if (isAggressivePhase) 5_000L else ADAPTIVE_COOLDOWN_PER_DEVICE_MS
1400
1863
  deviceConnectionAttempts.entries.removeIf {
1401
- now - it.value >= ADAPTIVE_COOLDOWN_PER_DEVICE_MS
1864
+ now - it.value >= effectiveCooldown
1402
1865
  }
1403
1866
 
1404
1867
  // Check per-device cooldown
1405
1868
  val lastAttempt = deviceConnectionAttempts[address]
1406
- if (lastAttempt != null && now - lastAttempt < ADAPTIVE_COOLDOWN_PER_DEVICE_MS) {
1869
+ if (lastAttempt != null && now - lastAttempt < effectiveCooldown) {
1407
1870
  return true
1408
1871
  }
1409
1872
 
1873
+ // During aggressive phase, allow more connection attempts
1874
+ if (isAggressivePhase) {
1875
+ // Allow up to 3x the normal rate during aggressive phase
1876
+ val maxAttempts = ADAPTIVE_MAX_CONNECTIONS_PER_MINUTE * 3
1877
+ if (globalConnectionAttempts.size >= maxAttempts) {
1878
+ return true
1879
+ }
1880
+ return false
1881
+ }
1882
+
1410
1883
  // In dense networks, apply global rate limiting
1411
1884
  if (estimatedVisiblePeerCount > ADAPTIVE_LOW_DENSITY_THRESHOLD) {
1412
1885
  val currentAttempts = globalConnectionAttempts.size
@@ -1537,12 +2010,19 @@ class BleManager(
1537
2010
  gattServer?.cancelConnection(device)
1538
2011
  return
1539
2012
  }
2013
+ // Check connection capacity
2014
+ if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
2015
+ Log.w(TAG, "Rejecting inbound connection from ${device.address}: connection cap reached")
2016
+ gattServer?.cancelConnection(device)
2017
+ return
2018
+ }
1540
2019
  val role = when (decision.intent) {
1541
2020
  ConnectionIntent.INTER_CLUSTER -> MeshRole.BRIDGE
1542
2021
  ConnectionIntent.INTRA_CLUSTER, ConnectionIntent.REJECTED -> MeshRole.MEMBER
1543
2022
  }
1544
2023
  connections.trackServerConnection(device.address)
1545
2024
  connections.setPendingRole(device.address, role)
2025
+ connectedCentrals[device.address] = System.currentTimeMillis()
1546
2026
  Log.i(TAG, "GATT server: Device connected: ${device.address} (role=$role)")
1547
2027
  emitDiagnostic("info", "Device connected to GATT server", mapOf("address" to device.address))
1548
2028
  }
@@ -1550,6 +2030,7 @@ class BleManager(
1550
2030
  val address = device.address
1551
2031
  connections.untrackServerConnection(address)
1552
2032
  connections.consumePendingRole(address)
2033
+ connectedCentrals.remove(address)
1553
2034
  // Don't immediately remove - connection might be re-established
1554
2035
  // Only remove if it's a permanent error (status != 0)
1555
2036
  if (status != 0 && status != 19) { // Not normal disconnect or connection timeout
@@ -1676,7 +2157,43 @@ class BleManager(
1676
2157
 
1677
2158
  // Try to reconnect if we were connected and state is still running
1678
2159
  if (wasConnected && state == TransportState.RUNNING) {
1679
- // Attempt reconnection after a short delay
2160
+ // Increment retry count and calculate backoff
2161
+ val retryCount = (connectionRetryCount[address] ?: 0) + 1
2162
+ connectionRetryCount[address] = retryCount
2163
+
2164
+ // Give up after max retries
2165
+ if (retryCount > MAX_CONNECTION_RETRIES) {
2166
+ Log.w(TAG, "Max retries ($MAX_CONNECTION_RETRIES) exceeded for $address on disconnect, giving up")
2167
+ emitDiagnostic("warning", "Max connection retries exceeded", mapOf(
2168
+ "address" to address,
2169
+ "retryCount" to retryCount
2170
+ ))
2171
+ connectionRetryCount.remove(address)
2172
+ // Notify peer lost since we're giving up
2173
+ connections.deviceIdForAddress(address)?.let { peerId ->
2174
+ protocol.removeNeighborRoutes(peerId)
2175
+ try {
2176
+ protocol.blePeerLost(peerId)
2177
+ } catch (e: Exception) {
2178
+ Log.e(TAG, "Error notifying peer lost", e)
2179
+ }
2180
+ meshController.registerDisconnection(peerId)
2181
+ refreshSelfMetrics()
2182
+ connections.removeIdentifiersForAddress(address)
2183
+ connections.removeConnectionRole(peerId)
2184
+ mainHandler.post {
2185
+ if (state == TransportState.RUNNING) {
2186
+ refreshAdvertising("disconnect_max_retries")
2187
+ }
2188
+ }
2189
+ maybeHandleRebalance("disconnect_max_retries")
2190
+ }
2191
+ return@onConnectionStateChange
2192
+ }
2193
+
2194
+ // Exponential backoff: 5s, 10s, 20s, 40s, 60s (capped)
2195
+ val backoffInterval = minOf(MAX_RECONNECT_INTERVAL_MS, MIN_RECONNECT_INTERVAL_MS * (1L shl (retryCount - 1)))
2196
+
1680
2197
  mainHandler.postDelayed({
1681
2198
  if (state == TransportState.RUNNING && connections.hasDeviceForAddress(address)) {
1682
2199
  try {
@@ -1688,7 +2205,7 @@ class BleManager(
1688
2205
  Log.e(TAG, "Error reconnecting to device", e)
1689
2206
  }
1690
2207
  }
1691
- }, 1000)
2208
+ }, backoffInterval)
1692
2209
  } else {
1693
2210
  // Notify protocol of peer loss only if we're not reconnecting
1694
2211
  connections.deviceIdForAddress(address)?.let { deviceId ->
@@ -1779,6 +2296,7 @@ class BleManager(
1779
2296
  if (deviceIdValue != null) {
1780
2297
  Log.i(TAG, "📝 Mapping ${gatt.device.address} -> $deviceIdValue")
1781
2298
  connections.setDeviceIdentifier(gatt.device.address, deviceIdValue)
2299
+ connectionRetryCount.remove(gatt.device.address) // Reset retry count on successful connection
1782
2300
 
1783
2301
  val role = connections.consumePendingRole(gatt.device.address) ?: MeshRole.MEMBER
1784
2302
  meshController.registerConnection(deviceIdValue, role)